@caribeng/ui 22.1.1 → 22.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +48 -8
  2. package/fesm2022/caribeng-ui-accordion.mjs +2 -2
  3. package/fesm2022/caribeng-ui-anchor.mjs +169 -0
  4. package/fesm2022/caribeng-ui-autocomplete.mjs +10 -8
  5. package/fesm2022/caribeng-ui-avatargroup.mjs +9 -3
  6. package/fesm2022/caribeng-ui-blockui.mjs +12 -4
  7. package/fesm2022/caribeng-ui-breadcrumb.mjs +5 -2
  8. package/fesm2022/caribeng-ui-button.mjs +22 -2
  9. package/fesm2022/caribeng-ui-caribe-alert.mjs +5 -2
  10. package/fesm2022/caribeng-ui-caribe-animation-lottie.mjs +6 -3
  11. package/fesm2022/caribeng-ui-caribe-button-group.mjs +6 -3
  12. package/fesm2022/caribeng-ui-caribe-calendar.mjs +11 -6
  13. package/fesm2022/caribeng-ui-caribe-card-content.mjs +6 -3
  14. package/fesm2022/caribeng-ui-caribe-card-move.mjs +29 -7
  15. package/fesm2022/caribeng-ui-caribe-card-select.mjs +56 -14
  16. package/fesm2022/caribeng-ui-caribe-circle-loading.mjs +10 -7
  17. package/fesm2022/caribeng-ui-caribe-coachmark.mjs +42 -19
  18. package/fesm2022/caribeng-ui-caribe-data-table.mjs +15 -9
  19. package/fesm2022/caribeng-ui-caribe-dots-indicator.mjs +38 -5
  20. package/fesm2022/caribeng-ui-caribe-expansion-panel.mjs +16 -5
  21. package/fesm2022/caribeng-ui-caribe-fab-menu.mjs +56 -7
  22. package/fesm2022/caribeng-ui-caribe-file-download.mjs +68 -27
  23. package/fesm2022/caribeng-ui-caribe-file-select.mjs +22 -17
  24. package/fesm2022/caribeng-ui-caribe-float-menu.mjs +6 -3
  25. package/fesm2022/caribeng-ui-caribe-footer.mjs +8 -4
  26. package/fesm2022/caribeng-ui-caribe-header.mjs +6 -3
  27. package/fesm2022/caribeng-ui-caribe-icon-button.mjs +6 -3
  28. package/fesm2022/caribeng-ui-caribe-illustration.mjs +6 -3
  29. package/fesm2022/caribeng-ui-caribe-inline-alert.mjs +5 -2
  30. package/fesm2022/caribeng-ui-caribe-input-search.mjs +14 -10
  31. package/fesm2022/caribeng-ui-caribe-input-select.mjs +12 -10
  32. package/fesm2022/caribeng-ui-caribe-input-token.mjs +61 -11
  33. package/fesm2022/caribeng-ui-caribe-link.mjs +7 -2
  34. package/fesm2022/caribeng-ui-caribe-loader.mjs +8 -5
  35. package/fesm2022/caribeng-ui-caribe-modal.mjs +32 -16
  36. package/fesm2022/caribeng-ui-caribe-off-canvas.mjs +26 -12
  37. package/fesm2022/caribeng-ui-caribe-onboarding.mjs +44 -19
  38. package/fesm2022/caribeng-ui-caribe-pictogram.mjs +6 -3
  39. package/fesm2022/caribeng-ui-caribe-pop-over.mjs +34 -10
  40. package/fesm2022/caribeng-ui-caribe-preloader.mjs +9 -6
  41. package/fesm2022/caribeng-ui-caribe-progress-bar.mjs +5 -2
  42. package/fesm2022/caribeng-ui-caribe-shortcut.mjs +8 -3
  43. package/fesm2022/caribeng-ui-caribe-step-indicator.mjs +12 -5
  44. package/fesm2022/caribeng-ui-caribe-stroke.mjs +6 -3
  45. package/fesm2022/caribeng-ui-caribe-switch.mjs +5 -3
  46. package/fesm2022/caribeng-ui-caribe-tag-button.mjs +1 -1
  47. package/fesm2022/caribeng-ui-caribe-tracker.mjs +5 -2
  48. package/fesm2022/caribeng-ui-caribe-transaction-status.mjs +1 -1
  49. package/fesm2022/caribeng-ui-caribe-voucher.mjs +11 -8
  50. package/fesm2022/caribeng-ui-carousel.mjs +49 -14
  51. package/fesm2022/caribeng-ui-cascadeselect.mjs +22 -8
  52. package/fesm2022/caribeng-ui-chart.mjs +2598 -303
  53. package/fesm2022/caribeng-ui-chip.mjs +9 -4
  54. package/fesm2022/caribeng-ui-chips.mjs +9 -5
  55. package/fesm2022/caribeng-ui-colorpicker.mjs +9 -7
  56. package/fesm2022/caribeng-ui-confirmdialog.mjs +74 -9
  57. package/fesm2022/caribeng-ui-confirmpopup.mjs +134 -9
  58. package/fesm2022/caribeng-ui-contextmenu.mjs +6 -3
  59. package/fesm2022/caribeng-ui-dataview.mjs +211 -6
  60. package/fesm2022/caribeng-ui-datepicker.mjs +1630 -393
  61. package/fesm2022/caribeng-ui-descriptions.mjs +124 -0
  62. package/fesm2022/caribeng-ui-dialog.mjs +10 -7
  63. package/fesm2022/caribeng-ui-dock.mjs +6 -3
  64. package/fesm2022/caribeng-ui-drawer.mjs +10 -7
  65. package/fesm2022/caribeng-ui-dynamicdialog.mjs +210 -6
  66. package/fesm2022/caribeng-ui-editor.mjs +4392 -243
  67. package/fesm2022/caribeng-ui-empty.mjs +80 -0
  68. package/fesm2022/caribeng-ui-file-card.mjs +2 -2
  69. package/fesm2022/caribeng-ui-fileupload.mjs +96 -19
  70. package/fesm2022/caribeng-ui-floatlabel.mjs +4 -2
  71. package/fesm2022/caribeng-ui-folder-card.mjs +10 -3
  72. package/fesm2022/caribeng-ui-galleria.mjs +76 -45
  73. package/fesm2022/caribeng-ui-image.mjs +52 -29
  74. package/fesm2022/caribeng-ui-inplace.mjs +22 -6
  75. package/fesm2022/caribeng-ui-input-number.mjs +9 -7
  76. package/fesm2022/caribeng-ui-inputgroup.mjs +2 -2
  77. package/fesm2022/caribeng-ui-inputmask.mjs +14 -11
  78. package/fesm2022/caribeng-ui-inputotp.mjs +12 -6
  79. package/fesm2022/caribeng-ui-keyfilter.mjs +72 -0
  80. package/fesm2022/caribeng-ui-listbox.mjs +13 -11
  81. package/fesm2022/caribeng-ui-megamenu.mjs +222 -30
  82. package/fesm2022/caribeng-ui-menu.mjs +11 -5
  83. package/fesm2022/caribeng-ui-menubar.mjs +10 -5
  84. package/fesm2022/caribeng-ui-message.mjs +7 -4
  85. package/fesm2022/caribeng-ui-metergroup.mjs +29 -11
  86. package/fesm2022/caribeng-ui-multiselect.mjs +399 -97
  87. package/fesm2022/caribeng-ui-orderlist.mjs +230 -11
  88. package/fesm2022/caribeng-ui-organizationchart.mjs +12 -7
  89. package/fesm2022/caribeng-ui-paginator.mjs +18 -15
  90. package/fesm2022/caribeng-ui-panel.mjs +5 -2
  91. package/fesm2022/caribeng-ui-panelmenu.mjs +6 -3
  92. package/fesm2022/caribeng-ui-password.mjs +8 -6
  93. package/fesm2022/caribeng-ui-pdfviewer.mjs +1553 -0
  94. package/fesm2022/caribeng-ui-picklist.mjs +251 -14
  95. package/fesm2022/caribeng-ui-pricing.mjs +17 -14
  96. package/fesm2022/caribeng-ui-rating.mjs +7 -3
  97. package/fesm2022/caribeng-ui-repeater.mjs +223 -0
  98. package/fesm2022/caribeng-ui-result.mjs +91 -0
  99. package/fesm2022/caribeng-ui-scrollpanel.mjs +6 -3
  100. package/fesm2022/caribeng-ui-scrolltop.mjs +6 -3
  101. package/fesm2022/caribeng-ui-select.mjs +379 -87
  102. package/fesm2022/caribeng-ui-selectbutton.mjs +2 -2
  103. package/fesm2022/caribeng-ui-slider.mjs +171 -75
  104. package/fesm2022/caribeng-ui-speeddial.mjs +7 -4
  105. package/fesm2022/caribeng-ui-spinner.mjs +5 -2
  106. package/fesm2022/caribeng-ui-splitbutton.mjs +163 -0
  107. package/fesm2022/caribeng-ui-splitter.mjs +5 -2
  108. package/fesm2022/caribeng-ui-statistic.mjs +183 -0
  109. package/fesm2022/caribeng-ui-stepper.mjs +8 -5
  110. package/fesm2022/caribeng-ui-steps.mjs +6 -3
  111. package/fesm2022/caribeng-ui-table.mjs +708 -82
  112. package/fesm2022/caribeng-ui-tabmenu.mjs +6 -3
  113. package/fesm2022/caribeng-ui-tag.mjs +2 -2
  114. package/fesm2022/caribeng-ui-terminal.mjs +45 -9
  115. package/fesm2022/caribeng-ui-themetoggle.mjs +82 -0
  116. package/fesm2022/caribeng-ui-tieredmenu.mjs +9 -6
  117. package/fesm2022/caribeng-ui-timeline.mjs +175 -6
  118. package/fesm2022/caribeng-ui-toast.mjs +7 -4
  119. package/fesm2022/caribeng-ui-togglebutton.mjs +8 -6
  120. package/fesm2022/caribeng-ui-toolbar.mjs +6 -3
  121. package/fesm2022/caribeng-ui-tree.mjs +302 -6
  122. package/fesm2022/caribeng-ui-treeselect.mjs +26 -10
  123. package/fesm2022/caribeng-ui-treetable.mjs +269 -6
  124. package/fesm2022/caribeng-ui-virtualscroller.mjs +123 -10
  125. package/fesm2022/caribeng-ui.mjs +829 -6
  126. package/package.json +53 -3
  127. package/schematics/collection.json +10 -0
  128. package/schematics/ng-add/index.js +125 -0
  129. package/schematics/ng-add/schema.js +2 -0
  130. package/schematics/ng-add/schema.json +66 -0
  131. package/styles/caribeng.css +380 -0
  132. package/styles/scss/_mixins.scss +46 -0
  133. package/styles/scss/_overlay.scss +15 -0
  134. package/styles/scss/_tokens.scss +267 -0
  135. package/styles/scss/_utilities.scss +73 -0
  136. package/styles/scss/_variables.scss +33 -0
  137. package/styles/scss/index.scss +16 -0
  138. package/types/caribeng-ui-anchor.d.ts +50 -0
  139. package/types/caribeng-ui-autocomplete.d.ts +4 -2
  140. package/types/caribeng-ui-avatargroup.d.ts +3 -1
  141. package/types/caribeng-ui-blockui.d.ts +9 -4
  142. package/types/caribeng-ui-breadcrumb.d.ts +3 -0
  143. package/types/caribeng-ui-button.d.ts +9 -1
  144. package/types/caribeng-ui-caribe-alert.d.ts +3 -0
  145. package/types/caribeng-ui-caribe-animation-lottie.d.ts +4 -1
  146. package/types/caribeng-ui-caribe-button-group.d.ts +4 -1
  147. package/types/caribeng-ui-caribe-calendar.d.ts +5 -1
  148. package/types/caribeng-ui-caribe-card-content.d.ts +4 -1
  149. package/types/caribeng-ui-caribe-card-move.d.ts +13 -13
  150. package/types/caribeng-ui-caribe-card-select.d.ts +10 -1
  151. package/types/caribeng-ui-caribe-circle-loading.d.ts +4 -1
  152. package/types/caribeng-ui-caribe-coachmark.d.ts +6 -0
  153. package/types/caribeng-ui-caribe-data-table.d.ts +7 -2
  154. package/types/caribeng-ui-caribe-dots-indicator.d.ts +9 -0
  155. package/types/caribeng-ui-caribe-expansion-panel.d.ts +7 -10
  156. package/types/caribeng-ui-caribe-fab-menu.d.ts +10 -1
  157. package/types/caribeng-ui-caribe-file-download.d.ts +8 -10
  158. package/types/caribeng-ui-caribe-file-select.d.ts +7 -1
  159. package/types/caribeng-ui-caribe-float-menu.d.ts +4 -1
  160. package/types/caribeng-ui-caribe-header.d.ts +3 -0
  161. package/types/caribeng-ui-caribe-icon-button.d.ts +4 -1
  162. package/types/caribeng-ui-caribe-illustration.d.ts +4 -1
  163. package/types/caribeng-ui-caribe-inline-alert.d.ts +3 -0
  164. package/types/caribeng-ui-caribe-input-search.d.ts +4 -2
  165. package/types/caribeng-ui-caribe-input-select.d.ts +4 -2
  166. package/types/caribeng-ui-caribe-input-token.d.ts +16 -1
  167. package/types/caribeng-ui-caribe-link.d.ts +3 -0
  168. package/types/caribeng-ui-caribe-loader.d.ts +4 -1
  169. package/types/caribeng-ui-caribe-modal.d.ts +7 -10
  170. package/types/caribeng-ui-caribe-off-canvas.d.ts +6 -1
  171. package/types/caribeng-ui-caribe-onboarding.d.ts +6 -0
  172. package/types/caribeng-ui-caribe-pictogram.d.ts +4 -1
  173. package/types/caribeng-ui-caribe-pop-over.d.ts +7 -10
  174. package/types/caribeng-ui-caribe-preloader.d.ts +4 -1
  175. package/types/caribeng-ui-caribe-progress-bar.d.ts +3 -0
  176. package/types/caribeng-ui-caribe-shortcut.d.ts +3 -1
  177. package/types/caribeng-ui-caribe-step-indicator.d.ts +6 -0
  178. package/types/caribeng-ui-caribe-stroke.d.ts +4 -1
  179. package/types/caribeng-ui-caribe-switch.d.ts +3 -1
  180. package/types/caribeng-ui-caribe-tracker.d.ts +3 -0
  181. package/types/caribeng-ui-caribe-voucher.d.ts +5 -2
  182. package/types/caribeng-ui-carousel.d.ts +10 -1
  183. package/types/caribeng-ui-cascadeselect.d.ts +7 -2
  184. package/types/caribeng-ui-chart.d.ts +469 -44
  185. package/types/caribeng-ui-chip.d.ts +6 -0
  186. package/types/caribeng-ui-chips.d.ts +6 -2
  187. package/types/caribeng-ui-colorpicker.d.ts +3 -1
  188. package/types/caribeng-ui-confirmdialog.d.ts +25 -6
  189. package/types/caribeng-ui-confirmpopup.d.ts +37 -5
  190. package/types/caribeng-ui-contextmenu.d.ts +4 -1
  191. package/types/caribeng-ui-dataview.d.ts +63 -5
  192. package/types/caribeng-ui-datepicker.d.ts +186 -21
  193. package/types/caribeng-ui-descriptions.d.ts +63 -0
  194. package/types/caribeng-ui-dialog.d.ts +4 -1
  195. package/types/caribeng-ui-dock.d.ts +4 -1
  196. package/types/caribeng-ui-drawer.d.ts +4 -1
  197. package/types/caribeng-ui-dynamicdialog.d.ts +93 -3
  198. package/types/caribeng-ui-editor.d.ts +486 -13
  199. package/types/caribeng-ui-empty.d.ts +34 -0
  200. package/types/caribeng-ui-file-card.d.ts +1 -1
  201. package/types/caribeng-ui-fileupload.d.ts +27 -4
  202. package/types/caribeng-ui-folder-card.d.ts +4 -1
  203. package/types/caribeng-ui-galleria.d.ts +9 -0
  204. package/types/caribeng-ui-image.d.ts +10 -0
  205. package/types/caribeng-ui-inplace.d.ts +8 -0
  206. package/types/caribeng-ui-input-number.d.ts +3 -1
  207. package/types/caribeng-ui-inputmask.d.ts +5 -0
  208. package/types/caribeng-ui-inputotp.d.ts +7 -2
  209. package/types/caribeng-ui-keyfilter.d.ts +30 -0
  210. package/types/caribeng-ui-listbox.d.ts +5 -3
  211. package/types/caribeng-ui-megamenu.d.ts +26 -2
  212. package/types/caribeng-ui-menu.d.ts +7 -2
  213. package/types/caribeng-ui-menubar.d.ts +5 -1
  214. package/types/caribeng-ui-message.d.ts +3 -0
  215. package/types/caribeng-ui-metergroup.d.ts +9 -1
  216. package/types/caribeng-ui-multiselect.d.ts +71 -10
  217. package/types/caribeng-ui-orderlist.d.ts +58 -6
  218. package/types/caribeng-ui-organizationchart.d.ts +7 -1
  219. package/types/caribeng-ui-paginator.d.ts +3 -0
  220. package/types/caribeng-ui-panel.d.ts +3 -0
  221. package/types/caribeng-ui-panelmenu.d.ts +4 -1
  222. package/types/caribeng-ui-password.d.ts +4 -2
  223. package/types/caribeng-ui-pdfviewer.d.ts +258 -0
  224. package/types/caribeng-ui-picklist.d.ts +67 -7
  225. package/types/caribeng-ui-pricing.d.ts +8 -5
  226. package/types/caribeng-ui-rating.d.ts +6 -2
  227. package/types/caribeng-ui-repeater.d.ts +79 -0
  228. package/types/caribeng-ui-result.d.ts +35 -0
  229. package/types/caribeng-ui-scrollpanel.d.ts +4 -1
  230. package/types/caribeng-ui-scrolltop.d.ts +4 -1
  231. package/types/caribeng-ui-select.d.ts +73 -9
  232. package/types/caribeng-ui-slider.d.ts +63 -10
  233. package/types/caribeng-ui-speeddial.d.ts +5 -2
  234. package/types/caribeng-ui-spinner.d.ts +3 -0
  235. package/types/caribeng-ui-splitbutton.d.ts +49 -0
  236. package/types/caribeng-ui-splitter.d.ts +3 -0
  237. package/types/caribeng-ui-statistic.d.ts +65 -0
  238. package/types/caribeng-ui-stepper.d.ts +3 -0
  239. package/types/caribeng-ui-steps.d.ts +4 -1
  240. package/types/caribeng-ui-table.d.ts +184 -16
  241. package/types/caribeng-ui-tabmenu.d.ts +4 -1
  242. package/types/caribeng-ui-terminal.d.ts +10 -0
  243. package/types/caribeng-ui-themetoggle.d.ts +32 -0
  244. package/types/caribeng-ui-tieredmenu.d.ts +5 -2
  245. package/types/caribeng-ui-timeline.d.ts +63 -4
  246. package/types/caribeng-ui-toast.d.ts +3 -0
  247. package/types/caribeng-ui-togglebutton.d.ts +5 -3
  248. package/types/caribeng-ui-toolbar.d.ts +7 -4
  249. package/types/caribeng-ui-tree.d.ts +76 -4
  250. package/types/caribeng-ui-treeselect.d.ts +10 -2
  251. package/types/caribeng-ui-treetable.d.ts +68 -4
  252. package/types/caribeng-ui-virtualscroller.d.ts +45 -5
  253. package/types/caribeng-ui.d.ts +501 -12
@@ -1,25 +1,38 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, model, signal, computed, viewChild, effect, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { inject, Injector, input, model, signal, computed, viewChild, effect, DestroyRef, afterNextRender, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { NgTemplateOutlet } from '@angular/common';
3
4
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
5
  import * as i1 from '@angular/cdk/overlay';
5
6
  import { OverlayModule } from '@angular/cdk/overlay';
6
- import { cbFieldState } from '@caribeng/ui';
7
+ import { injectCbTranslation, cbFieldState } from '@caribeng/ui';
7
8
  import { CbIcon } from '@caribeng/ui/icon';
8
9
 
9
10
  let nextUid = 0;
11
+ /** Filas extra que se pintan por encima y por debajo de las visibles en modo virtual. */
12
+ const VIRTUAL_BUFFER = 4;
10
13
  /**
11
14
  * Select (dropdown) de CaribeNG. Listbox accesible sobre CDK Overlay con
12
- * navegación por teclado, filtro opcional e integración con Angular Forms
15
+ * navegación por teclado, búsqueda al teclear, filtro opcional, grupos,
16
+ * scroll virtual para listas grandes e integración con Angular Forms
13
17
  * mediante `ControlValueAccessor`.
14
18
  *
15
19
  * @example
16
20
  * ```html
17
21
  * <cb-select [options]="ciudades" optionLabel="nombre" optionValue="id"
18
- * formControlName="ciudad" placeholder="Elige una ciudad" />
22
+ * formControlName="ciudad" placeholder="Elige una ciudad" showClear />
23
+ *
24
+ * <!-- Agrupado: [{ label: 'Colombia', items: [...] }, …] -->
25
+ * <cb-select [options]="paises" group optionLabel="nombre" />
26
+ *
27
+ * <!-- 10 000 opciones -->
28
+ * <cb-select [options]="clientes" virtualScroll [virtualScrollItemSize]="36" filter />
19
29
  * ```
20
30
  */
21
31
  class CbSelect {
22
32
  constructor() {
33
+ /** Textos de interfaz en el idioma activo (`provideCaribeNG({ locale })`). */
34
+ this.t = injectCbTranslation();
35
+ this.injector = inject(Injector);
23
36
  // --- Inputs ----------------------------------------------------------------
24
37
  this.options = input([], /* @ts-ignore */
25
38
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
@@ -29,15 +42,33 @@ class CbSelect {
29
42
  ...(ngDevMode ? [{ debugName: "optionValue" }] : /* istanbul ignore next */ []));
30
43
  this.optionDisabled = input('disabled', /* @ts-ignore */
31
44
  ...(ngDevMode ? [{ debugName: "optionDisabled" }] : /* istanbul ignore next */ []));
32
- this.placeholder = input('Seleccionar', /* @ts-ignore */
45
+ /** Las opciones son grupos (`{ label, items }`): se muestran con su título. */
46
+ this.group = input(false, { ...(ngDevMode ? { debugName: "group" } : /* istanbul ignore next */ {}), transform: toBoolean });
47
+ /** Propiedad con el título de cada grupo. */
48
+ this.optionGroupLabel = input('label', /* @ts-ignore */
49
+ ...(ngDevMode ? [{ debugName: "optionGroupLabel" }] : /* istanbul ignore next */ []));
50
+ /** Propiedad con las opciones de cada grupo. */
51
+ this.optionGroupChildren = input('items', /* @ts-ignore */
52
+ ...(ngDevMode ? [{ debugName: "optionGroupChildren" }] : /* istanbul ignore next */ []));
53
+ this.placeholder = input(undefined, /* @ts-ignore */
33
54
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
34
55
  this.size = input('md', /* @ts-ignore */
35
56
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
36
57
  this.filter = input(false, { ...(ngDevMode ? { debugName: "filter" } : /* istanbul ignore next */ {}), transform: toBoolean });
37
- this.filterPlaceholder = input('Buscar…', /* @ts-ignore */
58
+ this.filterPlaceholder = input(undefined, /* @ts-ignore */
38
59
  ...(ngDevMode ? [{ debugName: "filterPlaceholder" }] : /* istanbul ignore next */ []));
39
- this.emptyMessage = input('Sin resultados', /* @ts-ignore */
60
+ this.emptyMessage = input(undefined, /* @ts-ignore */
40
61
  ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
62
+ /** Muestra un botón para vaciar la selección. */
63
+ this.showClear = input(false, { ...(ngDevMode ? { debugName: "showClear" } : /* istanbul ignore next */ {}), transform: toBoolean });
64
+ /** Alto máximo de la lista (CSS). En modo virtual es su alto fijo. */
65
+ this.scrollHeight = input('16rem', /* @ts-ignore */
66
+ ...(ngDevMode ? [{ debugName: "scrollHeight" }] : /* istanbul ignore next */ []));
67
+ /** Pinta solo las opciones visibles: para listas de miles de elementos. */
68
+ this.virtualScroll = input(false, { ...(ngDevMode ? { debugName: "virtualScroll" } : /* istanbul ignore next */ {}), transform: toBoolean });
69
+ /** Alto fijo de cada fila en modo virtual (px). */
70
+ this.virtualScrollItemSize = input(36, /* @ts-ignore */
71
+ ...(ngDevMode ? [{ debugName: "virtualScrollItemSize" }] : /* istanbul ignore next */ []));
41
72
  this.invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: toBoolean });
42
73
  /** Estado de validación: inputs manuales + estado de Angular Forms. */
43
74
  this.fieldState = cbFieldState({ invalid: () => this.invalid() });
@@ -62,6 +93,8 @@ class CbSelect {
62
93
  ...(ngDevMode ? [{ debugName: "filterText" }] : /* istanbul ignore next */ []));
63
94
  this.triggerWidth = signal(0, /* @ts-ignore */
64
95
  ...(ngDevMode ? [{ debugName: "triggerWidth" }] : /* istanbul ignore next */ []));
96
+ this.scrollTop = signal(0, /* @ts-ignore */
97
+ ...(ngDevMode ? [{ debugName: "scrollTop" }] : /* istanbul ignore next */ []));
65
98
  this.cvaDisabled = signal(false, /* @ts-ignore */
66
99
  ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
67
100
  this.isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
@@ -70,32 +103,63 @@ class CbSelect {
70
103
  this.triggerRef = viewChild.required('trigger');
71
104
  this.filterInputRef = viewChild('filterInput', /* @ts-ignore */
72
105
  ...(ngDevMode ? [{ debugName: "filterInputRef" }] : /* istanbul ignore next */ []));
106
+ this.viewportRef = viewChild('viewport', /* @ts-ignore */
107
+ ...(ngDevMode ? [{ debugName: "viewportRef" }] : /* istanbul ignore next */ []));
73
108
  this.positions = [
74
109
  { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
75
110
  { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
76
111
  ];
112
+ // --- Búsqueda al teclear ------------------------------------------------------
113
+ this.typeahead = '';
77
114
  // --- Derivados -------------------------------------------------------------
78
- this.filteredOptions = computed(() => {
79
- const q = this.filterText().trim().toLowerCase();
80
- const opts = this.options();
81
- if (!this.filter() || !q)
82
- return opts;
83
- return opts.filter((o) => this.labelOf(o).toLowerCase().includes(q));
115
+ /** Todas las opciones (aplanando los grupos). */
116
+ this.flatOptions = computed(() => this.group() ? this.options().flatMap((g) => this.childrenOf(g)) : this.options(), /* @ts-ignore */
117
+ ...(ngDevMode ? [{ debugName: "flatOptions" }] : /* istanbul ignore next */ []));
118
+ this.filteredGroups = computed(() => {
119
+ const q = normalize(this.filterText());
120
+ const keep = (o) => !this.filter() || !q || normalize(this.labelOf(o)).includes(q);
121
+ let index = 0;
122
+ const indexed = (opts) => opts.filter(keep).map((opt) => ({ opt, index: index++ }));
123
+ if (!this.group())
124
+ return [{ label: null, options: indexed(this.options()) }];
125
+ return this.options()
126
+ .map((g) => ({ label: this.groupLabelOf(g), options: indexed(this.childrenOf(g)) }))
127
+ .filter((g) => g.options.length > 0);
84
128
  }, /* @ts-ignore */
129
+ ...(ngDevMode ? [{ debugName: "filteredGroups" }] : /* istanbul ignore next */ []));
130
+ this.filteredOptions = computed(() => this.filteredGroups().flatMap((g) => g.options.map((o) => o.opt)), /* @ts-ignore */
85
131
  ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
132
+ /** Filas del modo virtual (cabeceras de grupo + opciones). */
133
+ this.virtualRows = computed(() => this.filteredGroups().flatMap((g, gi) => [
134
+ ...(g.label !== null ? [{ kind: 'group', label: g.label, key: `g${gi}` }] : []),
135
+ ...g.options.map((o) => ({ kind: 'option', ...o })),
136
+ ]), /* @ts-ignore */
137
+ ...(ngDevMode ? [{ debugName: "virtualRows" }] : /* istanbul ignore next */ []));
138
+ this.viewportHeight = computed(() => cssToPx(this.scrollHeight()), /* @ts-ignore */
139
+ ...(ngDevMode ? [{ debugName: "viewportHeight" }] : /* istanbul ignore next */ []));
140
+ this.windowStart = computed(() => Math.max(0, Math.floor(this.scrollTop() / this.virtualScrollItemSize()) - VIRTUAL_BUFFER), /* @ts-ignore */
141
+ ...(ngDevMode ? [{ debugName: "windowStart" }] : /* istanbul ignore next */ []));
142
+ this.windowOffset = computed(() => this.windowStart() * this.virtualScrollItemSize(), /* @ts-ignore */
143
+ ...(ngDevMode ? [{ debugName: "windowOffset" }] : /* istanbul ignore next */ []));
144
+ this.visibleRows = computed(() => {
145
+ const size = this.virtualScrollItemSize();
146
+ const end = Math.ceil((this.scrollTop() + this.viewportHeight()) / size) + VIRTUAL_BUFFER;
147
+ return this.virtualRows().slice(this.windowStart(), end);
148
+ }, /* @ts-ignore */
149
+ ...(ngDevMode ? [{ debugName: "visibleRows" }] : /* istanbul ignore next */ []));
86
150
  this.selectedOption = computed(() => {
87
151
  const value = this.value();
88
- return this.options().find((o) => this.valueOf(o) === value) ?? null;
152
+ return this.flatOptions().find((o) => this.valueOf(o) === value) ?? null;
89
153
  }, /* @ts-ignore */
90
154
  ...(ngDevMode ? [{ debugName: "selectedOption" }] : /* istanbul ignore next */ []));
91
155
  this.displayLabel = computed(() => {
92
156
  const selected = this.selectedOption();
93
- return selected ? this.labelOf(selected) : this.placeholder();
157
+ return selected ? this.labelOf(selected) : (this.placeholder() ?? this.t().select);
94
158
  }, /* @ts-ignore */
95
159
  ...(ngDevMode ? [{ debugName: "displayLabel" }] : /* istanbul ignore next */ []));
96
- this.activeDescendant = computed(() => this.open() && this.activeIndex() >= 0
97
- ? this.optionId(this.activeIndex())
98
- : null, /* @ts-ignore */
160
+ this.canClear = computed(() => this.showClear() && this.selectedOption() !== null && !this.isDisabled(), /* @ts-ignore */
161
+ ...(ngDevMode ? [{ debugName: "canClear" }] : /* istanbul ignore next */ []));
162
+ this.activeDescendant = computed(() => this.open() && this.activeIndex() >= 0 ? this.optionId(this.activeIndex()) : null, /* @ts-ignore */
99
163
  ...(ngDevMode ? [{ debugName: "activeDescendant" }] : /* istanbul ignore next */ []));
100
164
  // --- ControlValueAccessor --------------------------------------------------
101
165
  this.onChange = () => { };
@@ -112,6 +176,7 @@ class CbSelect {
112
176
  if (this.open())
113
177
  this.ensureActiveVisible();
114
178
  });
179
+ inject(DestroyRef).onDestroy(() => clearTimeout(this.typeaheadTimer));
115
180
  }
116
181
  writeValue(value) {
117
182
  this.value.set(value ?? null);
@@ -135,9 +200,10 @@ class CbSelect {
135
200
  if (this.isDisabled())
136
201
  return;
137
202
  this.triggerWidth.set(this.triggerRef().nativeElement.offsetWidth);
203
+ this.scrollTop.set(0);
138
204
  this.open.set(true);
139
205
  const selectedIdx = this.filteredOptions().indexOf(this.selectedOption());
140
- this.activeIndex.set(selectedIdx >= 0 ? selectedIdx : this.firstEnabledIndex());
206
+ this.setActive(selectedIdx >= 0 ? selectedIdx : this.firstEnabledIndex());
141
207
  }
142
208
  close() {
143
209
  if (!this.open())
@@ -155,6 +221,13 @@ class CbSelect {
155
221
  this.onChange(value);
156
222
  this.close();
157
223
  }
224
+ /** Vacía la selección (botón de `showClear`) y devuelve el foco al disparador. */
225
+ clear() {
226
+ this.value.set(null);
227
+ this.onChange(null);
228
+ this.onTouched();
229
+ this.triggerRef().nativeElement.focus();
230
+ }
158
231
  onBlur() {
159
232
  if (!this.open())
160
233
  this.onTouched();
@@ -170,6 +243,22 @@ class CbSelect {
170
243
  }
171
244
  onTriggerKeydown(event) {
172
245
  const key = event.key;
246
+ const inFilter = event.target === this.filterInputRef()?.nativeElement;
247
+ if (!inFilter && isPrintable(event)) {
248
+ event.preventDefault();
249
+ if (!this.open()) {
250
+ this.openPanel();
251
+ // Sin selección previa, la búsqueda empieza por la primera opción.
252
+ if (!this.selectedOption())
253
+ this.activeIndex.set(-1);
254
+ }
255
+ // Con filtro, lo tecleado empieza la búsqueda; sin él, salta a la opción que coincide.
256
+ if (this.filter())
257
+ this.filterText.set(this.filterText() + key);
258
+ else
259
+ this.searchByTyping(key);
260
+ return;
261
+ }
173
262
  if (!this.open()) {
174
263
  if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(key)) {
175
264
  event.preventDefault();
@@ -187,12 +276,16 @@ class CbSelect {
187
276
  this.move(-1);
188
277
  break;
189
278
  case 'Home':
279
+ if (inFilter)
280
+ break;
190
281
  event.preventDefault();
191
- this.activeIndex.set(this.firstEnabledIndex());
282
+ this.setActive(this.firstEnabledIndex());
192
283
  break;
193
284
  case 'End':
285
+ if (inFilter)
286
+ break;
194
287
  event.preventDefault();
195
- this.activeIndex.set(this.lastEnabledIndex());
288
+ this.setActive(this.lastEnabledIndex());
196
289
  break;
197
290
  case 'Enter':
198
291
  event.preventDefault();
@@ -232,8 +325,38 @@ class CbSelect {
232
325
  optionId(index) {
233
326
  return `${this.inputId()}-opt-${index}`;
234
327
  }
235
- trackOption(opt) {
236
- return this.valueOf(opt);
328
+ groupId(index) {
329
+ return `${this.inputId()}-group-${index}`;
330
+ }
331
+ groupLabelOf(group) {
332
+ return String(group?.[this.optionGroupLabel()] ?? '');
333
+ }
334
+ childrenOf(group) {
335
+ const items = group?.[this.optionGroupChildren()];
336
+ return Array.isArray(items) ? items : [];
337
+ }
338
+ /** Cambia la opción activa y la desplaza a la vista. */
339
+ setActive(index) {
340
+ this.activeIndex.set(index);
341
+ if (index < 0 || !this.open())
342
+ return;
343
+ if (this.virtualScroll()) {
344
+ const size = this.virtualScrollItemSize();
345
+ const row = this.virtualRows().findIndex((r) => r.kind === 'option' && r.index === index);
346
+ const top = row * size;
347
+ const view = this.viewportHeight();
348
+ const current = this.scrollTop();
349
+ const next = top < current ? top : top + size > current + view ? top + size - view : current;
350
+ if (next !== current) {
351
+ this.scrollTop.set(next);
352
+ const viewport = this.viewportRef()?.nativeElement;
353
+ if (viewport)
354
+ viewport.scrollTop = next;
355
+ }
356
+ }
357
+ else {
358
+ afterNextRender(() => this.viewportRef()?.nativeElement.querySelector(`#${this.optionId(index)}`)?.scrollIntoView?.({ block: 'nearest' }), { injector: this.injector });
359
+ }
237
360
  }
238
361
  move(delta) {
239
362
  const opts = this.filteredOptions();
@@ -243,7 +366,25 @@ class CbSelect {
243
366
  for (let step = 0; step < opts.length; step++) {
244
367
  i = (i + delta + opts.length) % opts.length;
245
368
  if (!this.isOptionDisabled(opts[i])) {
246
- this.activeIndex.set(i);
369
+ this.setActive(i);
370
+ return;
371
+ }
372
+ }
373
+ }
374
+ /** Teclear salta a la siguiente opción que empieza por lo escrito (se reinicia tras 500 ms). */
375
+ searchByTyping(char) {
376
+ clearTimeout(this.typeaheadTimer);
377
+ this.typeahead += normalize(char);
378
+ this.typeaheadTimer = setTimeout(() => (this.typeahead = ''), 500);
379
+ const opts = this.filteredOptions();
380
+ // Repetir la misma letra recorre las opciones que empiezan por ella ("m", "m"…).
381
+ const query = [...this.typeahead].every((c) => c === this.typeahead[0]) ? this.typeahead[0] : this.typeahead;
382
+ // Con una sola letra se avanza a la siguiente coincidencia; con más, se refina desde la actual.
383
+ const from = query.length === 1 ? this.activeIndex() + 1 : Math.max(0, this.activeIndex());
384
+ for (let step = 0; step < opts.length; step++) {
385
+ const i = (from + step) % opts.length;
386
+ if (!this.isOptionDisabled(opts[i]) && normalize(this.labelOf(opts[i])).startsWith(query)) {
387
+ this.setActive(i);
247
388
  return;
248
389
  }
249
390
  }
@@ -266,17 +407,17 @@ class CbSelect {
266
407
  }
267
408
  ensureActiveVisible() {
268
409
  if (this.activeIndex() >= this.filteredOptions().length) {
269
- this.activeIndex.set(this.firstEnabledIndex());
410
+ this.setActive(this.firstEnabledIndex());
270
411
  }
271
412
  }
272
413
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbSelect, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
273
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbSelect, isStandalone: true, selector: "cb-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: false, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: false, transformFunction: null }, optionDisabled: { classPropertyName: "optionDisabled", publicName: "optionDisabled", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, filterPlaceholder: { classPropertyName: "filterPlaceholder", publicName: "filterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledBy: { classPropertyName: "ariaLabelledBy", publicName: "ariaLabelledBy", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { classAttribute: "cb-select-host" }, providers: [
414
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbSelect, isStandalone: true, selector: "cb-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: false, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: false, transformFunction: null }, optionDisabled: { classPropertyName: "optionDisabled", publicName: "optionDisabled", isSignal: true, isRequired: false, transformFunction: null }, group: { classPropertyName: "group", publicName: "group", isSignal: true, isRequired: false, transformFunction: null }, optionGroupLabel: { classPropertyName: "optionGroupLabel", publicName: "optionGroupLabel", isSignal: true, isRequired: false, transformFunction: null }, optionGroupChildren: { classPropertyName: "optionGroupChildren", publicName: "optionGroupChildren", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, filterPlaceholder: { classPropertyName: "filterPlaceholder", publicName: "filterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, showClear: { classPropertyName: "showClear", publicName: "showClear", isSignal: true, isRequired: false, transformFunction: null }, scrollHeight: { classPropertyName: "scrollHeight", publicName: "scrollHeight", isSignal: true, isRequired: false, transformFunction: null }, virtualScroll: { classPropertyName: "virtualScroll", publicName: "virtualScroll", isSignal: true, isRequired: false, transformFunction: null }, virtualScrollItemSize: { classPropertyName: "virtualScrollItemSize", publicName: "virtualScrollItemSize", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledBy: { classPropertyName: "ariaLabelledBy", publicName: "ariaLabelledBy", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { properties: { "class.cb-has-clear": "canClear()" }, classAttribute: "cb-select-host" }, providers: [
274
415
  {
275
416
  provide: NG_VALUE_ACCESSOR,
276
417
  useExisting: forwardRef(() => CbSelect),
277
418
  multi: true,
278
419
  },
279
- ], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "filterInputRef", first: true, predicate: ["filterInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
420
+ ], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "filterInputRef", first: true, predicate: ["filterInput"], descendants: true, isSignal: true }, { propertyName: "viewportRef", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], ngImport: i0, template: `
280
421
  <div
281
422
  #trigger
282
423
  class="cb-select__trigger"
@@ -292,7 +433,7 @@ class CbSelect {
292
433
  [attr.aria-haspopup]="'listbox'"
293
434
  [attr.aria-expanded]="open()"
294
435
  [attr.aria-controls]="listboxId"
295
- [attr.aria-activedescendant]="activeDescendant()"
436
+ [attr.aria-activedescendant]="filter() ? null : activeDescendant()"
296
437
  [attr.aria-disabled]="isDisabled() ? 'true' : null"
297
438
  [attr.aria-invalid]="fieldState.invalid() ? 'true' : null"
298
439
  [attr.aria-required]="fieldState.required() || null"
@@ -300,14 +441,16 @@ class CbSelect {
300
441
  (keydown)="onTriggerKeydown($event)"
301
442
  (blur)="onBlur()"
302
443
  >
303
- <span
304
- class="cb-select__label"
305
- [class.cb-select__label--placeholder]="!selectedOption()"
306
- >
444
+ <span class="cb-select__label" [class.cb-select__label--placeholder]="!selectedOption()">
307
445
  {{ displayLabel() }}
308
446
  </span>
309
447
  <cb-icon class="cb-select__chevron" name="chevron-down" [size]="16" aria-hidden="true" />
310
448
  </div>
449
+ @if (canClear()) {
450
+ <button type="button" class="cb-select__clear" [attr.aria-label]="t().clear" (click)="clear()">
451
+ <cb-icon name="x" [size]="14" aria-hidden="true" />
452
+ </button>
453
+ }
311
454
 
312
455
  <ng-template
313
456
  cdkConnectedOverlay
@@ -323,47 +466,113 @@ class CbSelect {
323
466
  <div class="cb-select__panel" [attr.data-theme]="null">
324
467
  @if (filter()) {
325
468
  <div class="cb-select__filter">
469
+ <!-- Con filtro, el foco vive aquí: es el combobox que anuncia la opción activa. -->
326
470
  <input
327
471
  #filterInput
328
472
  type="text"
329
473
  class="cb-select__filter-input"
330
- [placeholder]="filterPlaceholder()"
474
+ role="combobox"
475
+ aria-autocomplete="list"
476
+ aria-expanded="true"
477
+ [attr.aria-controls]="listboxId"
478
+ [attr.aria-activedescendant]="activeDescendant()"
479
+ [placeholder]="(filterPlaceholder() ?? t().search)"
331
480
  [value]="filterText()"
332
481
  (input)="onFilter($event)"
333
482
  (keydown)="onTriggerKeydown($event)"
334
- aria-label="Filtrar opciones"
483
+ [attr.aria-label]="t().filterOptions"
335
484
  />
336
485
  </div>
337
486
  }
338
- <ul class="cb-select__list" role="listbox" [id]="listboxId">
339
- @for (opt of filteredOptions(); track trackOption(opt); let i = $index) {
340
- <li
341
- class="cb-select__option"
342
- [class.cb-is-active]="i === activeIndex()"
343
- [class.cb-is-selected]="isSelected(opt)"
344
- [class.cb-is-disabled]="isOptionDisabled(opt)"
345
- role="option"
346
- [id]="optionId(i)"
347
- [attr.aria-selected]="isSelected(opt)"
348
- [attr.aria-disabled]="isOptionDisabled(opt) ? 'true' : null"
349
- (click)="selectOption(opt)"
350
- (mouseenter)="activeIndex.set(i)"
351
- >
352
- {{ labelOf(opt) }}
353
- </li>
354
- } @empty {
355
- <li class="cb-select__empty" role="option" aria-disabled="true">
356
- {{ emptyMessage() }}
487
+ @if (virtualScroll()) {
488
+ <ul
489
+ #viewport
490
+ class="cb-select__list cb-select__list--virtual"
491
+ role="listbox"
492
+ [id]="listboxId"
493
+ [style.height.px]="viewportHeight()"
494
+ (scroll)="scrollTop.set($any($event.target).scrollTop)"
495
+ >
496
+ <li role="presentation" class="cb-select__spacer" [style.height.px]="virtualRows().length * virtualScrollItemSize()">
497
+ <ul role="presentation" class="cb-select__window" [style.transform]="'translateY(' + windowOffset() + 'px)'">
498
+ @for (row of visibleRows(); track row.kind === 'group' ? row.key : row.index) {
499
+ @if (row.kind === 'group') {
500
+ <li role="presentation" class="cb-select__group-label" [style.height.px]="virtualScrollItemSize()">
501
+ {{ row.label }}
502
+ </li>
503
+ } @else {
504
+ <li
505
+ class="cb-select__option"
506
+ [class.cb-is-active]="row.index === activeIndex()"
507
+ [class.cb-is-selected]="isSelected(row.opt)"
508
+ [class.cb-is-disabled]="isOptionDisabled(row.opt)"
509
+ role="option"
510
+ [id]="optionId(row.index)"
511
+ [style.height.px]="virtualScrollItemSize()"
512
+ [attr.aria-selected]="isSelected(row.opt)"
513
+ [attr.aria-disabled]="isOptionDisabled(row.opt) ? 'true' : null"
514
+ [attr.aria-setsize]="filteredOptions().length"
515
+ [attr.aria-posinset]="row.index + 1"
516
+ (click)="selectOption(row.opt)"
517
+ (mouseenter)="activeIndex.set(row.index)"
518
+ >
519
+ {{ labelOf(row.opt) }}
520
+ </li>
521
+ }
522
+ }
523
+ </ul>
357
524
  </li>
358
- }
359
- </ul>
525
+ @if (!filteredOptions().length) {
526
+ <li class="cb-select__empty" role="option" aria-disabled="true">{{ (emptyMessage() ?? t().noResults) }}</li>
527
+ }
528
+ </ul>
529
+ } @else {
530
+ <ul #viewport class="cb-select__list" role="listbox" [id]="listboxId" [style.max-height]="scrollHeight()">
531
+ @for (g of filteredGroups(); track $index; let gi = $index) {
532
+ @if (g.label !== null) {
533
+ <li role="presentation">
534
+ <ul role="group" class="cb-select__group" [attr.aria-labelledby]="groupId(gi)">
535
+ <li role="presentation" class="cb-select__group-label" [id]="groupId(gi)">{{ g.label }}</li>
536
+ @for (item of g.options; track item.index) {
537
+ <ng-container *ngTemplateOutlet="optionTpl; context: { $implicit: item }" />
538
+ }
539
+ </ul>
540
+ </li>
541
+ } @else {
542
+ @for (item of g.options; track item.index) {
543
+ <ng-container *ngTemplateOutlet="optionTpl; context: { $implicit: item }" />
544
+ }
545
+ }
546
+ }
547
+ @if (!filteredOptions().length) {
548
+ <li class="cb-select__empty" role="option" aria-disabled="true">{{ (emptyMessage() ?? t().noResults) }}</li>
549
+ }
550
+ </ul>
551
+ }
360
552
  </div>
361
553
  </ng-template>
362
- `, isInline: true, styles: [":host{display:block}.cb-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--cb-space-2);box-sizing:border-box;width:100%;font-family:var(--cb-font-sans);color:var(--cb-color-text);background-color:var(--cb-color-bg);border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);cursor:pointer;-webkit-user-select:none;user-select:none;transition:border-color var(--cb-duration-fast) var(--cb-ease),box-shadow var(--cb-duration-fast) var(--cb-ease)}.cb-select__trigger:hover:not(.cb-is-disabled){border-color:var(--cb-color-text-muted)}.cb-select__trigger:focus-visible,.cb-select__trigger.cb-is-open{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 3px color-mix(in srgb,var(--cb-color-primary-500) 25%,transparent)}.cb-select__trigger.cb-is-invalid{border-color:var(--cb-color-danger)}.cb-select__trigger.cb-is-invalid.cb-is-open,.cb-select__trigger.cb-is-invalid:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--cb-color-danger) 25%,transparent)}.cb-select__trigger.cb-is-disabled{cursor:not-allowed;background-color:var(--cb-color-surface);color:var(--cb-color-text-muted);opacity:.8}.cb-select__trigger--sm{min-height:2rem;padding:var(--cb-space-1) var(--cb-space-3);font-size:var(--cb-text-sm)}.cb-select__trigger--md{min-height:2.5rem;padding:var(--cb-space-2) var(--cb-space-3);font-size:var(--cb-text-base)}.cb-select__trigger--lg{min-height:3rem;padding:var(--cb-space-3) var(--cb-space-4);font-size:var(--cb-text-lg)}.cb-select__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cb-select__label--placeholder{color:var(--cb-color-text-muted)}.cb-select__chevron{flex:none;color:var(--cb-color-text-muted);transition:transform var(--cb-duration-fast) var(--cb-ease)}.cb-select__trigger.cb-is-open .cb-select__chevron{transform:rotate(180deg)}.cb-select__panel{box-sizing:border-box;width:100%;background-color:var(--cb-color-surface-raised);border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);box-shadow:var(--cb-shadow-md);overflow:hidden;font-family:var(--cb-font-sans)}.cb-select__filter{padding:var(--cb-space-2);border-bottom:1px solid var(--cb-color-border)}.cb-select__filter-input{box-sizing:border-box;width:100%;padding:var(--cb-space-2) var(--cb-space-3);font-family:inherit;font-size:var(--cb-text-sm);color:var(--cb-color-text);background-color:var(--cb-color-bg);border:1px solid var(--cb-color-border-strong);border-radius:var(--cb-radius-sm)}.cb-select__filter-input:focus-visible{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 2px color-mix(in srgb,var(--cb-color-primary-500) 25%,transparent)}.cb-select__list{list-style:none;margin:0;padding:var(--cb-space-1);max-height:16rem;overflow-y:auto}.cb-select__option{display:flex;align-items:center;padding:var(--cb-space-2) var(--cb-space-3);border-radius:var(--cb-radius-sm);font-size:var(--cb-text-base);color:var(--cb-color-text);cursor:pointer}.cb-select__option.cb-is-active{background-color:var(--cb-color-surface-100)}.cb-select__option.cb-is-selected{color:var(--cb-color-primary-600);background-color:color-mix(in srgb,var(--cb-color-primary-500) 12%,transparent);font-weight:var(--cb-font-weight-semibold)}.cb-select__option.cb-is-selected.cb-is-active{background-color:color-mix(in srgb,var(--cb-color-primary-500) 20%,transparent)}.cb-select__option.cb-is-disabled{color:var(--cb-color-text-muted);cursor:not-allowed;opacity:.6}.cb-select__empty{padding:var(--cb-space-3);color:var(--cb-color-text-muted);font-size:var(--cb-text-sm);text-align:center}\n"], dependencies: [{ kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
554
+
555
+ <ng-template #optionTpl let-item>
556
+ <li
557
+ class="cb-select__option"
558
+ [class.cb-is-active]="item.index === activeIndex()"
559
+ [class.cb-is-selected]="isSelected(item.opt)"
560
+ [class.cb-is-disabled]="isOptionDisabled(item.opt)"
561
+ role="option"
562
+ [id]="optionId(item.index)"
563
+ [attr.aria-selected]="isSelected(item.opt)"
564
+ [attr.aria-disabled]="isOptionDisabled(item.opt) ? 'true' : null"
565
+ (click)="selectOption(item.opt)"
566
+ (mouseenter)="activeIndex.set(item.index)"
567
+ >
568
+ {{ labelOf(item.opt) }}
569
+ </li>
570
+ </ng-template>
571
+ `, isInline: true, styles: [":host{display:block}.cb-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--cb-space-2);box-sizing:border-box;width:100%;font-family:var(--cb-font-sans);color:var(--cb-color-text);background-color:var(--cb-color-bg);border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);cursor:pointer;-webkit-user-select:none;user-select:none;transition:border-color var(--cb-duration-fast) var(--cb-ease),box-shadow var(--cb-duration-fast) var(--cb-ease)}.cb-select__trigger:hover:not(.cb-is-disabled){border-color:var(--cb-color-text-muted)}.cb-select__trigger:focus-visible,.cb-select__trigger.cb-is-open{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 3px color-mix(in srgb,var(--cb-color-primary-500) 25%,transparent)}.cb-select__trigger.cb-is-invalid{border-color:var(--cb-color-danger)}.cb-select__trigger.cb-is-invalid.cb-is-open,.cb-select__trigger.cb-is-invalid:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--cb-color-danger) 25%,transparent)}.cb-select__trigger.cb-is-disabled{cursor:not-allowed;background-color:var(--cb-color-surface);color:var(--cb-color-text-muted);opacity:.8}.cb-select__trigger--sm{min-height:2rem;padding:var(--cb-space-1) var(--cb-space-3);font-size:var(--cb-text-sm)}.cb-select__trigger--md{min-height:2.5rem;padding:var(--cb-space-2) var(--cb-space-3);font-size:var(--cb-text-base)}.cb-select__trigger--lg{min-height:3rem;padding:var(--cb-space-3) var(--cb-space-4);font-size:var(--cb-text-lg)}.cb-select__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cb-select__label--placeholder{color:var(--cb-color-text-muted)}.cb-select__chevron{flex:none;color:var(--cb-color-text-muted);transition:transform var(--cb-duration-fast) var(--cb-ease)}.cb-select__trigger.cb-is-open .cb-select__chevron{transform:rotate(180deg)}.cb-select__panel{box-sizing:border-box;width:100%;background-color:var(--cb-color-surface-raised);border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);box-shadow:var(--cb-shadow-md);overflow:hidden;font-family:var(--cb-font-sans)}.cb-select__filter{padding:var(--cb-space-2);border-bottom:1px solid var(--cb-color-border)}.cb-select__filter-input{box-sizing:border-box;width:100%;padding:var(--cb-space-2) var(--cb-space-3);font-family:inherit;font-size:var(--cb-text-sm);color:var(--cb-color-text);background-color:var(--cb-color-bg);border:1px solid var(--cb-color-border-strong);border-radius:var(--cb-radius-sm)}.cb-select__filter-input:focus-visible{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 2px color-mix(in srgb,var(--cb-color-primary-500) 25%,transparent)}.cb-select__list{list-style:none;margin:0;padding:var(--cb-space-1);max-height:16rem;overflow-y:auto}.cb-select__option{display:flex;align-items:center;padding:var(--cb-space-2) var(--cb-space-3);border-radius:var(--cb-radius-sm);font-size:var(--cb-text-base);color:var(--cb-color-text);cursor:pointer}.cb-select__option.cb-is-active{background-color:var(--cb-color-surface-hover)}.cb-select__option.cb-is-selected{color:var(--cb-color-primary-600);background-color:color-mix(in srgb,var(--cb-color-primary-500) 12%,transparent);font-weight:var(--cb-font-weight-semibold)}.cb-select__option.cb-is-selected.cb-is-active{background-color:color-mix(in srgb,var(--cb-color-primary-500) 20%,transparent)}.cb-select__option.cb-is-disabled{color:var(--cb-color-text-muted);cursor:not-allowed;opacity:.6}.cb-select__empty{padding:var(--cb-space-3);color:var(--cb-color-text-muted);font-size:var(--cb-text-sm);text-align:center}.cb-select__group{list-style:none;margin:0;padding:0}.cb-select__group-label{box-sizing:border-box;display:flex;align-items:center;padding:var(--cb-space-2) var(--cb-space-3) var(--cb-space-1);font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold);letter-spacing:.02em;text-transform:uppercase;color:var(--cb-color-text-muted);cursor:default}.cb-select__list--virtual{max-height:none}.cb-select__spacer{position:relative;list-style:none}.cb-select__window{list-style:none;margin:0;padding:0}.cb-select__list--virtual .cb-select__option{box-sizing:border-box;padding-top:0;padding-bottom:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}:host{position:relative}:host(.cb-has-clear) .cb-select__trigger{padding-right:3.25rem}.cb-select__clear{position:absolute;top:50%;right:2.1rem;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;padding:0;color:var(--cb-color-text-muted);background:none;border:none;border-radius:var(--cb-radius-sm);transform:translateY(-50%);cursor:pointer}.cb-select__clear:hover{color:var(--cb-color-text);background-color:var(--cb-color-surface-hover)}.cb-select__clear:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}\n"], dependencies: [{ kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
363
572
  }
364
573
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbSelect, decorators: [{
365
574
  type: Component,
366
- args: [{ selector: 'cb-select', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [OverlayModule, CbIcon], host: { class: 'cb-select-host' }, providers: [
575
+ args: [{ selector: 'cb-select', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [OverlayModule, NgTemplateOutlet, CbIcon], host: { class: 'cb-select-host', '[class.cb-has-clear]': 'canClear()' }, providers: [
367
576
  {
368
577
  provide: NG_VALUE_ACCESSOR,
369
578
  useExisting: forwardRef(() => CbSelect),
@@ -385,7 +594,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
385
594
  [attr.aria-haspopup]="'listbox'"
386
595
  [attr.aria-expanded]="open()"
387
596
  [attr.aria-controls]="listboxId"
388
- [attr.aria-activedescendant]="activeDescendant()"
597
+ [attr.aria-activedescendant]="filter() ? null : activeDescendant()"
389
598
  [attr.aria-disabled]="isDisabled() ? 'true' : null"
390
599
  [attr.aria-invalid]="fieldState.invalid() ? 'true' : null"
391
600
  [attr.aria-required]="fieldState.required() || null"
@@ -393,14 +602,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
393
602
  (keydown)="onTriggerKeydown($event)"
394
603
  (blur)="onBlur()"
395
604
  >
396
- <span
397
- class="cb-select__label"
398
- [class.cb-select__label--placeholder]="!selectedOption()"
399
- >
605
+ <span class="cb-select__label" [class.cb-select__label--placeholder]="!selectedOption()">
400
606
  {{ displayLabel() }}
401
607
  </span>
402
608
  <cb-icon class="cb-select__chevron" name="chevron-down" [size]="16" aria-hidden="true" />
403
609
  </div>
610
+ @if (canClear()) {
611
+ <button type="button" class="cb-select__clear" [attr.aria-label]="t().clear" (click)="clear()">
612
+ <cb-icon name="x" [size]="14" aria-hidden="true" />
613
+ </button>
614
+ }
404
615
 
405
616
  <ng-template
406
617
  cdkConnectedOverlay
@@ -416,47 +627,128 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
416
627
  <div class="cb-select__panel" [attr.data-theme]="null">
417
628
  @if (filter()) {
418
629
  <div class="cb-select__filter">
630
+ <!-- Con filtro, el foco vive aquí: es el combobox que anuncia la opción activa. -->
419
631
  <input
420
632
  #filterInput
421
633
  type="text"
422
634
  class="cb-select__filter-input"
423
- [placeholder]="filterPlaceholder()"
635
+ role="combobox"
636
+ aria-autocomplete="list"
637
+ aria-expanded="true"
638
+ [attr.aria-controls]="listboxId"
639
+ [attr.aria-activedescendant]="activeDescendant()"
640
+ [placeholder]="(filterPlaceholder() ?? t().search)"
424
641
  [value]="filterText()"
425
642
  (input)="onFilter($event)"
426
643
  (keydown)="onTriggerKeydown($event)"
427
- aria-label="Filtrar opciones"
644
+ [attr.aria-label]="t().filterOptions"
428
645
  />
429
646
  </div>
430
647
  }
431
- <ul class="cb-select__list" role="listbox" [id]="listboxId">
432
- @for (opt of filteredOptions(); track trackOption(opt); let i = $index) {
433
- <li
434
- class="cb-select__option"
435
- [class.cb-is-active]="i === activeIndex()"
436
- [class.cb-is-selected]="isSelected(opt)"
437
- [class.cb-is-disabled]="isOptionDisabled(opt)"
438
- role="option"
439
- [id]="optionId(i)"
440
- [attr.aria-selected]="isSelected(opt)"
441
- [attr.aria-disabled]="isOptionDisabled(opt) ? 'true' : null"
442
- (click)="selectOption(opt)"
443
- (mouseenter)="activeIndex.set(i)"
444
- >
445
- {{ labelOf(opt) }}
648
+ @if (virtualScroll()) {
649
+ <ul
650
+ #viewport
651
+ class="cb-select__list cb-select__list--virtual"
652
+ role="listbox"
653
+ [id]="listboxId"
654
+ [style.height.px]="viewportHeight()"
655
+ (scroll)="scrollTop.set($any($event.target).scrollTop)"
656
+ >
657
+ <li role="presentation" class="cb-select__spacer" [style.height.px]="virtualRows().length * virtualScrollItemSize()">
658
+ <ul role="presentation" class="cb-select__window" [style.transform]="'translateY(' + windowOffset() + 'px)'">
659
+ @for (row of visibleRows(); track row.kind === 'group' ? row.key : row.index) {
660
+ @if (row.kind === 'group') {
661
+ <li role="presentation" class="cb-select__group-label" [style.height.px]="virtualScrollItemSize()">
662
+ {{ row.label }}
663
+ </li>
664
+ } @else {
665
+ <li
666
+ class="cb-select__option"
667
+ [class.cb-is-active]="row.index === activeIndex()"
668
+ [class.cb-is-selected]="isSelected(row.opt)"
669
+ [class.cb-is-disabled]="isOptionDisabled(row.opt)"
670
+ role="option"
671
+ [id]="optionId(row.index)"
672
+ [style.height.px]="virtualScrollItemSize()"
673
+ [attr.aria-selected]="isSelected(row.opt)"
674
+ [attr.aria-disabled]="isOptionDisabled(row.opt) ? 'true' : null"
675
+ [attr.aria-setsize]="filteredOptions().length"
676
+ [attr.aria-posinset]="row.index + 1"
677
+ (click)="selectOption(row.opt)"
678
+ (mouseenter)="activeIndex.set(row.index)"
679
+ >
680
+ {{ labelOf(row.opt) }}
681
+ </li>
682
+ }
683
+ }
684
+ </ul>
446
685
  </li>
447
- } @empty {
448
- <li class="cb-select__empty" role="option" aria-disabled="true">
449
- {{ emptyMessage() }}
450
- </li>
451
- }
452
- </ul>
686
+ @if (!filteredOptions().length) {
687
+ <li class="cb-select__empty" role="option" aria-disabled="true">{{ (emptyMessage() ?? t().noResults) }}</li>
688
+ }
689
+ </ul>
690
+ } @else {
691
+ <ul #viewport class="cb-select__list" role="listbox" [id]="listboxId" [style.max-height]="scrollHeight()">
692
+ @for (g of filteredGroups(); track $index; let gi = $index) {
693
+ @if (g.label !== null) {
694
+ <li role="presentation">
695
+ <ul role="group" class="cb-select__group" [attr.aria-labelledby]="groupId(gi)">
696
+ <li role="presentation" class="cb-select__group-label" [id]="groupId(gi)">{{ g.label }}</li>
697
+ @for (item of g.options; track item.index) {
698
+ <ng-container *ngTemplateOutlet="optionTpl; context: { $implicit: item }" />
699
+ }
700
+ </ul>
701
+ </li>
702
+ } @else {
703
+ @for (item of g.options; track item.index) {
704
+ <ng-container *ngTemplateOutlet="optionTpl; context: { $implicit: item }" />
705
+ }
706
+ }
707
+ }
708
+ @if (!filteredOptions().length) {
709
+ <li class="cb-select__empty" role="option" aria-disabled="true">{{ (emptyMessage() ?? t().noResults) }}</li>
710
+ }
711
+ </ul>
712
+ }
453
713
  </div>
454
714
  </ng-template>
455
- `, styles: [":host{display:block}.cb-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--cb-space-2);box-sizing:border-box;width:100%;font-family:var(--cb-font-sans);color:var(--cb-color-text);background-color:var(--cb-color-bg);border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);cursor:pointer;-webkit-user-select:none;user-select:none;transition:border-color var(--cb-duration-fast) var(--cb-ease),box-shadow var(--cb-duration-fast) var(--cb-ease)}.cb-select__trigger:hover:not(.cb-is-disabled){border-color:var(--cb-color-text-muted)}.cb-select__trigger:focus-visible,.cb-select__trigger.cb-is-open{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 3px color-mix(in srgb,var(--cb-color-primary-500) 25%,transparent)}.cb-select__trigger.cb-is-invalid{border-color:var(--cb-color-danger)}.cb-select__trigger.cb-is-invalid.cb-is-open,.cb-select__trigger.cb-is-invalid:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--cb-color-danger) 25%,transparent)}.cb-select__trigger.cb-is-disabled{cursor:not-allowed;background-color:var(--cb-color-surface);color:var(--cb-color-text-muted);opacity:.8}.cb-select__trigger--sm{min-height:2rem;padding:var(--cb-space-1) var(--cb-space-3);font-size:var(--cb-text-sm)}.cb-select__trigger--md{min-height:2.5rem;padding:var(--cb-space-2) var(--cb-space-3);font-size:var(--cb-text-base)}.cb-select__trigger--lg{min-height:3rem;padding:var(--cb-space-3) var(--cb-space-4);font-size:var(--cb-text-lg)}.cb-select__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cb-select__label--placeholder{color:var(--cb-color-text-muted)}.cb-select__chevron{flex:none;color:var(--cb-color-text-muted);transition:transform var(--cb-duration-fast) var(--cb-ease)}.cb-select__trigger.cb-is-open .cb-select__chevron{transform:rotate(180deg)}.cb-select__panel{box-sizing:border-box;width:100%;background-color:var(--cb-color-surface-raised);border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);box-shadow:var(--cb-shadow-md);overflow:hidden;font-family:var(--cb-font-sans)}.cb-select__filter{padding:var(--cb-space-2);border-bottom:1px solid var(--cb-color-border)}.cb-select__filter-input{box-sizing:border-box;width:100%;padding:var(--cb-space-2) var(--cb-space-3);font-family:inherit;font-size:var(--cb-text-sm);color:var(--cb-color-text);background-color:var(--cb-color-bg);border:1px solid var(--cb-color-border-strong);border-radius:var(--cb-radius-sm)}.cb-select__filter-input:focus-visible{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 2px color-mix(in srgb,var(--cb-color-primary-500) 25%,transparent)}.cb-select__list{list-style:none;margin:0;padding:var(--cb-space-1);max-height:16rem;overflow-y:auto}.cb-select__option{display:flex;align-items:center;padding:var(--cb-space-2) var(--cb-space-3);border-radius:var(--cb-radius-sm);font-size:var(--cb-text-base);color:var(--cb-color-text);cursor:pointer}.cb-select__option.cb-is-active{background-color:var(--cb-color-surface-100)}.cb-select__option.cb-is-selected{color:var(--cb-color-primary-600);background-color:color-mix(in srgb,var(--cb-color-primary-500) 12%,transparent);font-weight:var(--cb-font-weight-semibold)}.cb-select__option.cb-is-selected.cb-is-active{background-color:color-mix(in srgb,var(--cb-color-primary-500) 20%,transparent)}.cb-select__option.cb-is-disabled{color:var(--cb-color-text-muted);cursor:not-allowed;opacity:.6}.cb-select__empty{padding:var(--cb-space-3);color:var(--cb-color-text-muted);font-size:var(--cb-text-sm);text-align:center}\n"] }]
456
- }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], filterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledBy", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], filterInputRef: [{ type: i0.ViewChild, args: ['filterInput', { isSignal: true }] }] } });
715
+
716
+ <ng-template #optionTpl let-item>
717
+ <li
718
+ class="cb-select__option"
719
+ [class.cb-is-active]="item.index === activeIndex()"
720
+ [class.cb-is-selected]="isSelected(item.opt)"
721
+ [class.cb-is-disabled]="isOptionDisabled(item.opt)"
722
+ role="option"
723
+ [id]="optionId(item.index)"
724
+ [attr.aria-selected]="isSelected(item.opt)"
725
+ [attr.aria-disabled]="isOptionDisabled(item.opt) ? 'true' : null"
726
+ (click)="selectOption(item.opt)"
727
+ (mouseenter)="activeIndex.set(item.index)"
728
+ >
729
+ {{ labelOf(item.opt) }}
730
+ </li>
731
+ </ng-template>
732
+ `, styles: [":host{display:block}.cb-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--cb-space-2);box-sizing:border-box;width:100%;font-family:var(--cb-font-sans);color:var(--cb-color-text);background-color:var(--cb-color-bg);border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);cursor:pointer;-webkit-user-select:none;user-select:none;transition:border-color var(--cb-duration-fast) var(--cb-ease),box-shadow var(--cb-duration-fast) var(--cb-ease)}.cb-select__trigger:hover:not(.cb-is-disabled){border-color:var(--cb-color-text-muted)}.cb-select__trigger:focus-visible,.cb-select__trigger.cb-is-open{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 3px color-mix(in srgb,var(--cb-color-primary-500) 25%,transparent)}.cb-select__trigger.cb-is-invalid{border-color:var(--cb-color-danger)}.cb-select__trigger.cb-is-invalid.cb-is-open,.cb-select__trigger.cb-is-invalid:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--cb-color-danger) 25%,transparent)}.cb-select__trigger.cb-is-disabled{cursor:not-allowed;background-color:var(--cb-color-surface);color:var(--cb-color-text-muted);opacity:.8}.cb-select__trigger--sm{min-height:2rem;padding:var(--cb-space-1) var(--cb-space-3);font-size:var(--cb-text-sm)}.cb-select__trigger--md{min-height:2.5rem;padding:var(--cb-space-2) var(--cb-space-3);font-size:var(--cb-text-base)}.cb-select__trigger--lg{min-height:3rem;padding:var(--cb-space-3) var(--cb-space-4);font-size:var(--cb-text-lg)}.cb-select__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cb-select__label--placeholder{color:var(--cb-color-text-muted)}.cb-select__chevron{flex:none;color:var(--cb-color-text-muted);transition:transform var(--cb-duration-fast) var(--cb-ease)}.cb-select__trigger.cb-is-open .cb-select__chevron{transform:rotate(180deg)}.cb-select__panel{box-sizing:border-box;width:100%;background-color:var(--cb-color-surface-raised);border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);box-shadow:var(--cb-shadow-md);overflow:hidden;font-family:var(--cb-font-sans)}.cb-select__filter{padding:var(--cb-space-2);border-bottom:1px solid var(--cb-color-border)}.cb-select__filter-input{box-sizing:border-box;width:100%;padding:var(--cb-space-2) var(--cb-space-3);font-family:inherit;font-size:var(--cb-text-sm);color:var(--cb-color-text);background-color:var(--cb-color-bg);border:1px solid var(--cb-color-border-strong);border-radius:var(--cb-radius-sm)}.cb-select__filter-input:focus-visible{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 2px color-mix(in srgb,var(--cb-color-primary-500) 25%,transparent)}.cb-select__list{list-style:none;margin:0;padding:var(--cb-space-1);max-height:16rem;overflow-y:auto}.cb-select__option{display:flex;align-items:center;padding:var(--cb-space-2) var(--cb-space-3);border-radius:var(--cb-radius-sm);font-size:var(--cb-text-base);color:var(--cb-color-text);cursor:pointer}.cb-select__option.cb-is-active{background-color:var(--cb-color-surface-hover)}.cb-select__option.cb-is-selected{color:var(--cb-color-primary-600);background-color:color-mix(in srgb,var(--cb-color-primary-500) 12%,transparent);font-weight:var(--cb-font-weight-semibold)}.cb-select__option.cb-is-selected.cb-is-active{background-color:color-mix(in srgb,var(--cb-color-primary-500) 20%,transparent)}.cb-select__option.cb-is-disabled{color:var(--cb-color-text-muted);cursor:not-allowed;opacity:.6}.cb-select__empty{padding:var(--cb-space-3);color:var(--cb-color-text-muted);font-size:var(--cb-text-sm);text-align:center}.cb-select__group{list-style:none;margin:0;padding:0}.cb-select__group-label{box-sizing:border-box;display:flex;align-items:center;padding:var(--cb-space-2) var(--cb-space-3) var(--cb-space-1);font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold);letter-spacing:.02em;text-transform:uppercase;color:var(--cb-color-text-muted);cursor:default}.cb-select__list--virtual{max-height:none}.cb-select__spacer{position:relative;list-style:none}.cb-select__window{list-style:none;margin:0;padding:0}.cb-select__list--virtual .cb-select__option{box-sizing:border-box;padding-top:0;padding-bottom:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}:host{position:relative}:host(.cb-has-clear) .cb-select__trigger{padding-right:3.25rem}.cb-select__clear{position:absolute;top:50%;right:2.1rem;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;padding:0;color:var(--cb-color-text-muted);background:none;border:none;border-radius:var(--cb-radius-sm);transform:translateY(-50%);cursor:pointer}.cb-select__clear:hover{color:var(--cb-color-text);background-color:var(--cb-color-surface-hover)}.cb-select__clear:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}\n"] }]
733
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: false }] }], optionGroupLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroupLabel", required: false }] }], optionGroupChildren: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroupChildren", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], filterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], showClear: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClear", required: false }] }], scrollHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollHeight", required: false }] }], virtualScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScroll", required: false }] }], virtualScrollItemSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScrollItemSize", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledBy", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], filterInputRef: [{ type: i0.ViewChild, args: ['filterInput', { isSignal: true }] }], viewportRef: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }] } });
457
734
  function toBoolean(value) {
458
735
  return value !== false && value != null && `${value}` !== 'false';
459
736
  }
737
+ /** Minúsculas y sin tildes, para que "medellin" encuentre "Medellín". */
738
+ function normalize(text) {
739
+ return text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLocaleLowerCase().trim();
740
+ }
741
+ /** Tecla imprimible sin modificadores (para la búsqueda al teclear). */
742
+ function isPrintable(event) {
743
+ return event.key.length === 1 && event.key !== ' ' && !event.ctrlKey && !event.metaKey && !event.altKey;
744
+ }
745
+ /** Convierte un alto CSS (`px` o `rem`) a píxeles para el modo virtual. */
746
+ function cssToPx(value) {
747
+ const n = parseFloat(value);
748
+ if (Number.isNaN(n))
749
+ return 256;
750
+ return value.trim().endsWith('rem') ? n * 16 : n;
751
+ }
460
752
 
461
753
  /*
462
754
  * Public API Surface de @caribeng/ui/select