@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,37 @@
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
7
  import { CbIcon } from '@caribeng/ui/icon';
7
- import { cbFieldState } from '@caribeng/ui';
8
+ import { injectCbTranslation, cbFormat, cbFieldState } from '@caribeng/ui';
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
  * Selección múltiple (multiselect) de CaribeNG. Listbox accesible sobre CDK
12
- * Overlay con casillas, filtro y seleccionar-todo. Integrado con Angular Forms
13
- * vía `ControlValueAccessor`; el modelo es un array de valores.
15
+ * Overlay con casillas, filtro, seleccionar-todo, grupos, búsqueda al teclear
16
+ * y scroll virtual. Integrado con Angular Forms vía `ControlValueAccessor`;
17
+ * el modelo es un array de valores.
14
18
  *
15
19
  * @example
16
20
  * ```html
17
21
  * <cb-multiselect [options]="ciudades" optionLabel="nombre" optionValue="id"
18
22
  * formControlName="ciudades" placeholder="Elige ciudades" />
23
+ *
24
+ * <!-- Agrupado: [{ label: 'Colombia', items: [...] }, …] -->
25
+ * <cb-multiselect [options]="paises" group filter />
19
26
  * ```
20
27
  */
21
28
  class CbMultiSelect {
22
29
  constructor() {
30
+ /** Textos de interfaz en el idioma activo (`provideCaribeNG({ locale })`). */
31
+ this.t = injectCbTranslation();
32
+ /** Rellena `{0}`, `{1}`… de un texto traducido. */
33
+ this.fmt = cbFormat;
34
+ this.injector = inject(Injector);
23
35
  this.options = input([], /* @ts-ignore */
24
36
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
25
37
  this.optionLabel = input('label', /* @ts-ignore */
@@ -28,19 +40,35 @@ class CbMultiSelect {
28
40
  ...(ngDevMode ? [{ debugName: "optionValue" }] : /* istanbul ignore next */ []));
29
41
  this.optionDisabled = input('disabled', /* @ts-ignore */
30
42
  ...(ngDevMode ? [{ debugName: "optionDisabled" }] : /* istanbul ignore next */ []));
31
- this.placeholder = input('Seleccionar', /* @ts-ignore */
43
+ /** Las opciones son grupos (`{ label, items }`): se muestran con su título. */
44
+ this.group = input(false, { ...(ngDevMode ? { debugName: "group" } : /* istanbul ignore next */ {}), transform: toBoolean });
45
+ /** Propiedad con el título de cada grupo. */
46
+ this.optionGroupLabel = input('label', /* @ts-ignore */
47
+ ...(ngDevMode ? [{ debugName: "optionGroupLabel" }] : /* istanbul ignore next */ []));
48
+ /** Propiedad con las opciones de cada grupo. */
49
+ this.optionGroupChildren = input('items', /* @ts-ignore */
50
+ ...(ngDevMode ? [{ debugName: "optionGroupChildren" }] : /* istanbul ignore next */ []));
51
+ this.placeholder = input(undefined, /* @ts-ignore */
32
52
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
33
53
  this.size = input('md', /* @ts-ignore */
34
54
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
35
55
  this.filter = input(false, { ...(ngDevMode ? { debugName: "filter" } : /* istanbul ignore next */ {}), transform: toBoolean });
36
- this.filterPlaceholder = input('Buscar…', /* @ts-ignore */
56
+ this.filterPlaceholder = input(undefined, /* @ts-ignore */
37
57
  ...(ngDevMode ? [{ debugName: "filterPlaceholder" }] : /* istanbul ignore next */ []));
38
- this.emptyMessage = input('Sin resultados', /* @ts-ignore */
58
+ this.emptyMessage = input(undefined, /* @ts-ignore */
39
59
  ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
40
60
  this.showToggleAll = input(true, { ...(ngDevMode ? { debugName: "showToggleAll" } : /* istanbul ignore next */ {}), transform: toBoolean });
41
61
  /** Nº máx. de etiquetas antes de mostrar "N seleccionados". */
42
62
  this.maxSelectedLabels = input(3, /* @ts-ignore */
43
63
  ...(ngDevMode ? [{ debugName: "maxSelectedLabels" }] : /* istanbul ignore next */ []));
64
+ /** Alto máximo de la lista (CSS). En modo virtual es su alto fijo. */
65
+ this.scrollHeight = input('15rem', /* @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 */ []));
44
72
  this.invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: toBoolean });
45
73
  /** Estado de validación: inputs manuales + estado de Angular Forms. */
46
74
  this.fieldState = cbFieldState({ invalid: () => this.invalid() });
@@ -63,6 +91,8 @@ class CbMultiSelect {
63
91
  ...(ngDevMode ? [{ debugName: "filterText" }] : /* istanbul ignore next */ []));
64
92
  this.triggerWidth = signal(0, /* @ts-ignore */
65
93
  ...(ngDevMode ? [{ debugName: "triggerWidth" }] : /* istanbul ignore next */ []));
94
+ this.scrollTop = signal(0, /* @ts-ignore */
95
+ ...(ngDevMode ? [{ debugName: "scrollTop" }] : /* istanbul ignore next */ []));
66
96
  this.cvaDisabled = signal(false, /* @ts-ignore */
67
97
  ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
68
98
  this.isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
@@ -71,34 +101,67 @@ class CbMultiSelect {
71
101
  this.triggerRef = viewChild.required('trigger');
72
102
  this.filterInputRef = viewChild('filterInput', /* @ts-ignore */
73
103
  ...(ngDevMode ? [{ debugName: "filterInputRef" }] : /* istanbul ignore next */ []));
104
+ this.viewportRef = viewChild('viewport', /* @ts-ignore */
105
+ ...(ngDevMode ? [{ debugName: "viewportRef" }] : /* istanbul ignore next */ []));
74
106
  this.positions = [
75
107
  { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
76
108
  { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
77
109
  ];
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));
110
+ // --- Búsqueda al teclear ------------------------------------------------------
111
+ this.typeahead = '';
112
+ /** Todas las opciones (aplanando los grupos). */
113
+ this.flatOptions = computed(() => this.group() ? this.options().flatMap((g) => this.childrenOf(g)) : this.options(), /* @ts-ignore */
114
+ ...(ngDevMode ? [{ debugName: "flatOptions" }] : /* istanbul ignore next */ []));
115
+ this.filteredGroups = computed(() => {
116
+ const q = normalize(this.filterText());
117
+ const keep = (o) => !this.filter() || !q || normalize(this.labelOf(o)).includes(q);
118
+ let index = 0;
119
+ const indexed = (opts) => opts.filter(keep).map((opt) => ({ opt, index: index++ }));
120
+ if (!this.group())
121
+ return [{ label: null, options: indexed(this.options()) }];
122
+ return this.options()
123
+ .map((g) => ({ label: this.groupLabelOf(g), options: indexed(this.childrenOf(g)) }))
124
+ .filter((g) => g.options.length > 0);
84
125
  }, /* @ts-ignore */
126
+ ...(ngDevMode ? [{ debugName: "filteredGroups" }] : /* istanbul ignore next */ []));
127
+ this.filteredOptions = computed(() => this.filteredGroups().flatMap((g) => g.options.map((o) => o.opt)), /* @ts-ignore */
85
128
  ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
86
- this.selected = computed(() => this.options().filter((o) => this.value().includes(this.valueOf(o))), /* @ts-ignore */
129
+ /** Filas del modo virtual (cabeceras de grupo + opciones). */
130
+ this.virtualRows = computed(() => this.filteredGroups().flatMap((g, gi) => [
131
+ ...(g.label !== null ? [{ kind: 'group', label: g.label, key: `g${gi}` }] : []),
132
+ ...g.options.map((o) => ({ kind: 'option', ...o })),
133
+ ]), /* @ts-ignore */
134
+ ...(ngDevMode ? [{ debugName: "virtualRows" }] : /* istanbul ignore next */ []));
135
+ this.viewportHeight = computed(() => cssToPx(this.scrollHeight()), /* @ts-ignore */
136
+ ...(ngDevMode ? [{ debugName: "viewportHeight" }] : /* istanbul ignore next */ []));
137
+ this.windowStart = computed(() => Math.max(0, Math.floor(this.scrollTop() / this.virtualScrollItemSize()) - VIRTUAL_BUFFER), /* @ts-ignore */
138
+ ...(ngDevMode ? [{ debugName: "windowStart" }] : /* istanbul ignore next */ []));
139
+ this.windowOffset = computed(() => this.windowStart() * this.virtualScrollItemSize(), /* @ts-ignore */
140
+ ...(ngDevMode ? [{ debugName: "windowOffset" }] : /* istanbul ignore next */ []));
141
+ this.visibleRows = computed(() => {
142
+ const size = this.virtualScrollItemSize();
143
+ const end = Math.ceil((this.scrollTop() + this.viewportHeight()) / size) + VIRTUAL_BUFFER;
144
+ return this.virtualRows().slice(this.windowStart(), end);
145
+ }, /* @ts-ignore */
146
+ ...(ngDevMode ? [{ debugName: "visibleRows" }] : /* istanbul ignore next */ []));
147
+ this.selected = computed(() => this.flatOptions().filter((o) => this.value().includes(this.valueOf(o))), /* @ts-ignore */
87
148
  ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
88
149
  this.displayLabel = computed(() => {
89
150
  const sel = this.selected();
90
151
  if (!sel.length)
91
- return this.placeholder();
152
+ return (this.placeholder() ?? this.t().select);
92
153
  if (sel.length > this.maxSelectedLabels())
93
- return `${sel.length} seleccionados`;
154
+ return cbFormat(this.t().selectedCount, sel.length);
94
155
  return sel.map((o) => this.labelOf(o)).join(', ');
95
156
  }, /* @ts-ignore */
96
157
  ...(ngDevMode ? [{ debugName: "displayLabel" }] : /* istanbul ignore next */ []));
97
158
  this.allSelected = computed(() => {
98
- const enabled = this.options().filter((o) => !this.isOptionDisabled(o));
159
+ const enabled = this.flatOptions().filter((o) => !this.isOptionDisabled(o));
99
160
  return enabled.length > 0 && enabled.every((o) => this.value().includes(this.valueOf(o)));
100
161
  }, /* @ts-ignore */
101
162
  ...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
163
+ this.activeDescendant = computed(() => this.open() && this.activeIndex() >= 0 ? this.optionId(this.activeIndex()) : null, /* @ts-ignore */
164
+ ...(ngDevMode ? [{ debugName: "activeDescendant" }] : /* istanbul ignore next */ []));
102
165
  this.onChange = () => { };
103
166
  this.onTouched = () => { };
104
167
  effect(() => {
@@ -106,6 +169,13 @@ class CbMultiSelect {
106
169
  queueMicrotask(() => this.filterInputRef()?.nativeElement.focus());
107
170
  }
108
171
  });
172
+ // Mantiene el índice activo dentro de la lista filtrada.
173
+ effect(() => {
174
+ const length = this.filteredOptions().length;
175
+ if (this.open() && this.activeIndex() >= length)
176
+ this.setActive(this.firstEnabledIndex());
177
+ });
178
+ inject(DestroyRef).onDestroy(() => clearTimeout(this.typeaheadTimer));
109
179
  }
110
180
  // --- ControlValueAccessor --------------------------------------------------
111
181
  writeValue(value) {
@@ -130,8 +200,9 @@ class CbMultiSelect {
130
200
  if (this.isDisabled())
131
201
  return;
132
202
  this.triggerWidth.set(this.triggerRef().nativeElement.offsetWidth);
203
+ this.scrollTop.set(0);
133
204
  this.open.set(true);
134
- this.activeIndex.set(this.firstEnabledIndex());
205
+ this.setActive(this.firstEnabledIndex());
135
206
  }
136
207
  close() {
137
208
  if (!this.open())
@@ -150,7 +221,7 @@ class CbMultiSelect {
150
221
  this.commit(next);
151
222
  }
152
223
  toggleAll() {
153
- const enabled = this.options().filter((o) => !this.isOptionDisabled(o));
224
+ const enabled = this.flatOptions().filter((o) => !this.isOptionDisabled(o));
154
225
  this.commit(this.allSelected() ? [] : enabled.map((o) => this.valueOf(o)));
155
226
  }
156
227
  onBlur() {
@@ -168,6 +239,20 @@ class CbMultiSelect {
168
239
  }
169
240
  onTriggerKeydown(event) {
170
241
  const key = event.key;
242
+ // En el filtro, Espacio, Inicio y Fin escriben o mueven el cursor del texto.
243
+ const inFilter = event.target === this.filterInputRef()?.nativeElement;
244
+ if (!inFilter && isPrintable(event)) {
245
+ event.preventDefault();
246
+ if (!this.open()) {
247
+ this.openPanel();
248
+ this.activeIndex.set(-1);
249
+ }
250
+ if (this.filter())
251
+ this.filterText.set(this.filterText() + key);
252
+ else
253
+ this.searchByTyping(key);
254
+ return;
255
+ }
171
256
  if (!this.open()) {
172
257
  if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(key)) {
173
258
  event.preventDefault();
@@ -185,22 +270,25 @@ class CbMultiSelect {
185
270
  this.move(-1);
186
271
  break;
187
272
  case 'Home':
273
+ if (inFilter)
274
+ break;
188
275
  event.preventDefault();
189
- this.activeIndex.set(this.firstEnabledIndex());
276
+ this.setActive(this.firstEnabledIndex());
190
277
  break;
191
278
  case 'End':
279
+ if (inFilter)
280
+ break;
192
281
  event.preventDefault();
193
- this.activeIndex.set(this.filteredOptions().length - 1);
282
+ this.setActive(this.lastEnabledIndex());
283
+ break;
284
+ case ' ':
285
+ if (inFilter)
286
+ break;
287
+ this.toggleActive(event);
194
288
  break;
195
289
  case 'Enter':
196
- case ' ': {
197
- const opt = this.filteredOptions()[this.activeIndex()];
198
- if (opt !== undefined) {
199
- event.preventDefault();
200
- this.toggleOption(opt);
201
- }
290
+ this.toggleActive(event);
202
291
  break;
203
- }
204
292
  case 'Escape':
205
293
  event.preventDefault();
206
294
  this.close();
@@ -235,12 +323,57 @@ class CbMultiSelect {
235
323
  optionId(index) {
236
324
  return `${this.inputId()}-opt-${index}`;
237
325
  }
238
- trackOption(opt) {
239
- return this.valueOf(opt);
326
+ groupId(index) {
327
+ return `${this.inputId()}-group-${index}`;
328
+ }
329
+ groupLabelOf(group) {
330
+ return String(group?.[this.optionGroupLabel()] ?? '');
331
+ }
332
+ childrenOf(group) {
333
+ const items = group?.[this.optionGroupChildren()];
334
+ return Array.isArray(items) ? items : [];
335
+ }
336
+ toggleActive(event) {
337
+ const opt = this.filteredOptions()[this.activeIndex()];
338
+ if (opt === undefined)
339
+ return;
340
+ event.preventDefault();
341
+ this.toggleOption(opt);
342
+ }
343
+ /** Cambia la opción activa y la desplaza a la vista. */
344
+ setActive(index) {
345
+ this.activeIndex.set(index);
346
+ if (index < 0 || !this.open())
347
+ return;
348
+ if (this.virtualScroll()) {
349
+ const size = this.virtualScrollItemSize();
350
+ const row = this.virtualRows().findIndex((r) => r.kind === 'option' && r.index === index);
351
+ const top = row * size;
352
+ const view = this.viewportHeight();
353
+ const current = this.scrollTop();
354
+ const next = top < current ? top : top + size > current + view ? top + size - view : current;
355
+ if (next !== current) {
356
+ this.scrollTop.set(next);
357
+ const viewport = this.viewportRef()?.nativeElement;
358
+ if (viewport)
359
+ viewport.scrollTop = next;
360
+ }
361
+ }
362
+ else {
363
+ afterNextRender(() => this.viewportRef()?.nativeElement.querySelector(`#${this.optionId(index)}`)?.scrollIntoView?.({ block: 'nearest' }), { injector: this.injector });
364
+ }
240
365
  }
241
366
  firstEnabledIndex() {
242
367
  return this.filteredOptions().findIndex((o) => !this.isOptionDisabled(o));
243
368
  }
369
+ lastEnabledIndex() {
370
+ const opts = this.filteredOptions();
371
+ for (let i = opts.length - 1; i >= 0; i--) {
372
+ if (!this.isOptionDisabled(opts[i]))
373
+ return i;
374
+ }
375
+ return -1;
376
+ }
244
377
  move(delta) {
245
378
  const opts = this.filteredOptions();
246
379
  if (!opts.length)
@@ -249,7 +382,25 @@ class CbMultiSelect {
249
382
  for (let s = 0; s < opts.length; s++) {
250
383
  i = (i + delta + opts.length) % opts.length;
251
384
  if (!this.isOptionDisabled(opts[i])) {
252
- this.activeIndex.set(i);
385
+ this.setActive(i);
386
+ return;
387
+ }
388
+ }
389
+ }
390
+ /** Teclear salta a la siguiente opción que empieza por lo escrito (se reinicia tras 500 ms). */
391
+ searchByTyping(char) {
392
+ clearTimeout(this.typeaheadTimer);
393
+ this.typeahead += normalize(char);
394
+ this.typeaheadTimer = setTimeout(() => (this.typeahead = ''), 500);
395
+ const opts = this.filteredOptions();
396
+ // Repetir la misma letra recorre las opciones que empiezan por ella ("m", "m"…).
397
+ const query = [...this.typeahead].every((c) => c === this.typeahead[0]) ? this.typeahead[0] : this.typeahead;
398
+ // Con una sola letra se avanza a la siguiente coincidencia; con más, se refina desde la actual.
399
+ const from = query.length === 1 ? this.activeIndex() + 1 : Math.max(0, this.activeIndex());
400
+ for (let step = 0; step < opts.length; step++) {
401
+ const i = (from + step) % opts.length;
402
+ if (!this.isOptionDisabled(opts[i]) && normalize(this.labelOf(opts[i])).startsWith(query)) {
403
+ this.setActive(i);
253
404
  return;
254
405
  }
255
406
  }
@@ -259,13 +410,13 @@ class CbMultiSelect {
259
410
  this.onChange(next);
260
411
  }
261
412
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbMultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
262
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbMultiSelect, isStandalone: true, selector: "cb-multiselect", 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 }, showToggleAll: { classPropertyName: "showToggleAll", publicName: "showToggleAll", isSignal: true, isRequired: false, transformFunction: null }, maxSelectedLabels: { classPropertyName: "maxSelectedLabels", publicName: "maxSelectedLabels", 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-multiselect-host" }, providers: [
413
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbMultiSelect, isStandalone: true, selector: "cb-multiselect", 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 }, showToggleAll: { classPropertyName: "showToggleAll", publicName: "showToggleAll", isSignal: true, isRequired: false, transformFunction: null }, maxSelectedLabels: { classPropertyName: "maxSelectedLabels", publicName: "maxSelectedLabels", 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: { classAttribute: "cb-multiselect-host" }, providers: [
263
414
  {
264
415
  provide: NG_VALUE_ACCESSOR,
265
416
  useExisting: forwardRef(() => CbMultiSelect),
266
417
  multi: true,
267
418
  },
268
- ], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "filterInputRef", first: true, predicate: ["filterInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
419
+ ], 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: `
269
420
  <div
270
421
  #trigger
271
422
  class="cb-multiselect__trigger"
@@ -281,6 +432,7 @@ class CbMultiSelect {
281
432
  aria-haspopup="listbox"
282
433
  [attr.aria-expanded]="open()"
283
434
  [attr.aria-controls]="listboxId"
435
+ [attr.aria-activedescendant]="filter() ? null : activeDescendant()"
284
436
  [attr.aria-disabled]="isDisabled() ? 'true' : null"
285
437
  [attr.aria-invalid]="fieldState.invalid() ? 'true' : null"
286
438
  [attr.aria-required]="fieldState.required() || null"
@@ -309,59 +461,126 @@ class CbMultiSelect {
309
461
  @if (filter() || showToggleAll()) {
310
462
  <div class="cb-multiselect__header">
311
463
  @if (showToggleAll()) {
312
- <button type="button" class="cb-multiselect__all" (click)="toggleAll()">
313
- <span class="cb-multiselect__check" [class.cb-is-checked]="allSelected()">
314
- @if (allSelected()) { <cb-icon name="check" [size]="14" aria-hidden="true" /> }
464
+ <button type="button" class="cb-multiselect__all" [attr.aria-pressed]="allSelected()" (click)="toggleAll()">
465
+ <span class="cb-multiselect__check" [class.cb-is-checked]="allSelected()" aria-hidden="true">
466
+ @if (allSelected()) {
467
+ <cb-icon name="check" [size]="14" />
468
+ }
315
469
  </span>
316
- Todos
470
+ {{ t().selectAll }}
317
471
  </button>
318
472
  }
319
473
  @if (filter()) {
474
+ <!-- Con filtro, el foco vive aquí: es el combobox que anuncia la opción activa. -->
320
475
  <input
321
476
  #filterInput
322
477
  type="text"
323
478
  class="cb-multiselect__filter-input"
324
- [placeholder]="filterPlaceholder()"
479
+ role="combobox"
480
+ aria-autocomplete="list"
481
+ aria-expanded="true"
482
+ [attr.aria-controls]="listboxId"
483
+ [attr.aria-activedescendant]="activeDescendant()"
484
+ [placeholder]="(filterPlaceholder() ?? t().search)"
325
485
  [value]="filterText()"
326
486
  (input)="onFilter($event)"
327
487
  (keydown)="onTriggerKeydown($event)"
328
- aria-label="Filtrar opciones"
488
+ [attr.aria-label]="t().filterOptions"
329
489
  />
330
490
  }
331
491
  </div>
332
492
  }
333
- <ul class="cb-multiselect__list" role="listbox" aria-multiselectable="true" [id]="listboxId">
334
- @for (opt of filteredOptions(); track trackOption(opt); let i = $index) {
335
- <li
336
- class="cb-multiselect__option"
337
- [class.cb-is-active]="i === activeIndex()"
338
- [class.cb-is-selected]="isSelected(opt)"
339
- [class.cb-is-disabled]="isOptionDisabled(opt)"
340
- role="option"
341
- [id]="optionId(i)"
342
- [attr.aria-selected]="isSelected(opt)"
343
- [attr.aria-disabled]="isOptionDisabled(opt) ? 'true' : null"
344
- (click)="toggleOption(opt)"
345
- (mouseenter)="activeIndex.set(i)"
346
- >
347
- <span class="cb-multiselect__check" [class.cb-is-checked]="isSelected(opt)">
348
- @if (isSelected(opt)) { <cb-icon name="check" [size]="14" aria-hidden="true" /> }
349
- </span>
350
- {{ labelOf(opt) }}
351
- </li>
352
- } @empty {
353
- <li class="cb-multiselect__empty" role="option" aria-disabled="true">
354
- {{ emptyMessage() }}
493
+ @if (virtualScroll()) {
494
+ <ul
495
+ #viewport
496
+ class="cb-multiselect__list cb-multiselect__list--virtual"
497
+ role="listbox"
498
+ aria-multiselectable="true"
499
+ [id]="listboxId"
500
+ [style.height.px]="viewportHeight()"
501
+ (scroll)="scrollTop.set($any($event.target).scrollTop)"
502
+ >
503
+ <li role="presentation" class="cb-multiselect__spacer" [style.height.px]="virtualRows().length * virtualScrollItemSize()">
504
+ <ul role="presentation" class="cb-multiselect__window" [style.transform]="'translateY(' + windowOffset() + 'px)'">
505
+ @for (row of visibleRows(); track row.kind === 'group' ? row.key : row.index) {
506
+ @if (row.kind === 'group') {
507
+ <li role="presentation" class="cb-multiselect__group-label" [style.height.px]="virtualScrollItemSize()">
508
+ {{ row.label }}
509
+ </li>
510
+ } @else {
511
+ <ng-container
512
+ *ngTemplateOutlet="optionTpl; context: { $implicit: row, height: virtualScrollItemSize(), virtual: true }"
513
+ />
514
+ }
515
+ }
516
+ </ul>
355
517
  </li>
356
- }
357
- </ul>
518
+ @if (!filteredOptions().length) {
519
+ <li class="cb-multiselect__empty" role="option" aria-disabled="true">{{ (emptyMessage() ?? t().noResults) }}</li>
520
+ }
521
+ </ul>
522
+ } @else {
523
+ <ul
524
+ #viewport
525
+ class="cb-multiselect__list"
526
+ role="listbox"
527
+ aria-multiselectable="true"
528
+ [id]="listboxId"
529
+ [style.max-height]="scrollHeight()"
530
+ >
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-multiselect__group" [attr.aria-labelledby]="groupId(gi)">
535
+ <li role="presentation" class="cb-multiselect__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, height: null, virtual: false }" />
538
+ }
539
+ </ul>
540
+ </li>
541
+ } @else {
542
+ @for (item of g.options; track item.index) {
543
+ <ng-container *ngTemplateOutlet="optionTpl; context: { $implicit: item, height: null, virtual: false }" />
544
+ }
545
+ }
546
+ }
547
+ @if (!filteredOptions().length) {
548
+ <li class="cb-multiselect__empty" role="option" aria-disabled="true">{{ (emptyMessage() ?? t().noResults) }}</li>
549
+ }
550
+ </ul>
551
+ }
358
552
  </div>
359
553
  </ng-template>
360
- `, isInline: true, styles: [":host{display:block}.cb-multiselect__trigger{display:flex;align-items:center;gap:var(--cb-space-2);width:100%;min-height:2.5rem;padding:0 var(--cb-space-3);background-color:var(--cb-color-bg);border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);color:var(--cb-color-text);font-family:var(--cb-font-sans);cursor:pointer;transition:border-color var(--cb-duration-fast) var(--cb-ease),box-shadow var(--cb-duration-fast) var(--cb-ease)}.cb-multiselect__trigger--sm{min-height:2.125rem;font-size:var(--cb-text-sm)}.cb-multiselect__trigger--lg{min-height:2.875rem;font-size:var(--cb-text-lg)}.cb-multiselect__trigger:focus-visible,.cb-multiselect__trigger.cb-is-open{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 3px var(--cb-color-focus-ring)}.cb-multiselect__trigger.cb-is-invalid{border-color:var(--cb-color-danger)}.cb-multiselect__trigger.cb-is-disabled{opacity:var(--cb-opacity-disabled, .55);cursor:default;background-color:var(--cb-color-surface-100)}.cb-multiselect__label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.cb-multiselect__label--placeholder{color:var(--cb-color-text-muted)}.cb-multiselect__chevron{flex:none;color:var(--cb-color-text-muted);transition:transform var(--cb-duration-fast) var(--cb-ease)}.cb-multiselect__trigger.cb-is-open .cb-multiselect__chevron{transform:rotate(180deg)}.cb-multiselect__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-multiselect__header{display:flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-2);border-bottom:1px solid var(--cb-color-border)}.cb-multiselect__all{display:inline-flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-1) var(--cb-space-2);border:none;background:transparent;color:var(--cb-color-text);border-radius:var(--cb-radius-sm);cursor:pointer;font:inherit;white-space:nowrap}.cb-multiselect__all:hover{background-color:var(--cb-color-surface-100)}.cb-multiselect__filter-input{flex:1 1 auto;min-width:0;padding:var(--cb-space-1) var(--cb-space-2);border:var(--cb-border-width) solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background-color:var(--cb-color-bg);color:var(--cb-color-text);font:inherit;outline:none}.cb-multiselect__filter-input:focus{border-color:var(--cb-color-primary-500)}.cb-multiselect__list{list-style:none;margin:0;padding:var(--cb-space-1);max-height:15rem;overflow-y:auto}.cb-multiselect__option{display:flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-2) var(--cb-space-3);border-radius:var(--cb-radius-sm);cursor:pointer;color:var(--cb-color-text)}.cb-multiselect__option.cb-is-active{background-color:var(--cb-color-surface-100)}.cb-multiselect__option.cb-is-selected{color:var(--cb-color-primary-700)}.cb-multiselect__option.cb-is-disabled{opacity:var(--cb-opacity-disabled, .55);cursor:default}.cb-multiselect__check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:1.1rem;height:1.1rem;border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-sm);color:var(--cb-color-primary-contrast)}.cb-multiselect__check.cb-is-checked{background-color:var(--cb-color-primary-500);border-color:var(--cb-color-primary-500)}.cb-multiselect__empty{padding:var(--cb-space-3);color:var(--cb-color-text-muted);text-align:center}:host-context([data-theme=\"dark\"]) .cb-multiselect__option.cb-is-selected{color:var(--cb-color-primary-200)}\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 let-height="height" let-virtual="virtual">
556
+ <li
557
+ class="cb-multiselect__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
+ [style.height.px]="height"
564
+ [attr.aria-selected]="isSelected(item.opt)"
565
+ [attr.aria-disabled]="isOptionDisabled(item.opt) ? 'true' : null"
566
+ [attr.aria-setsize]="virtual ? filteredOptions().length : null"
567
+ [attr.aria-posinset]="virtual ? item.index + 1 : null"
568
+ (click)="toggleOption(item.opt)"
569
+ (mouseenter)="activeIndex.set(item.index)"
570
+ >
571
+ <span class="cb-multiselect__check" [class.cb-is-checked]="isSelected(item.opt)" aria-hidden="true">
572
+ @if (isSelected(item.opt)) {
573
+ <cb-icon name="check" [size]="14" />
574
+ }
575
+ </span>
576
+ {{ labelOf(item.opt) }}
577
+ </li>
578
+ </ng-template>
579
+ `, isInline: true, styles: [":host{display:block}.cb-multiselect__trigger{display:flex;align-items:center;gap:var(--cb-space-2);width:100%;min-height:2.5rem;padding:0 var(--cb-space-3);background-color:var(--cb-color-bg);border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);color:var(--cb-color-text);font-family:var(--cb-font-sans);cursor:pointer;transition:border-color var(--cb-duration-fast) var(--cb-ease),box-shadow var(--cb-duration-fast) var(--cb-ease)}.cb-multiselect__trigger--sm{min-height:2.125rem;font-size:var(--cb-text-sm)}.cb-multiselect__trigger--lg{min-height:2.875rem;font-size:var(--cb-text-lg)}.cb-multiselect__trigger:focus-visible,.cb-multiselect__trigger.cb-is-open{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 3px var(--cb-color-focus-ring)}.cb-multiselect__trigger.cb-is-invalid{border-color:var(--cb-color-danger)}.cb-multiselect__trigger.cb-is-disabled{opacity:var(--cb-opacity-disabled, .55);cursor:default;background-color:var(--cb-color-surface)}.cb-multiselect__label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.cb-multiselect__label--placeholder{color:var(--cb-color-text-muted)}.cb-multiselect__chevron{flex:none;color:var(--cb-color-text-muted);transition:transform var(--cb-duration-fast) var(--cb-ease)}.cb-multiselect__trigger.cb-is-open .cb-multiselect__chevron{transform:rotate(180deg)}.cb-multiselect__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-multiselect__header{display:flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-2);border-bottom:1px solid var(--cb-color-border)}.cb-multiselect__all{display:inline-flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-1) var(--cb-space-2);border:none;background:transparent;color:var(--cb-color-text);border-radius:var(--cb-radius-sm);cursor:pointer;font:inherit;white-space:nowrap}.cb-multiselect__all:hover{background-color:var(--cb-color-surface-hover)}.cb-multiselect__filter-input{flex:1 1 auto;min-width:0;padding:var(--cb-space-1) var(--cb-space-2);border:var(--cb-border-width) solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background-color:var(--cb-color-bg);color:var(--cb-color-text);font:inherit;outline:none}.cb-multiselect__filter-input:focus{border-color:var(--cb-color-primary-500)}.cb-multiselect__list{list-style:none;margin:0;padding:var(--cb-space-1);max-height:15rem;overflow-y:auto}.cb-multiselect__option{display:flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-2) var(--cb-space-3);border-radius:var(--cb-radius-sm);cursor:pointer;color:var(--cb-color-text)}.cb-multiselect__option.cb-is-active{background-color:var(--cb-color-surface-hover)}.cb-multiselect__option.cb-is-selected{color:var(--cb-color-primary-700)}.cb-multiselect__option.cb-is-disabled{opacity:var(--cb-opacity-disabled, .55);cursor:default}.cb-multiselect__check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:1.1rem;height:1.1rem;border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-sm);color:var(--cb-color-primary-contrast)}.cb-multiselect__check.cb-is-checked{background-color:var(--cb-color-primary-500);border-color:var(--cb-color-primary-500)}.cb-multiselect__empty{padding:var(--cb-space-3);color:var(--cb-color-text-muted);text-align:center}:host-context([data-theme=\"dark\"]) .cb-multiselect__option.cb-is-selected{color:var(--cb-color-primary-200)}.cb-multiselect__group{list-style:none;margin:0;padding:0}.cb-multiselect__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-multiselect__list--virtual{max-height:none}.cb-multiselect__spacer{position:relative;list-style:none}.cb-multiselect__window{list-style:none;margin:0;padding:0}.cb-multiselect__list--virtual .cb-multiselect__option{box-sizing:border-box;padding-top:0;padding-bottom:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\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 }); }
361
580
  }
362
581
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbMultiSelect, decorators: [{
363
582
  type: Component,
364
- args: [{ selector: 'cb-multiselect', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [OverlayModule, CbIcon], host: { class: 'cb-multiselect-host' }, providers: [
583
+ args: [{ selector: 'cb-multiselect', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [OverlayModule, NgTemplateOutlet, CbIcon], host: { class: 'cb-multiselect-host' }, providers: [
365
584
  {
366
585
  provide: NG_VALUE_ACCESSOR,
367
586
  useExisting: forwardRef(() => CbMultiSelect),
@@ -383,6 +602,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
383
602
  aria-haspopup="listbox"
384
603
  [attr.aria-expanded]="open()"
385
604
  [attr.aria-controls]="listboxId"
605
+ [attr.aria-activedescendant]="filter() ? null : activeDescendant()"
386
606
  [attr.aria-disabled]="isDisabled() ? 'true' : null"
387
607
  [attr.aria-invalid]="fieldState.invalid() ? 'true' : null"
388
608
  [attr.aria-required]="fieldState.required() || null"
@@ -411,59 +631,141 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
411
631
  @if (filter() || showToggleAll()) {
412
632
  <div class="cb-multiselect__header">
413
633
  @if (showToggleAll()) {
414
- <button type="button" class="cb-multiselect__all" (click)="toggleAll()">
415
- <span class="cb-multiselect__check" [class.cb-is-checked]="allSelected()">
416
- @if (allSelected()) { <cb-icon name="check" [size]="14" aria-hidden="true" /> }
634
+ <button type="button" class="cb-multiselect__all" [attr.aria-pressed]="allSelected()" (click)="toggleAll()">
635
+ <span class="cb-multiselect__check" [class.cb-is-checked]="allSelected()" aria-hidden="true">
636
+ @if (allSelected()) {
637
+ <cb-icon name="check" [size]="14" />
638
+ }
417
639
  </span>
418
- Todos
640
+ {{ t().selectAll }}
419
641
  </button>
420
642
  }
421
643
  @if (filter()) {
644
+ <!-- Con filtro, el foco vive aquí: es el combobox que anuncia la opción activa. -->
422
645
  <input
423
646
  #filterInput
424
647
  type="text"
425
648
  class="cb-multiselect__filter-input"
426
- [placeholder]="filterPlaceholder()"
649
+ role="combobox"
650
+ aria-autocomplete="list"
651
+ aria-expanded="true"
652
+ [attr.aria-controls]="listboxId"
653
+ [attr.aria-activedescendant]="activeDescendant()"
654
+ [placeholder]="(filterPlaceholder() ?? t().search)"
427
655
  [value]="filterText()"
428
656
  (input)="onFilter($event)"
429
657
  (keydown)="onTriggerKeydown($event)"
430
- aria-label="Filtrar opciones"
658
+ [attr.aria-label]="t().filterOptions"
431
659
  />
432
660
  }
433
661
  </div>
434
662
  }
435
- <ul class="cb-multiselect__list" role="listbox" aria-multiselectable="true" [id]="listboxId">
436
- @for (opt of filteredOptions(); track trackOption(opt); let i = $index) {
437
- <li
438
- class="cb-multiselect__option"
439
- [class.cb-is-active]="i === activeIndex()"
440
- [class.cb-is-selected]="isSelected(opt)"
441
- [class.cb-is-disabled]="isOptionDisabled(opt)"
442
- role="option"
443
- [id]="optionId(i)"
444
- [attr.aria-selected]="isSelected(opt)"
445
- [attr.aria-disabled]="isOptionDisabled(opt) ? 'true' : null"
446
- (click)="toggleOption(opt)"
447
- (mouseenter)="activeIndex.set(i)"
448
- >
449
- <span class="cb-multiselect__check" [class.cb-is-checked]="isSelected(opt)">
450
- @if (isSelected(opt)) { <cb-icon name="check" [size]="14" aria-hidden="true" /> }
451
- </span>
452
- {{ labelOf(opt) }}
453
- </li>
454
- } @empty {
455
- <li class="cb-multiselect__empty" role="option" aria-disabled="true">
456
- {{ emptyMessage() }}
663
+ @if (virtualScroll()) {
664
+ <ul
665
+ #viewport
666
+ class="cb-multiselect__list cb-multiselect__list--virtual"
667
+ role="listbox"
668
+ aria-multiselectable="true"
669
+ [id]="listboxId"
670
+ [style.height.px]="viewportHeight()"
671
+ (scroll)="scrollTop.set($any($event.target).scrollTop)"
672
+ >
673
+ <li role="presentation" class="cb-multiselect__spacer" [style.height.px]="virtualRows().length * virtualScrollItemSize()">
674
+ <ul role="presentation" class="cb-multiselect__window" [style.transform]="'translateY(' + windowOffset() + 'px)'">
675
+ @for (row of visibleRows(); track row.kind === 'group' ? row.key : row.index) {
676
+ @if (row.kind === 'group') {
677
+ <li role="presentation" class="cb-multiselect__group-label" [style.height.px]="virtualScrollItemSize()">
678
+ {{ row.label }}
679
+ </li>
680
+ } @else {
681
+ <ng-container
682
+ *ngTemplateOutlet="optionTpl; context: { $implicit: row, height: virtualScrollItemSize(), virtual: true }"
683
+ />
684
+ }
685
+ }
686
+ </ul>
457
687
  </li>
458
- }
459
- </ul>
688
+ @if (!filteredOptions().length) {
689
+ <li class="cb-multiselect__empty" role="option" aria-disabled="true">{{ (emptyMessage() ?? t().noResults) }}</li>
690
+ }
691
+ </ul>
692
+ } @else {
693
+ <ul
694
+ #viewport
695
+ class="cb-multiselect__list"
696
+ role="listbox"
697
+ aria-multiselectable="true"
698
+ [id]="listboxId"
699
+ [style.max-height]="scrollHeight()"
700
+ >
701
+ @for (g of filteredGroups(); track $index; let gi = $index) {
702
+ @if (g.label !== null) {
703
+ <li role="presentation">
704
+ <ul role="group" class="cb-multiselect__group" [attr.aria-labelledby]="groupId(gi)">
705
+ <li role="presentation" class="cb-multiselect__group-label" [id]="groupId(gi)">{{ g.label }}</li>
706
+ @for (item of g.options; track item.index) {
707
+ <ng-container *ngTemplateOutlet="optionTpl; context: { $implicit: item, height: null, virtual: false }" />
708
+ }
709
+ </ul>
710
+ </li>
711
+ } @else {
712
+ @for (item of g.options; track item.index) {
713
+ <ng-container *ngTemplateOutlet="optionTpl; context: { $implicit: item, height: null, virtual: false }" />
714
+ }
715
+ }
716
+ }
717
+ @if (!filteredOptions().length) {
718
+ <li class="cb-multiselect__empty" role="option" aria-disabled="true">{{ (emptyMessage() ?? t().noResults) }}</li>
719
+ }
720
+ </ul>
721
+ }
460
722
  </div>
461
723
  </ng-template>
462
- `, styles: [":host{display:block}.cb-multiselect__trigger{display:flex;align-items:center;gap:var(--cb-space-2);width:100%;min-height:2.5rem;padding:0 var(--cb-space-3);background-color:var(--cb-color-bg);border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);color:var(--cb-color-text);font-family:var(--cb-font-sans);cursor:pointer;transition:border-color var(--cb-duration-fast) var(--cb-ease),box-shadow var(--cb-duration-fast) var(--cb-ease)}.cb-multiselect__trigger--sm{min-height:2.125rem;font-size:var(--cb-text-sm)}.cb-multiselect__trigger--lg{min-height:2.875rem;font-size:var(--cb-text-lg)}.cb-multiselect__trigger:focus-visible,.cb-multiselect__trigger.cb-is-open{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 3px var(--cb-color-focus-ring)}.cb-multiselect__trigger.cb-is-invalid{border-color:var(--cb-color-danger)}.cb-multiselect__trigger.cb-is-disabled{opacity:var(--cb-opacity-disabled, .55);cursor:default;background-color:var(--cb-color-surface-100)}.cb-multiselect__label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.cb-multiselect__label--placeholder{color:var(--cb-color-text-muted)}.cb-multiselect__chevron{flex:none;color:var(--cb-color-text-muted);transition:transform var(--cb-duration-fast) var(--cb-ease)}.cb-multiselect__trigger.cb-is-open .cb-multiselect__chevron{transform:rotate(180deg)}.cb-multiselect__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-multiselect__header{display:flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-2);border-bottom:1px solid var(--cb-color-border)}.cb-multiselect__all{display:inline-flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-1) var(--cb-space-2);border:none;background:transparent;color:var(--cb-color-text);border-radius:var(--cb-radius-sm);cursor:pointer;font:inherit;white-space:nowrap}.cb-multiselect__all:hover{background-color:var(--cb-color-surface-100)}.cb-multiselect__filter-input{flex:1 1 auto;min-width:0;padding:var(--cb-space-1) var(--cb-space-2);border:var(--cb-border-width) solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background-color:var(--cb-color-bg);color:var(--cb-color-text);font:inherit;outline:none}.cb-multiselect__filter-input:focus{border-color:var(--cb-color-primary-500)}.cb-multiselect__list{list-style:none;margin:0;padding:var(--cb-space-1);max-height:15rem;overflow-y:auto}.cb-multiselect__option{display:flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-2) var(--cb-space-3);border-radius:var(--cb-radius-sm);cursor:pointer;color:var(--cb-color-text)}.cb-multiselect__option.cb-is-active{background-color:var(--cb-color-surface-100)}.cb-multiselect__option.cb-is-selected{color:var(--cb-color-primary-700)}.cb-multiselect__option.cb-is-disabled{opacity:var(--cb-opacity-disabled, .55);cursor:default}.cb-multiselect__check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:1.1rem;height:1.1rem;border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-sm);color:var(--cb-color-primary-contrast)}.cb-multiselect__check.cb-is-checked{background-color:var(--cb-color-primary-500);border-color:var(--cb-color-primary-500)}.cb-multiselect__empty{padding:var(--cb-space-3);color:var(--cb-color-text-muted);text-align:center}:host-context([data-theme=\"dark\"]) .cb-multiselect__option.cb-is-selected{color:var(--cb-color-primary-200)}\n"] }]
463
- }], 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 }] }], showToggleAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToggleAll", required: false }] }], maxSelectedLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelectedLabels", 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 }] }] } });
724
+
725
+ <ng-template #optionTpl let-item let-height="height" let-virtual="virtual">
726
+ <li
727
+ class="cb-multiselect__option"
728
+ [class.cb-is-active]="item.index === activeIndex()"
729
+ [class.cb-is-selected]="isSelected(item.opt)"
730
+ [class.cb-is-disabled]="isOptionDisabled(item.opt)"
731
+ role="option"
732
+ [id]="optionId(item.index)"
733
+ [style.height.px]="height"
734
+ [attr.aria-selected]="isSelected(item.opt)"
735
+ [attr.aria-disabled]="isOptionDisabled(item.opt) ? 'true' : null"
736
+ [attr.aria-setsize]="virtual ? filteredOptions().length : null"
737
+ [attr.aria-posinset]="virtual ? item.index + 1 : null"
738
+ (click)="toggleOption(item.opt)"
739
+ (mouseenter)="activeIndex.set(item.index)"
740
+ >
741
+ <span class="cb-multiselect__check" [class.cb-is-checked]="isSelected(item.opt)" aria-hidden="true">
742
+ @if (isSelected(item.opt)) {
743
+ <cb-icon name="check" [size]="14" />
744
+ }
745
+ </span>
746
+ {{ labelOf(item.opt) }}
747
+ </li>
748
+ </ng-template>
749
+ `, styles: [":host{display:block}.cb-multiselect__trigger{display:flex;align-items:center;gap:var(--cb-space-2);width:100%;min-height:2.5rem;padding:0 var(--cb-space-3);background-color:var(--cb-color-bg);border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);color:var(--cb-color-text);font-family:var(--cb-font-sans);cursor:pointer;transition:border-color var(--cb-duration-fast) var(--cb-ease),box-shadow var(--cb-duration-fast) var(--cb-ease)}.cb-multiselect__trigger--sm{min-height:2.125rem;font-size:var(--cb-text-sm)}.cb-multiselect__trigger--lg{min-height:2.875rem;font-size:var(--cb-text-lg)}.cb-multiselect__trigger:focus-visible,.cb-multiselect__trigger.cb-is-open{outline:none;border-color:var(--cb-color-primary-500);box-shadow:0 0 0 3px var(--cb-color-focus-ring)}.cb-multiselect__trigger.cb-is-invalid{border-color:var(--cb-color-danger)}.cb-multiselect__trigger.cb-is-disabled{opacity:var(--cb-opacity-disabled, .55);cursor:default;background-color:var(--cb-color-surface)}.cb-multiselect__label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.cb-multiselect__label--placeholder{color:var(--cb-color-text-muted)}.cb-multiselect__chevron{flex:none;color:var(--cb-color-text-muted);transition:transform var(--cb-duration-fast) var(--cb-ease)}.cb-multiselect__trigger.cb-is-open .cb-multiselect__chevron{transform:rotate(180deg)}.cb-multiselect__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-multiselect__header{display:flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-2);border-bottom:1px solid var(--cb-color-border)}.cb-multiselect__all{display:inline-flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-1) var(--cb-space-2);border:none;background:transparent;color:var(--cb-color-text);border-radius:var(--cb-radius-sm);cursor:pointer;font:inherit;white-space:nowrap}.cb-multiselect__all:hover{background-color:var(--cb-color-surface-hover)}.cb-multiselect__filter-input{flex:1 1 auto;min-width:0;padding:var(--cb-space-1) var(--cb-space-2);border:var(--cb-border-width) solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background-color:var(--cb-color-bg);color:var(--cb-color-text);font:inherit;outline:none}.cb-multiselect__filter-input:focus{border-color:var(--cb-color-primary-500)}.cb-multiselect__list{list-style:none;margin:0;padding:var(--cb-space-1);max-height:15rem;overflow-y:auto}.cb-multiselect__option{display:flex;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-2) var(--cb-space-3);border-radius:var(--cb-radius-sm);cursor:pointer;color:var(--cb-color-text)}.cb-multiselect__option.cb-is-active{background-color:var(--cb-color-surface-hover)}.cb-multiselect__option.cb-is-selected{color:var(--cb-color-primary-700)}.cb-multiselect__option.cb-is-disabled{opacity:var(--cb-opacity-disabled, .55);cursor:default}.cb-multiselect__check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:1.1rem;height:1.1rem;border:var(--cb-border-width) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-sm);color:var(--cb-color-primary-contrast)}.cb-multiselect__check.cb-is-checked{background-color:var(--cb-color-primary-500);border-color:var(--cb-color-primary-500)}.cb-multiselect__empty{padding:var(--cb-space-3);color:var(--cb-color-text-muted);text-align:center}:host-context([data-theme=\"dark\"]) .cb-multiselect__option.cb-is-selected{color:var(--cb-color-primary-200)}.cb-multiselect__group{list-style:none;margin:0;padding:0}.cb-multiselect__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-multiselect__list--virtual{max-height:none}.cb-multiselect__spacer{position:relative;list-style:none}.cb-multiselect__window{list-style:none;margin:0;padding:0}.cb-multiselect__list--virtual .cb-multiselect__option{box-sizing:border-box;padding-top:0;padding-bottom:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
750
+ }], 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 }] }], showToggleAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToggleAll", required: false }] }], maxSelectedLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelectedLabels", 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 }] }] } });
464
751
  function toBoolean(value) {
465
752
  return value !== false && value != null && `${value}` !== 'false';
466
753
  }
754
+ /** Minúsculas y sin tildes, para que "medellin" encuentre "Medellín". */
755
+ function normalize(text) {
756
+ return text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLocaleLowerCase().trim();
757
+ }
758
+ /** Tecla imprimible sin modificadores (para la búsqueda al teclear). */
759
+ function isPrintable(event) {
760
+ return event.key.length === 1 && event.key !== ' ' && !event.ctrlKey && !event.metaKey && !event.altKey;
761
+ }
762
+ /** Convierte un alto CSS (`px` o `rem`) a píxeles para el modo virtual. */
763
+ function cssToPx(value) {
764
+ const n = parseFloat(value);
765
+ if (Number.isNaN(n))
766
+ return 256;
767
+ return value.trim().endsWith('rem') ? n * 16 : n;
768
+ }
467
769
 
468
770
  /*
469
771
  * Public API Surface de @caribeng/ui/multiselect