@cdevhub/ngx-tw 0.5.0 → 0.6.1

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 (209) hide show
  1. package/README.md +78 -15
  2. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +16 -5
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
  4. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +26 -15
  5. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
  6. package/fesm2022/cdevhub-ngx-tw-badge.mjs +17 -5
  7. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
  8. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +27 -6
  9. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
  10. package/fesm2022/cdevhub-ngx-tw-button.mjs +23 -11
  11. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
  12. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +5 -5
  13. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
  14. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +3 -3
  15. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -1
  16. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +427 -122
  17. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
  18. package/fesm2022/cdevhub-ngx-tw-card.mjs +21 -11
  19. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
  20. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +215 -96
  21. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
  22. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +108 -64
  23. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
  24. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +18 -6
  25. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
  26. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +55 -30
  27. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
  28. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs +218 -0
  29. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs.map +1 -0
  30. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +236 -211
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
  32. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs +200 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs.map +1 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +34 -7
  35. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs +135 -15
  37. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
  38. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs +203 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs.map +1 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +160 -102
  41. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
  42. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs +209 -0
  43. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs.map +1 -0
  44. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +182 -107
  45. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
  46. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
  47. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
  48. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +11 -2
  49. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  50. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
  51. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
  53. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
  54. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
  55. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
  56. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
  57. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
  59. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
  61. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  62. package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
  63. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-item.mjs +28 -4
  65. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
  67. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
  68. package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
  69. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  70. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
  71. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
  73. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
  75. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
  77. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
  79. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
  81. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
  83. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
  84. package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
  85. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  86. package/fesm2022/{cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs → cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs} +3 -3
  87. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
  88. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
  89. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
  90. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +23 -7
  91. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  92. package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
  93. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
  95. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
  97. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
  99. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
  101. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  102. package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
  103. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
  105. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
  107. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
  109. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
  111. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
  112. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
  113. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
  114. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
  115. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  116. package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
  117. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  118. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
  119. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
  120. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
  121. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  122. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
  123. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  124. package/fesm2022/{cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs → cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs} +107 -19
  125. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
  126. package/fesm2022/cdevhub-ngx-tw-toast.mjs +21 -13
  127. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  128. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
  129. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  130. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
  131. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
  132. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
  133. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
  134. package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
  135. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
  136. package/index.json +1 -1
  137. package/package.json +63 -2
  138. package/theme/_base.css +13 -0
  139. package/theme/_dark.css +111 -36
  140. package/theme/_high-contrast-dark.css +367 -0
  141. package/theme/_high-contrast.css +13 -2
  142. package/theme/_light.css +332 -0
  143. package/theme/_semantic.css +60 -26
  144. package/theme/_typography.css +17 -0
  145. package/theme/index.css +86 -7
  146. package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
  147. package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
  148. package/types/cdevhub-ngx-tw-button.d.ts +1 -0
  149. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
  150. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
  151. package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
  152. package/types/cdevhub-ngx-tw-card.d.ts +11 -3
  153. package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
  154. package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
  155. package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
  156. package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
  157. package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
  158. package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
  159. package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
  160. package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
  161. package/types/cdevhub-ngx-tw-core.d.ts +152 -21
  162. package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
  163. package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
  164. package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
  165. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
  166. package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
  167. package/types/cdevhub-ngx-tw-dialog.d.ts +12 -3
  168. package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
  169. package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
  170. package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
  171. package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
  172. package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
  173. package/types/cdevhub-ngx-tw-input.d.ts +5 -8
  174. package/types/cdevhub-ngx-tw-item.d.ts +2 -0
  175. package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
  176. package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
  177. package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
  178. package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
  179. package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
  180. package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
  181. package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
  182. package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
  183. package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
  184. package/types/cdevhub-ngx-tw-select.d.ts +104 -33
  185. package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
  186. package/types/cdevhub-ngx-tw-sheet.d.ts +19 -3
  187. package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
  188. package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
  189. package/types/cdevhub-ngx-tw-split.d.ts +32 -2
  190. package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
  191. package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
  192. package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
  193. package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
  194. package/types/cdevhub-ngx-tw-table.d.ts +53 -10
  195. package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
  196. package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
  197. package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
  198. package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
  199. package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
  200. package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
  201. package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
  202. package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
  203. package/types/cdevhub-ngx-tw-toast.d.ts +20 -6
  204. package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
  205. package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
  206. package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
  207. package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
  208. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs.map +0 -1
  209. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs.map +0 -1
@@ -1,15 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, ElementRef, signal, viewChild, computed, ChangeDetectionStrategy, Component, TemplateRef, Directive, input, model, output, contentChild, ViewContainerRef, Injector, DestroyRef, linkedSignal, effect, untracked, afterNextRender, isDevMode, forwardRef } from '@angular/core';
3
3
  import { NgTemplateOutlet } from '@angular/common';
4
- import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
4
+ import { NgControl } from '@angular/forms';
5
5
  import { Overlay } from '@angular/cdk/overlay';
6
- import { ComponentPortal } from '@angular/cdk/portal';
7
6
  import { Platform } from '@angular/cdk/platform';
8
7
  import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
9
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
10
- import { Subscription, merge } from 'rxjs';
11
8
  import { tv } from 'tailwind-variants';
12
- import { TW_ERROR_STATE_MATCHER, buildSelectLikePositions, resolveSelectScrollStrategy, consumeOverlayEscape } from '@cdevhub/ngx-tw/core';
9
+ import { PickerOverlayCoordinator, wireErrorState, resolveSelectScrollStrategy, buildSelectLikePositions } from '@cdevhub/ngx-tw/core';
13
10
  import { TW_FORM_FIELD, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
14
11
 
15
12
  /**
@@ -44,6 +41,16 @@ class SelectOverlayComponent {
44
41
  /** @internal */
45
42
  activeIndex = signal(-1, /* @ts-ignore */
46
43
  ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
44
+ /**
45
+ * @internal Id of the active option, mirrored from the parent select.
46
+ *
47
+ * In searchable mode DOM focus sits on this panel's search input, so
48
+ * `aria-activedescendant` has to live there — an implicit `searchbox`
49
+ * allows it (inherited from `textbox`). The trigger drops the attribute for
50
+ * as long as the panel is open.
51
+ */
52
+ activeDescendantId = signal(null, /* @ts-ignore */
53
+ ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : /* istanbul ignore next */ []));
47
54
  /** @internal */
48
55
  renderedRows = signal([], /* @ts-ignore */
49
56
  ...(ngDevMode ? [{ debugName: "renderedRows" }] : /* istanbul ignore next */ []));
@@ -195,6 +202,7 @@ class SelectOverlayComponent {
195
202
  [id]="searchInputId()"
196
203
  [attr.aria-label]="'Search'"
197
204
  [attr.aria-controls]="listboxId()"
205
+ [attr.aria-activedescendant]="activeDescendantId()"
198
206
  [value]="search()"
199
207
  class="w-full px-3 py-1.5 rounded-md border border-border bg-surface text-fg placeholder:text-fg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 transition-colors duration-normal motion-reduce:transition-none text-sm"
200
208
  placeholder="Search…"
@@ -311,6 +319,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
311
319
  [id]="searchInputId()"
312
320
  [attr.aria-label]="'Search'"
313
321
  [attr.aria-controls]="listboxId()"
322
+ [attr.aria-activedescendant]="activeDescendantId()"
314
323
  [value]="search()"
315
324
  class="w-full px-3 py-1.5 rounded-md border border-border bg-surface text-fg placeholder:text-fg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 transition-colors duration-normal motion-reduce:transition-none text-sm"
316
325
  placeholder="Search…"
@@ -403,36 +412,94 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
403
412
  }]
404
413
  }], propDecorators: { searchInputRef: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }] } });
405
414
 
406
- // Duration for leave animation — matches theme/_base.css scale-out/fade-out.
407
- const ANIMATION_DURATION = 120;
408
415
  // ── tv() config ───────────────────────────────────────────────────
409
416
  const selectVariants = tv({
410
417
  slots: {
411
418
  root: 'relative inline-block w-full',
412
- trigger: 'w-full inline-flex items-center gap-2 text-fg cursor-pointer transition-[color,border-color,background-color,box-shadow] duration-normal motion-reduce:transition-none',
419
+ // `flex`, NOT `inline-flex`. An inline-level trigger inside the block
420
+ // `root` generates a line box, so the host's height became
421
+ // `max(strutAscent, triggerAscent) + triggerDescent` — taller than the
422
+ // trigger itself whenever the consumer's inherited strut out-ascends it.
423
+ // That is what made `xs` measure an odd 27px (26px trigger + a
424
+ // font-metric-dependent fraction) while combobox, whose trigger is
425
+ // block-level `flex`, measured a clean 26. A block-level trigger removes
426
+ // the line box entirely, so the host height equals the pinned height.
427
+ trigger: 'w-full flex items-center gap-2 text-fg cursor-pointer transition-[color,border-color,background-color,box-shadow] duration-normal motion-reduce:transition-none',
413
428
  valueText: 'flex-1 min-w-0 text-left truncate',
414
429
  placeholderText: 'flex-1 min-w-0 text-left truncate text-fg-subtle',
415
430
  chevron: 'shrink-0 text-fg-muted transition-transform duration-normal motion-reduce:transition-none',
416
- clearButton: 'inline-flex items-center justify-center shrink-0 rounded-md text-fg-muted hover:text-fg hover:bg-surface-muted transition-colors duration-normal motion-reduce:transition-none size-5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
431
+ // In-flow placeholder that holds the clear control's slot inside the
432
+ // trigger's flex row. The control itself is an absolutely positioned
433
+ // SIBLING of the trigger, because HTML's content model forbids
434
+ // interactive content inside a `button`. Keeping a same-sized box in flow
435
+ // means the value text truncates at exactly the same point, and the
436
+ // `naked` variant's `h-auto` trigger keeps the same 24px floor, as when
437
+ // the control was itself a flex item. Reserving the space with trailing
438
+ // padding instead does NOT work: tailwind-merge does not treat `pe-*` as
439
+ // conflicting with `px-*`, so both would survive the merge.
440
+ clearSpacer: 'size-6 shrink-0',
441
+ // `size-6` (24px) is the WCAG 2.2 SC 2.5.8 target-size floor and the `xs`
442
+ // step of the square-interactive scale. It does NOT scale with `size`:
443
+ // the clear sits over the trigger, whose smallest pinned height is 24px
444
+ // (`xs` -> `h-6`), so any larger step would overflow the smallest
445
+ // trigger. The floor is what the success criterion asks for.
446
+ //
447
+ // `end-*` (inset-inline-end), never `right-*`: the trigger's flex row
448
+ // reverses under `dir="rtl"`, so a physical offset would park the control
449
+ // on the wrong side. Per-size values live in the `size` variant below.
450
+ clearButton: 'absolute top-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-md text-fg-muted hover:text-fg hover:bg-surface-muted transition-colors duration-normal motion-reduce:transition-none size-6 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
417
451
  },
418
452
  variants: {
453
+ // Trigger height is PINNED (docs/vertical-rhythm.md §1-3): the box is
454
+ // always one line, so it declares its border-box height and carries no
455
+ // vertical padding. Horizontal padding and the font size are unchanged.
456
+ //
457
+ // The `clearButton` offsets place the absolutely positioned control back
458
+ // over the in-flow `clearSpacer` it replaced. Each is the trigger's
459
+ // 1px border + its horizontal padding + the chevron's width + `gap-2`,
460
+ // rounded to the nearest spacing step (so each sits 1px inboard of the
461
+ // spacer, which is not perceivable). The `naked` variant drops the border
462
+ // and the padding and takes its own values in `compoundVariants` below.
419
463
  size: {
420
464
  // xs density: chevron uses `size-3.5` (14px) — half-step that lines up
421
465
  // with text-xs inside the compact trigger.
422
- xs: { trigger: 'px-2 py-1 text-xs', chevron: 'size-3.5' },
423
- sm: { trigger: 'px-3 py-1.5 text-sm', chevron: 'size-4' },
424
- md: { trigger: 'px-4 py-2 text-sm', chevron: 'size-4' },
425
- lg: { trigger: 'px-5 py-2.5 text-base', chevron: 'size-5' },
426
- xl: { trigger: 'px-6 py-3 text-base', chevron: 'size-5' },
466
+ xs: { trigger: 'px-2 text-xs h-6', chevron: 'size-3.5', clearButton: 'end-8' },
467
+ sm: { trigger: 'px-3 text-sm h-8', chevron: 'size-4', clearButton: 'end-9' },
468
+ md: { trigger: 'px-4 text-sm h-9', chevron: 'size-4', clearButton: 'end-10' },
469
+ lg: { trigger: 'px-5 text-base h-11', chevron: 'size-5', clearButton: 'end-12' },
470
+ xl: { trigger: 'px-6 text-base h-12', chevron: 'size-5', clearButton: 'end-13' },
427
471
  },
428
472
  variant: {
429
473
  default: {
430
474
  trigger: 'border border-border bg-surface rounded-md hover:border-border-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
431
475
  },
476
+ // `naked` = wrapped in a `tw-form-field`, which owns the box: its
477
+ // controlWrapper draws the border and the vertical padding. `h-auto`
478
+ // releases the pinned height (declared after `size`, so twMerge keeps
479
+ // it — same ordering `p-0` already relies on) so the trigger
480
+ // contributes only its line box and the field's own rhythm governs.
481
+ //
482
+ // The focus ring is NOT declared here — it depends on whether a
483
+ // form-field is actually present. See `fieldOwnsFocusRing` below.
432
484
  naked: {
433
- trigger: 'bg-transparent border-0 rounded-none p-0 focus-visible:outline-none',
485
+ trigger: 'bg-transparent border-0 rounded-none p-0 h-auto',
434
486
  },
435
487
  },
488
+ // Whether an ancestor `tw-form-field` draws the focus ring for us. Only
489
+ // the `naked` variant reads it, via the compound variants below: inside
490
+ // a field the control wrapper owns the ring, so the trigger suppresses
491
+ // its own; used standalone there is no wrapper, so the trigger has to
492
+ // draw the canonical ring itself or the composite has no focus
493
+ // indicator at all (WCAG 2.2 SC 2.4.7).
494
+ //
495
+ // This cannot be expressed by adding `focus-visible:outline-none` to
496
+ // `naked` and overriding it later: `outline-none` (outline-style) and
497
+ // `outline-2` (outline-width) sit in different tailwind-merge conflict
498
+ // groups, so both survive the merge and `outline-style: none` wins.
499
+ fieldOwnsFocusRing: {
500
+ true: {},
501
+ false: {},
502
+ },
436
503
  open: {
437
504
  true: { chevron: 'rotate-180' },
438
505
  false: { chevron: 'rotate-0' },
@@ -462,6 +529,25 @@ const selectVariants = tv({
462
529
  { variant: 'default', focused: true, color: 'success', class: { trigger: 'border-success-500' } },
463
530
  { variant: 'default', focused: true, color: 'warning', class: { trigger: 'border-warning-500' } },
464
531
  { variant: 'default', focused: true, color: 'error', class: { trigger: 'border-error-500' } },
532
+ // `naked` drops the border and the horizontal padding (`px-0`), so the
533
+ // clear control's offset collapses to chevron width + `gap-2`.
534
+ { variant: 'naked', size: 'xs', class: { clearButton: 'end-6' } },
535
+ { variant: 'naked', size: 'sm', class: { clearButton: 'end-6' } },
536
+ { variant: 'naked', size: 'md', class: { clearButton: 'end-6' } },
537
+ { variant: 'naked', size: 'lg', class: { clearButton: 'end-7' } },
538
+ { variant: 'naked', size: 'xl', class: { clearButton: 'end-7' } },
539
+ {
540
+ variant: 'naked',
541
+ fieldOwnsFocusRing: true,
542
+ class: { trigger: 'focus-visible:outline-none' },
543
+ },
544
+ {
545
+ variant: 'naked',
546
+ fieldOwnsFocusRing: false,
547
+ class: {
548
+ trigger: 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
549
+ },
550
+ },
465
551
  ],
466
552
  defaultVariants: {
467
553
  size: 'md',
@@ -470,6 +556,7 @@ const selectVariants = tv({
470
556
  disabled: false,
471
557
  focused: false,
472
558
  color: 'primary',
559
+ fieldOwnsFocusRing: false,
473
560
  },
474
561
  }, { twMerge: true });
475
562
  // ── Option-state color lookups ──
@@ -590,16 +677,16 @@ class SelectComponent {
590
677
  /** Array of options to render in the panel. Accepts either `TwSelectOption<T>` objects or arbitrary records read via the accessor inputs. Defaults to an empty array. */
591
678
  options = input([], /* @ts-ignore */
592
679
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
593
- /** Accessor returning the visible label for an option. Override when passing arbitrary objects. */
680
+ /** Accessor returning the visible label for an option. Override when passing arbitrary objects. Defaults to reading `.label`, falling back to `String(.value)`. */
594
681
  optionLabel = input(defaultOptionLabel, /* @ts-ignore */
595
682
  ...(ngDevMode ? [{ debugName: "optionLabel" }] : /* istanbul ignore next */ []));
596
- /** Accessor returning the value for an option. The result is what `value` / `valueChange` emit. */
683
+ /** Accessor returning the value for an option. The result is what `value` / `valueChange` emit. Defaults to reading `.value`. */
597
684
  optionValue = input(defaultOptionValue, /* @ts-ignore */
598
685
  ...(ngDevMode ? [{ debugName: "optionValue" }] : /* istanbul ignore next */ []));
599
686
  /** Accessor returning the disabled state for an option. Defaults to reading `.disabled`. */
600
687
  optionDisabled = input(defaultOptionDisabled, /* @ts-ignore */
601
688
  ...(ngDevMode ? [{ debugName: "optionDisabled" }] : /* istanbul ignore next */ []));
602
- /** Accessor returning the group name for an option. Options sharing a group render under a labelled `role="group"` region. */
689
+ /** Accessor returning the group name for an option. Options sharing a group render under a labelled `role="group"` region. Defaults to reading `.group`. */
603
690
  optionGroup = input(defaultOptionGroup, /* @ts-ignore */
604
691
  ...(ngDevMode ? [{ debugName: "optionGroup" }] : /* istanbul ignore next */ []));
605
692
  /** When true, enables multi-selection. The `value` model becomes a `T[]` and the panel renders checkable options. Defaults to `false`. */
@@ -614,12 +701,12 @@ class SelectComponent {
614
701
  return label.toLowerCase().includes(search.toLowerCase());
615
702
  }, /* @ts-ignore */
616
703
  ...(ngDevMode ? [{ debugName: "filterPredicate" }] : /* istanbul ignore next */ []));
617
- /** Placeholder text shown in the trigger when no value is selected. */
704
+ /** Placeholder text shown in the trigger when no value is selected. Defaults to `undefined`. */
618
705
  placeholder = input(undefined, /* @ts-ignore */
619
706
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
620
- /** When true, the trigger cannot be activated and the panel cannot open. Defaults to `false`. */
707
+ /** When true, the trigger cannot be activated and the panel cannot open. Defaults to `false`. Alias: `disabled`. */
621
708
  disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled' });
622
- /** When true, exposes `aria-required="true"` on the trigger. Defaults to `false`. */
709
+ /** When true, exposes `aria-required="true"` on the trigger and the `*` marker on a wrapping `tw-form-field`. Also inferred from `Validators.required` on a bound control, so a reactive/template-driven form does not have to state it twice. Defaults to `false`. */
623
710
  requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required' });
624
711
  /** Controls trigger padding, font size, and panel option density. Defaults to `'md'`. */
625
712
  size = input('md', /* @ts-ignore */
@@ -633,7 +720,7 @@ class SelectComponent {
633
720
  /** Overlay panel width. `'trigger'` matches the trigger's measured width; `'auto'` lets content decide; a number is applied as pixels; a string is passed through as a CSS length. Defaults to `'trigger'`. */
634
721
  panelWidth = input('trigger', /* @ts-ignore */
635
722
  ...(ngDevMode ? [{ debugName: "panelWidth" }] : /* istanbul ignore next */ []));
636
- /** Extra class(es) applied to the overlay panel element. */
723
+ /** Extra class(es) applied to the overlay panel element. Defaults to an empty string. */
637
724
  panelClass = input('', /* @ts-ignore */
638
725
  ...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
639
726
  /** Maximum height of the listbox scroll region in pixels. Defaults to `256`. */
@@ -648,14 +735,14 @@ class SelectComponent {
648
735
  /** Pixel distance between trigger and panel. Defaults to `4`. */
649
736
  offset = input(4, /* @ts-ignore */
650
737
  ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
651
- /** Fallback message rendered when the filter yields no options and no `*twSelectEmpty` template is provided. */
738
+ /** Fallback message rendered when the filter yields no options and no `*twSelectEmpty` template is provided. Defaults to `'No results'`. */
652
739
  emptyMessage = input('No results', /* @ts-ignore */
653
740
  ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
654
- /** Accessible name for the combobox trigger. */
741
+ /** Accessible name for the combobox trigger. Defaults to `undefined`. Alias: `aria-label`. */
655
742
  ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
656
- /** ID of an external element that labels the combobox. */
743
+ /** ID of an external element that labels the combobox. Defaults to `undefined`. Alias: `aria-labelledby`. */
657
744
  ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
658
- /** ID of an external element that describes the combobox. */
745
+ /** ID of an external element that describes the combobox. Defaults to `undefined`. Alias: `aria-describedby`. */
659
746
  ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
660
747
  /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, the select uses the `TW_ERROR_STATE_MATCHER` token's value. */
661
748
  errorStateMatcher = input(undefined, /* @ts-ignore */
@@ -664,18 +751,18 @@ class SelectComponent {
664
751
  compareWith = input(Object.is, /* @ts-ignore */
665
752
  ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
666
753
  // ── Models (two-way) ──
667
- /** Two-way bound selected value(s). Single-select: `T | null`. Multi-select: `T[]`. */
754
+ /** Two-way bound selected value(s). Single-select: `T | null`. Multi-select: `T[]`. Defaults to `null`. */
668
755
  value = model(null, /* @ts-ignore */
669
756
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
670
- /** Two-way bound open state of the panel. */
757
+ /** Two-way bound open state of the panel. Defaults to `false`. */
671
758
  open = model(false, /* @ts-ignore */
672
759
  ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
673
760
  // ── Outputs ──
674
- /** Fires when the panel's visibility finishes changing. */
761
+ /** Fires when the panel's visibility finishes changing. Payload carries the new `open` state and the trigger element the overlay is anchored to. */
675
762
  openedChange = output();
676
763
  /** Fires after any selection change, with `added`, `removed`, `previousValue`, and a `source` discriminator. */
677
764
  selectionChange = output();
678
- /** Fires whenever the search input changes (only when `searchable` is true). */
765
+ /** Fires whenever the search input changes (only when `searchable` is true). Payload carries the raw, untrimmed query and the post-filter `visibleCount`. Emitted immediately, not debounced. */
679
766
  searchChange = output();
680
767
  // ── Content queries ──
681
768
  /** @internal */
@@ -693,8 +780,13 @@ class SelectComponent {
693
780
  /** @internal */
694
781
  footerTemplateChild = contentChild(SelectFooterTemplateDirective, /* @ts-ignore */
695
782
  ...(ngDevMode ? [{ debugName: "footerTemplateChild" }] : /* istanbul ignore next */ []));
783
+ // ── View queries ──
784
+ /** @internal The combobox trigger — focus target after the clear control unmounts. */
785
+ triggerButtonRef = viewChild('triggerButton', /* @ts-ignore */
786
+ ...(ngDevMode ? [{ debugName: "triggerButtonRef" }] : /* istanbul ignore next */ []));
696
787
  // ── Injected deps ──
697
788
  overlayService = inject(Overlay);
789
+ coordinator = inject(PickerOverlayCoordinator);
698
790
  elementRef = inject((ElementRef));
699
791
  viewContainerRef = inject(ViewContainerRef);
700
792
  injector = inject(Injector);
@@ -704,9 +796,6 @@ class SelectComponent {
704
796
  platform = inject(Platform);
705
797
  formField = inject(TW_FORM_FIELD, { optional: true });
706
798
  ngControl = inject(NgControl, { optional: true, self: true });
707
- parentForm = inject(NgForm, { optional: true });
708
- parentFormGroup = inject(FormGroupDirective, { optional: true });
709
- defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
710
799
  // ── Identity ──
711
800
  uid = nextSelectId++;
712
801
  /** @internal */
@@ -732,27 +821,35 @@ class SelectComponent {
732
821
  ...(ngDevMode ? [{ debugName: "focusedSignal" }] : /* istanbul ignore next */ []));
733
822
  cvaDisabled = signal(false, /* @ts-ignore */
734
823
  ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
735
- _ngControlRev = signal(0, /* @ts-ignore */
736
- ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
737
- _formSubmitRev = signal(0, /* @ts-ignore */
738
- ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
824
+ /** @internal Shared `errorState` / `required` / `errors` derivation — see `wireErrorState`. */
825
+ errorWiring = wireErrorState({
826
+ ngControl: () => this.ngControl,
827
+ matcher: () => this.errorStateMatcher(),
828
+ required: () => this.requiredInput(),
829
+ });
739
830
  describedByIdsSignal = signal([], /* @ts-ignore */
740
831
  ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
741
832
  labelledByIdsSignal = signal([], /* @ts-ignore */
742
833
  ...(ngDevMode ? [{ debugName: "labelledByIdsSignal" }] : /* istanbul ignore next */ []));
743
- closingSignal = signal(false, /* @ts-ignore */
744
- ...(ngDevMode ? [{ debugName: "closingSignal" }] : /* istanbul ignore next */ []));
745
834
  onChange = () => { };
746
835
  onTouched = () => { };
747
- overlayRef = null;
748
- overlayInstanceSignal = signal(null, /* @ts-ignore */
749
- ...(ngDevMode ? [{ debugName: "overlayInstanceSignal" }] : /* istanbul ignore next */ []));
750
- perOpenSubs = null;
751
- closeTimer = null;
836
+ /**
837
+ * Watches the host for size changes so `panelWidth="trigger"` keeps tracking
838
+ * while the panel is up. Minted per open and disconnected on close, because
839
+ * the `OverlayRef` it feeds is disposed on close (see `openOverlay`).
840
+ */
752
841
  resizeObserver = null;
753
- get overlayInstance() {
754
- return this.overlayInstanceSignal();
755
- }
842
+ // Overlay bookkeeping is deliberately held in plain fields, not signals — same
843
+ // shape as popover and command-palette. A signal here would be track-read by the
844
+ // open/close lifecycle effect that also writes it, which is the cycle shape
845
+ // CLAUDE.md forbids. The only reactive bit is `isAttached`, written by the
846
+ // lifecycle effect and read only by the state-push effect below, so the two
847
+ // never form a loop.
848
+ overlayInstance = null;
849
+ closing = false;
850
+ /** Flips true once the overlay component is attached; the sole trigger for the state-push effect. */
851
+ isAttached = signal(false, /* @ts-ignore */
852
+ ...(ngDevMode ? [{ debugName: "isAttached" }] : /* istanbul ignore next */ []));
756
853
  // ── Derived state ──
757
854
  /** @internal */
758
855
  isDisabled = computed(() => this.disabledInput() || this.cvaDisabled(), /* @ts-ignore */
@@ -841,6 +938,21 @@ class SelectComponent {
841
938
  return this.optionId(idx);
842
939
  }, /* @ts-ignore */
843
940
  ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : /* istanbul ignore next */ []));
941
+ /**
942
+ * @internal `aria-activedescendant` for the trigger button.
943
+ *
944
+ * Null while a searchable panel is open: `openOverlay()` moves DOM focus to
945
+ * the overlay's search input, and `aria-activedescendant` is only honoured
946
+ * on the element that actually holds focus. Leaving it on the blurred
947
+ * trigger made arrow-key navigation silent to assistive tech. The overlay's
948
+ * search input carries it instead in that mode.
949
+ */
950
+ triggerActiveDescendantId = computed(() => {
951
+ if (this.searchable() && this.open())
952
+ return null;
953
+ return this.activeDescendantId();
954
+ }, /* @ts-ignore */
955
+ ...(ngDevMode ? [{ debugName: "triggerActiveDescendantId" }] : /* istanbul ignore next */ []));
844
956
  /** @internal */
845
957
  describedBy = computed(() => {
846
958
  const extra = this.describedByIdsSignal();
@@ -877,6 +989,7 @@ class SelectComponent {
877
989
  disabled: this.isDisabled(),
878
990
  focused: this.focusedSignal(),
879
991
  color: this.color(),
992
+ fieldOwnsFocusRing: !!this.formField,
880
993
  }), /* @ts-ignore */
881
994
  ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
882
995
  /** @internal */
@@ -898,6 +1011,9 @@ class SelectComponent {
898
1011
  clearButtonClasses = computed(() => this.variantResult().clearButton(), /* @ts-ignore */
899
1012
  ...(ngDevMode ? [{ debugName: "clearButtonClasses" }] : /* istanbul ignore next */ []));
900
1013
  /** @internal */
1014
+ clearSpacerClasses = computed(() => this.variantResult().clearSpacer(), /* @ts-ignore */
1015
+ ...(ngDevMode ? [{ debugName: "clearSpacerClasses" }] : /* istanbul ignore next */ []));
1016
+ /** @internal */
901
1017
  optionSizeClass = computed(() => {
902
1018
  switch (this.size()) {
903
1019
  case 'xs':
@@ -946,19 +1062,20 @@ class SelectComponent {
946
1062
  empty = this.isEmpty;
947
1063
  /** @internal */
948
1064
  disabled = this.isDisabled;
949
- /** @internal */
950
- required = computed(() => this.requiredInput(), /* @ts-ignore */
951
- ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
1065
+ /**
1066
+ * @internal Resolved required state: the `required` input OR'd with
1067
+ * `Validators.required` on a bound `NgControl`. Without the validator arm the
1068
+ * form-field `*` marker (`FormFieldComponent.isRequired`) and the trigger's
1069
+ * `aria-required` silently vanish under reactive/template-driven forms, while
1070
+ * signal forms shows them — `cvaControlCreate` writes the `required` *input*
1071
+ * directly from the field state and never consults validators. The OR keeps
1072
+ * both branches true at once.
1073
+ */
1074
+ required = this.errorWiring.required;
1075
+ /** @internal Active validation errors map from the bound `NgControl`. Drives `[twError match="…"]` inside a wrapping `tw-form-field`; without it the form-field's key set is permanently empty and every `match`ed error stays hidden. */
1076
+ errors = this.errorWiring.errors;
952
1077
  /** @internal Error-state per the configured `ErrorStateMatcher`. Reads the bound `NgControl.invalid` through the matcher. */
953
- errorState = computed(() => {
954
- this._ngControlRev();
955
- this._formSubmitRev();
956
- const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
957
- const form = this.parentFormGroup ??
958
- this.parentForm;
959
- return matcher.isErrorState(this.ngControl?.control ?? null, form);
960
- }, /* @ts-ignore */
961
- ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
1078
+ errorState = this.errorWiring.errorState;
962
1079
  /** @internal */
963
1080
  controlType = 'select';
964
1081
  /** @internal */
@@ -981,27 +1098,32 @@ class SelectComponent {
981
1098
  this.ngControl.valueAccessor = this;
982
1099
  }
983
1100
  // Mirror parent `open` model into overlay lifecycle.
1101
+ // Only `open` and `isDisabled` are read in the tracked phase; the lifecycle
1102
+ // calls run inside `untracked()` because they both read and write panel state
1103
+ // (openOverlay resets `search` and then reads it back through visibleOptions).
984
1104
  effect(() => {
985
1105
  const shouldOpen = this.open();
986
1106
  const disabled = this.isDisabled();
987
- if (disabled && this.overlayInstance) {
988
- this.closeOverlay();
989
- return;
990
- }
991
- if (shouldOpen && !this.overlayInstance && !disabled && !this.closingSignal()) {
992
- this.openOverlay();
993
- }
994
- else if (!shouldOpen && this.overlayInstance && !this.closingSignal()) {
995
- this.closeOverlay();
996
- }
1107
+ untracked(() => {
1108
+ if (disabled && this.overlayInstance) {
1109
+ this.closeOverlay();
1110
+ return;
1111
+ }
1112
+ if (shouldOpen && !this.overlayInstance && !disabled && !this.closing) {
1113
+ this.openOverlay();
1114
+ }
1115
+ else if (!shouldOpen && this.overlayInstance && !this.closing) {
1116
+ this.closeOverlay();
1117
+ }
1118
+ });
997
1119
  });
998
1120
  // Push state into overlay component whenever anything relevant changes.
999
- // Reads happen in the tracked phase; writes to the overlay instance are
1000
- // wrapped in `untracked()` so they never feed back into this effect.
1121
+ // `isAttached` is the trigger it is written by the lifecycle effect above
1122
+ // and never written here, so the two effects cannot feed each other. All
1123
+ // reads happen in the tracked phase; every write to the overlay instance is
1124
+ // wrapped in `untracked()` so it never feeds back into this effect.
1001
1125
  effect(() => {
1002
- const instance = this.overlayInstance;
1003
- if (!instance)
1004
- return;
1126
+ const attached = this.isAttached();
1005
1127
  const size = this.size();
1006
1128
  const color = this.color();
1007
1129
  const multiple = this.multiple();
@@ -1010,6 +1132,7 @@ class SelectComponent {
1010
1132
  const emptyMessage = this.emptyMessage();
1011
1133
  const search = this.search();
1012
1134
  const activeIndex = this.activeIndex();
1135
+ const activeDescendantId = this.activeDescendantId();
1013
1136
  const renderedRows = this.renderedRows();
1014
1137
  const visibleOptions = this.visibleOptions();
1015
1138
  const optionTemplate = this.optionTemplateChild();
@@ -1020,6 +1143,9 @@ class SelectComponent {
1020
1143
  const checkmarkColorClass = this.checkmarkColorClass();
1021
1144
  const panelClassValue = this.resolvePanelClass();
1022
1145
  untracked(() => {
1146
+ const instance = this.overlayInstance;
1147
+ if (!attached || !instance)
1148
+ return;
1023
1149
  instance.size.set(size);
1024
1150
  instance.color.set(color);
1025
1151
  instance.multiple.set(multiple);
@@ -1028,6 +1154,7 @@ class SelectComponent {
1028
1154
  instance.emptyMessage.set(emptyMessage);
1029
1155
  instance.search.set(search);
1030
1156
  instance.activeIndex.set(activeIndex);
1157
+ instance.activeDescendantId.set(activeDescendantId);
1031
1158
  instance.renderedRows.set(renderedRows);
1032
1159
  instance.visibleOptions.set(visibleOptions);
1033
1160
  instance.optionTemplate.set(optionTemplate);
@@ -1077,20 +1204,20 @@ class SelectComponent {
1077
1204
  if (wasFocused) {
1078
1205
  // Blur often flips `touched` on the bound `NgControl`; bump the
1079
1206
  // revision so `errorState` recomputes.
1080
- this._ngControlRev.update((n) => n + 1);
1207
+ this.errorWiring.bump();
1081
1208
  }
1082
1209
  }
1083
1210
  });
1211
+ // The overlay itself is torn down by `PickerOverlayCoordinator`, which
1212
+ // registers its own `DestroyRef.onDestroy` during this component's field
1213
+ // initialisation — i.e. before this hook — and disposes the `OverlayRef`,
1214
+ // cancels the close timer and completes the per-open streams there.
1084
1215
  this.destroyRef.onDestroy(() => {
1085
1216
  monitorSub.unsubscribe();
1086
1217
  this.focusMonitor.stopMonitoring(this.elementRef);
1087
- this.clearCloseTimer();
1088
- this.perOpenSubs?.unsubscribe();
1089
1218
  this.resizeObserver?.disconnect();
1090
1219
  this.resizeObserver = null;
1091
- this.overlayRef?.dispose();
1092
- this.overlayRef = null;
1093
- this.overlayInstanceSignal.set(null);
1220
+ this.overlayInstance = null;
1094
1221
  });
1095
1222
  }
1096
1223
  // ── Public methods ──
@@ -1167,20 +1294,54 @@ class SelectComponent {
1167
1294
  return;
1168
1295
  this.toggle();
1169
1296
  }
1170
- /** @internal */
1297
+ /**
1298
+ * @internal
1299
+ *
1300
+ * `stopPropagation` + `preventDefault` are load-bearing even now that the
1301
+ * control is a sibling of the trigger rather than nested inside it: inside a
1302
+ * `tw-form-field` the click still bubbles to the field's control wrapper,
1303
+ * which calls `onContainerClick()` and would open the panel on a clear.
1304
+ */
1171
1305
  onClearClick(event) {
1172
1306
  event.stopPropagation();
1173
1307
  event.preventDefault();
1174
1308
  this.clear();
1309
+ this.focusTrigger();
1175
1310
  }
1176
- /** @internal */
1311
+ /**
1312
+ * @internal
1313
+ *
1314
+ * A native `button` already activates on Enter and Space in a browser, so
1315
+ * this looks redundant — it is not. `preventDefault()` here suppresses that
1316
+ * native activation, so the two paths cannot both fire, and jsdom never
1317
+ * synthesises a click from a key event at all, which makes this the only
1318
+ * keyboard path a unit spec can exercise.
1319
+ */
1177
1320
  onClearKeydown(event) {
1178
1321
  if (event.key === 'Enter' || event.key === ' ') {
1179
1322
  event.preventDefault();
1180
1323
  event.stopPropagation();
1181
1324
  this.clear();
1325
+ this.focusTrigger();
1182
1326
  }
1183
1327
  }
1328
+ /**
1329
+ * Clearing removes the clear control from the DOM (`showClearButton()` goes
1330
+ * false), so whatever had focus is destroyed under the user. Hand focus back
1331
+ * to the trigger rather than letting it fall to `<body>` (SC 2.4.3).
1332
+ *
1333
+ * The disabled guard is not redundant. `[disabled]` on a native button makes
1334
+ * `.focus()` a silent no-op (the same trap `calendar-cell` hit), and today
1335
+ * the only reason the path cannot be reached is that `showClearButton()`
1336
+ * happens to include `!isDisabled()`. That is ordering, not a guarantee: a
1337
+ * `setDisabledState(true)` landing between the keydown and this call would
1338
+ * drop focus to `<body>` with nothing to show for it.
1339
+ */
1340
+ focusTrigger() {
1341
+ if (this.isDisabled())
1342
+ return;
1343
+ this.triggerButtonRef()?.nativeElement.focus();
1344
+ }
1184
1345
  /** @internal */
1185
1346
  onTriggerKeydown(event) {
1186
1347
  if (this.isDisabled())
@@ -1311,7 +1472,12 @@ class SelectComponent {
1311
1472
  this.typeAheadBuffer += char.toLowerCase();
1312
1473
  const labelFn = this.optionLabel();
1313
1474
  const visible = this.visibleOptions();
1314
- const match = visible.findIndex((o) => labelFn(o.option).toLowerCase().startsWith(this.typeAheadBuffer));
1475
+ // Disabled options are skipped, matching every other navigation path in this
1476
+ // file (`findEnabledFrom`). Without the filter, `aria-activedescendant` could
1477
+ // land on an option that `selectByVisibleIndex` then refuses to commit, so
1478
+ // Enter did nothing with no feedback.
1479
+ const match = visible.findIndex((o) => !o.disabled &&
1480
+ labelFn(o.option).toLowerCase().startsWith(this.typeAheadBuffer));
1315
1481
  if (match >= 0) {
1316
1482
  if (!this.open())
1317
1483
  this.openPanel();
@@ -1446,12 +1612,47 @@ class SelectComponent {
1446
1612
  }
1447
1613
  }
1448
1614
  // ── Overlay lifecycle ──
1615
+ /**
1616
+ * Builds and attaches the panel through {@link PickerOverlayCoordinator}.
1617
+ *
1618
+ * The coordinator disposes the `OverlayRef` on close, so every value read
1619
+ * here — `offset`, `scrollStrategy`, `panelClass`, and the position
1620
+ * strategy's origin — is re-read on each open rather than frozen at the
1621
+ * first one. That replaces the previous `refreshPositionConfig()` /
1622
+ * `applyScrollStrategy()` pair, which could only refresh the two inputs it
1623
+ * knew about on a reused ref.
1624
+ *
1625
+ * `focusTrap: false` is load-bearing: this is an `aria-activedescendant`
1626
+ * listbox, so DOM focus stays on the trigger (or the panel's search input)
1627
+ * and a trap's anchor elements would inject tab stops around the panel.
1628
+ */
1449
1629
  openOverlay() {
1450
- this.ensureOverlay();
1451
- this.attachOverlayComponent();
1630
+ const result = this.coordinator.open({
1631
+ origin: this.elementRef,
1632
+ portalComponent: SelectOverlayComponent,
1633
+ viewContainerRef: this.viewContainerRef,
1634
+ injector: this.injector,
1635
+ positions: buildSelectLikePositions(this.offset()),
1636
+ scrollStrategy: resolveSelectScrollStrategy(this.scrollStrategy(), this.overlayService),
1637
+ panelClass: 'tw-select-panel',
1638
+ focusTrap: false,
1639
+ });
1640
+ if (!result)
1641
+ return;
1642
+ const instance = result.instance;
1643
+ instance.onSearchInput.set((v) => this.search.set(v));
1644
+ instance.onOptionSelect.set((i) => this.selectByVisibleIndex(i, 'user'));
1645
+ instance.onOptionActivate.set((i) => this.activeIndex.set(i));
1646
+ instance.onPanelKeydown.set((e) => this.handleKeydown(e));
1647
+ this.overlayInstance = instance;
1648
+ this.installResizeObserver();
1649
+ this.updateOverlaySize();
1452
1650
  this.subscribePerOpen();
1453
1651
  this.search.set('');
1454
1652
  this.initActiveIndexOnOpen();
1653
+ // Set last: this is what wakes the state-push effect, and it must not fire
1654
+ // until the instance exists and the per-open state has been reset.
1655
+ this.isAttached.set(true);
1455
1656
  queueMicrotask(() => {
1456
1657
  if (this.searchable()) {
1457
1658
  this.overlayInstance?.focusSearchInput();
@@ -1460,47 +1661,27 @@ class SelectComponent {
1460
1661
  this.openedChange.emit({ open: true, trigger: this.elementRef.nativeElement });
1461
1662
  }
1462
1663
  closeOverlay() {
1463
- if (this.closingSignal() || !this.overlayInstance)
1664
+ if (this.closing || !this.overlayInstance)
1464
1665
  return;
1465
- this.closingSignal.set(true);
1666
+ this.closing = true;
1466
1667
  this.overlayInstance.leaving.set(true);
1467
- this.elementRef.nativeElement.querySelector('button')?.focus();
1468
- this.closeTimer = setTimeout(() => {
1469
- this.closeTimer = null;
1470
- if (this.overlayRef?.hasAttached()) {
1471
- this.overlayRef.detach();
1472
- }
1473
- this.perOpenSubs?.unsubscribe();
1474
- this.perOpenSubs = null;
1475
- this.overlayInstanceSignal.set(null);
1668
+ this.triggerButtonRef()?.nativeElement.focus();
1669
+ // The coordinator runs the leave animation, then detaches and disposes;
1670
+ // this callback fires once the overlay is fully gone.
1671
+ this.coordinator.close(() => {
1672
+ // Safe to drop the observer: the next open mints a fresh one against the
1673
+ // fresh OverlayRef. Keeping it alive would leave it feeding a disposed
1674
+ // ref, which is the mirror image of the bug the old reuse path had.
1675
+ this.resizeObserver?.disconnect();
1676
+ this.resizeObserver = null;
1677
+ this.overlayInstance = null;
1678
+ this.isAttached.set(false);
1476
1679
  this.search.set('');
1477
1680
  this.activeIndex.set(-1);
1478
1681
  untracked(() => this.open.set(false));
1479
- this.closingSignal.set(false);
1682
+ this.closing = false;
1480
1683
  this.openedChange.emit({ open: false, trigger: this.elementRef.nativeElement });
1481
- }, ANIMATION_DURATION);
1482
- }
1483
- ensureOverlay() {
1484
- if (this.overlayRef) {
1485
- this.updateOverlaySize();
1486
- return;
1487
- }
1488
- const positionStrategy = this.overlayService
1489
- .position()
1490
- .flexibleConnectedTo(this.elementRef)
1491
- .withPositions(buildSelectLikePositions(this.offset()))
1492
- .withFlexibleDimensions(false)
1493
- .withPush(false)
1494
- .withViewportMargin(8);
1495
- this.overlayRef = this.overlayService.create({
1496
- positionStrategy,
1497
- scrollStrategy: resolveSelectScrollStrategy(this.scrollStrategy(), this.overlayService),
1498
- hasBackdrop: true,
1499
- backdropClass: 'cdk-overlay-transparent-backdrop',
1500
- panelClass: 'tw-select-panel',
1501
1684
  });
1502
- this.installResizeObserver();
1503
- this.updateOverlaySize();
1504
1685
  }
1505
1686
  installResizeObserver() {
1506
1687
  if (!this.platform.isBrowser || typeof ResizeObserver === 'undefined')
@@ -1510,61 +1691,43 @@ class SelectComponent {
1510
1691
  this.resizeObserver.observe(this.elementRef.nativeElement);
1511
1692
  }
1512
1693
  updateOverlaySize() {
1513
- if (!this.overlayRef)
1694
+ const overlayRef = this.coordinator.ref();
1695
+ if (!overlayRef)
1514
1696
  return;
1515
1697
  const width = this.panelWidth();
1516
1698
  if (width === 'trigger') {
1517
- const button = this.elementRef.nativeElement.querySelector('button');
1699
+ // The view query, NOT `querySelector('button')`: the clear control is now
1700
+ // a second button inside the host, and measuring the wrong one would
1701
+ // silently corrupt `panelWidth="trigger"`.
1702
+ const button = this.triggerButtonRef()?.nativeElement ?? null;
1518
1703
  const rect = (button ?? this.elementRef.nativeElement).getBoundingClientRect();
1519
- this.overlayRef.updateSize({ width: rect.width });
1704
+ overlayRef.updateSize({ width: rect.width });
1520
1705
  }
1521
1706
  else if (width === 'auto') {
1522
- this.overlayRef.updateSize({ width: undefined, minWidth: undefined });
1523
- }
1524
- else if (typeof width === 'number') {
1525
- this.overlayRef.updateSize({ width });
1707
+ overlayRef.updateSize({ width: undefined, minWidth: undefined });
1526
1708
  }
1527
1709
  else {
1528
- this.overlayRef.updateSize({ width });
1710
+ overlayRef.updateSize({ width });
1529
1711
  }
1530
1712
  }
1531
- attachOverlayComponent() {
1532
- if (!this.overlayRef)
1533
- return;
1534
- const portal = new ComponentPortal(SelectOverlayComponent, this.viewContainerRef, this.injector);
1535
- const ref = this.overlayRef.attach(portal);
1536
- const instance = ref.instance;
1537
- instance.onSearchInput.set((v) => this.search.set(v));
1538
- instance.onOptionSelect.set((i) => this.selectByVisibleIndex(i, 'user'));
1539
- instance.onOptionActivate.set((i) => this.activeIndex.set(i));
1540
- instance.onPanelKeydown.set((e) => this.handleKeydown(e));
1541
- this.overlayInstanceSignal.set(instance);
1542
- }
1713
+ /**
1714
+ * Registers the backdrop-click and Escape listeners for one open.
1715
+ *
1716
+ * Both streams come from the coordinator, which completes them when the
1717
+ * overlay closes — so there is no per-open `Subscription` aggregate to keep
1718
+ * and no way for listeners to accumulate across opens.
1719
+ */
1543
1720
  subscribePerOpen() {
1544
- this.perOpenSubs?.unsubscribe();
1545
- this.perOpenSubs = new Subscription();
1546
- if (!this.overlayRef)
1547
- return;
1548
- this.perOpenSubs.add(this.overlayRef
1549
- .backdropClick()
1550
- .pipe(takeUntilDestroyed(this.destroyRef))
1551
- .subscribe(() => this.closePanel()));
1552
- const teardownEscape = consumeOverlayEscape(this.overlayRef, (event) => {
1721
+ this.coordinator.backdropClick$().subscribe(() => this.closePanel());
1722
+ this.coordinator.escape$().subscribe((event) => {
1553
1723
  event.preventDefault();
1554
1724
  this.closePanel();
1555
1725
  });
1556
- this.perOpenSubs.add(() => teardownEscape());
1557
1726
  }
1558
1727
  resolvePanelClass() {
1559
1728
  const raw = this.panelClass();
1560
1729
  return Array.isArray(raw) ? raw.join(' ') : raw;
1561
1730
  }
1562
- clearCloseTimer() {
1563
- if (this.closeTimer !== null) {
1564
- clearTimeout(this.closeTimer);
1565
- this.closeTimer = null;
1566
- }
1567
- }
1568
1731
  /** @internal */
1569
1732
  buildTriggerContext() {
1570
1733
  return {
@@ -1624,8 +1787,7 @@ class SelectComponent {
1624
1787
  onContainerClick(event) {
1625
1788
  if (this.isDisabled())
1626
1789
  return;
1627
- const btn = this.elementRef.nativeElement.querySelector('button');
1628
- btn?.focus();
1790
+ this.triggerButtonRef()?.nativeElement.focus();
1629
1791
  if (!event.defaultPrevented) {
1630
1792
  this.openPanel();
1631
1793
  }
@@ -1634,21 +1796,7 @@ class SelectComponent {
1634
1796
  // NgControl's `control` is set by the parent FormControl* directive before
1635
1797
  // children's `ngOnInit`. Subscribe here so errorState reacts to status/value
1636
1798
  // changes on the bound control.
1637
- const ctrl = this.ngControl?.control;
1638
- if (ctrl) {
1639
- const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
1640
- if (streams.length) {
1641
- merge(...streams)
1642
- .pipe(takeUntilDestroyed(this.destroyRef))
1643
- .subscribe(() => this._ngControlRev.update((n) => n + 1));
1644
- }
1645
- }
1646
- const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
1647
- if (submit) {
1648
- submit
1649
- .pipe(takeUntilDestroyed(this.destroyRef))
1650
- .subscribe(() => this._formSubmitRev.update((n) => n + 1));
1651
- }
1799
+ this.errorWiring.connect();
1652
1800
  }
1653
1801
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: SelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1654
1802
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: SelectComponent, isStandalone: true, selector: "tw-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 }, optionGroup: { classPropertyName: "optionGroup", publicName: "optionGroup", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, filterPredicate: { classPropertyName: "filterPredicate", publicName: "filterPredicate", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requiredInput: { classPropertyName: "requiredInput", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, panelMaxHeight: { classPropertyName: "panelMaxHeight", publicName: "panelMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, closeOnSelect: { classPropertyName: "closeOnSelect", publicName: "closeOnSelect", isSignal: true, isRequired: false, transformFunction: null }, scrollStrategy: { classPropertyName: "scrollStrategy", publicName: "scrollStrategy", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", open: "openChange", openedChange: "openedChange", selectionChange: "selectionChange", searchChange: "searchChange" }, host: { properties: { "class": "rootClasses()" } }, providers: [
@@ -1656,7 +1804,8 @@ class SelectComponent {
1656
1804
  provide: TW_FORM_FIELD_CONTROL,
1657
1805
  useExisting: forwardRef(() => SelectComponent),
1658
1806
  },
1659
- ], queries: [{ propertyName: "triggerTemplateChild", first: true, predicate: SelectTriggerTemplateDirective, descendants: true, isSignal: true }, { propertyName: "optionTemplateChild", first: true, predicate: SelectOptionTemplateDirective, descendants: true, isSignal: true }, { propertyName: "emptyTemplateChild", first: true, predicate: SelectEmptyTemplateDirective, descendants: true, isSignal: true }, { propertyName: "headerTemplateChild", first: true, predicate: SelectHeaderTemplateDirective, descendants: true, isSignal: true }, { propertyName: "footerTemplateChild", first: true, predicate: SelectFooterTemplateDirective, descendants: true, isSignal: true }], ngImport: i0, template: `
1807
+ PickerOverlayCoordinator,
1808
+ ], queries: [{ propertyName: "triggerTemplateChild", first: true, predicate: SelectTriggerTemplateDirective, descendants: true, isSignal: true }, { propertyName: "optionTemplateChild", first: true, predicate: SelectOptionTemplateDirective, descendants: true, isSignal: true }, { propertyName: "emptyTemplateChild", first: true, predicate: SelectEmptyTemplateDirective, descendants: true, isSignal: true }, { propertyName: "headerTemplateChild", first: true, predicate: SelectHeaderTemplateDirective, descendants: true, isSignal: true }, { propertyName: "footerTemplateChild", first: true, predicate: SelectFooterTemplateDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "triggerButtonRef", first: true, predicate: ["triggerButton"], descendants: true, isSignal: true }], ngImport: i0, template: `
1660
1809
  <button
1661
1810
  #triggerButton
1662
1811
  type="button"
@@ -1666,12 +1815,12 @@ class SelectComponent {
1666
1815
  [attr.aria-haspopup]="'listbox'"
1667
1816
  [attr.aria-expanded]="open() ? 'true' : 'false'"
1668
1817
  [attr.aria-controls]="listboxId"
1669
- [attr.aria-activedescendant]="activeDescendantId() || null"
1818
+ [attr.aria-activedescendant]="triggerActiveDescendantId()"
1670
1819
  [attr.aria-autocomplete]="searchable() ? 'list' : 'none'"
1671
1820
  [attr.aria-label]="ariaLabel() || null"
1672
1821
  [attr.aria-labelledby]="labelledBy() || null"
1673
1822
  [attr.aria-describedby]="describedBy() || null"
1674
- [attr.aria-required]="requiredInput() || null"
1823
+ [attr.aria-required]="required() || null"
1675
1824
  [attr.aria-invalid]="errorState() || null"
1676
1825
  [attr.aria-disabled]="isDisabled() || null"
1677
1826
  [disabled]="isDisabled()"
@@ -1692,22 +1841,11 @@ class SelectComponent {
1692
1841
  }
1693
1842
 
1694
1843
  @if (showClearButton()) {
1695
- <span
1696
- role="button"
1697
- tabindex="-1"
1698
- [class]="clearButtonClasses()"
1699
- [attr.aria-label]="'Clear selection'"
1700
- (click)="onClearClick($event)"
1701
- (keydown)="onClearKeydown($event)"
1702
- >
1703
- <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-3">
1704
- <path
1705
- fill-rule="evenodd"
1706
- d="M10 8.586 4.707 3.293a1 1 0 0 0-1.414 1.414L8.586 10l-5.293 5.293a1 1 0 1 0 1.414 1.414L10 11.414l5.293 5.293a1 1 0 0 0 1.414-1.414L11.414 10l5.293-5.293a1 1 0 0 0-1.414-1.414L10 8.586Z"
1707
- clip-rule="evenodd"
1708
- />
1709
- </svg>
1710
- </span>
1844
+ <!--
1845
+ Holds the clear control's slot in the flex row. The control itself is
1846
+ rendered after this button, not inside it - see clearSpacer above.
1847
+ -->
1848
+ <span [class]="clearSpacerClasses()" aria-hidden="true"></span>
1711
1849
  }
1712
1850
 
1713
1851
  <svg
@@ -1724,6 +1862,30 @@ class SelectComponent {
1724
1862
  </svg>
1725
1863
  </button>
1726
1864
 
1865
+ @if (showClearButton()) {
1866
+ <!--
1867
+ A real, native button and a SIBLING of the trigger: HTML's content model
1868
+ for button is "no interactive content descendant", so the previous
1869
+ role="button" tabindex="0" span nested inside the trigger was invalid.
1870
+ It sits after the trigger in DOM order, so the tab order is unchanged.
1871
+ -->
1872
+ <button
1873
+ type="button"
1874
+ [class]="clearButtonClasses()"
1875
+ aria-label="Clear selection"
1876
+ (click)="onClearClick($event)"
1877
+ (keydown)="onClearKeydown($event)"
1878
+ >
1879
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-3">
1880
+ <path
1881
+ fill-rule="evenodd"
1882
+ d="M10 8.586 4.707 3.293a1 1 0 0 0-1.414 1.414L8.586 10l-5.293 5.293a1 1 0 1 0 1.414 1.414L10 11.414l5.293 5.293a1 1 0 0 0 1.414-1.414L11.414 10l5.293-5.293a1 1 0 0 0-1.414-1.414L10 8.586Z"
1883
+ clip-rule="evenodd"
1884
+ />
1885
+ </svg>
1886
+ </button>
1887
+ }
1888
+
1727
1889
  <span hidden>
1728
1890
  <ng-content />
1729
1891
  </span>
@@ -1740,6 +1902,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
1740
1902
  provide: TW_FORM_FIELD_CONTROL,
1741
1903
  useExisting: forwardRef(() => SelectComponent),
1742
1904
  },
1905
+ PickerOverlayCoordinator,
1743
1906
  ],
1744
1907
  template: `
1745
1908
  <button
@@ -1751,12 +1914,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
1751
1914
  [attr.aria-haspopup]="'listbox'"
1752
1915
  [attr.aria-expanded]="open() ? 'true' : 'false'"
1753
1916
  [attr.aria-controls]="listboxId"
1754
- [attr.aria-activedescendant]="activeDescendantId() || null"
1917
+ [attr.aria-activedescendant]="triggerActiveDescendantId()"
1755
1918
  [attr.aria-autocomplete]="searchable() ? 'list' : 'none'"
1756
1919
  [attr.aria-label]="ariaLabel() || null"
1757
1920
  [attr.aria-labelledby]="labelledBy() || null"
1758
1921
  [attr.aria-describedby]="describedBy() || null"
1759
- [attr.aria-required]="requiredInput() || null"
1922
+ [attr.aria-required]="required() || null"
1760
1923
  [attr.aria-invalid]="errorState() || null"
1761
1924
  [attr.aria-disabled]="isDisabled() || null"
1762
1925
  [disabled]="isDisabled()"
@@ -1777,22 +1940,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
1777
1940
  }
1778
1941
 
1779
1942
  @if (showClearButton()) {
1780
- <span
1781
- role="button"
1782
- tabindex="-1"
1783
- [class]="clearButtonClasses()"
1784
- [attr.aria-label]="'Clear selection'"
1785
- (click)="onClearClick($event)"
1786
- (keydown)="onClearKeydown($event)"
1787
- >
1788
- <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-3">
1789
- <path
1790
- fill-rule="evenodd"
1791
- d="M10 8.586 4.707 3.293a1 1 0 0 0-1.414 1.414L8.586 10l-5.293 5.293a1 1 0 1 0 1.414 1.414L10 11.414l5.293 5.293a1 1 0 0 0 1.414-1.414L11.414 10l5.293-5.293a1 1 0 0 0-1.414-1.414L10 8.586Z"
1792
- clip-rule="evenodd"
1793
- />
1794
- </svg>
1795
- </span>
1943
+ <!--
1944
+ Holds the clear control's slot in the flex row. The control itself is
1945
+ rendered after this button, not inside it - see clearSpacer above.
1946
+ -->
1947
+ <span [class]="clearSpacerClasses()" aria-hidden="true"></span>
1796
1948
  }
1797
1949
 
1798
1950
  <svg
@@ -1809,6 +1961,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
1809
1961
  </svg>
1810
1962
  </button>
1811
1963
 
1964
+ @if (showClearButton()) {
1965
+ <!--
1966
+ A real, native button and a SIBLING of the trigger: HTML's content model
1967
+ for button is "no interactive content descendant", so the previous
1968
+ role="button" tabindex="0" span nested inside the trigger was invalid.
1969
+ It sits after the trigger in DOM order, so the tab order is unchanged.
1970
+ -->
1971
+ <button
1972
+ type="button"
1973
+ [class]="clearButtonClasses()"
1974
+ aria-label="Clear selection"
1975
+ (click)="onClearClick($event)"
1976
+ (keydown)="onClearKeydown($event)"
1977
+ >
1978
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-3">
1979
+ <path
1980
+ fill-rule="evenodd"
1981
+ d="M10 8.586 4.707 3.293a1 1 0 0 0-1.414 1.414L8.586 10l-5.293 5.293a1 1 0 1 0 1.414 1.414L10 11.414l5.293 5.293a1 1 0 0 0 1.414-1.414L11.414 10l5.293-5.293a1 1 0 0 0-1.414-1.414L10 8.586Z"
1982
+ clip-rule="evenodd"
1983
+ />
1984
+ </svg>
1985
+ </button>
1986
+ }
1987
+
1812
1988
  <span hidden>
1813
1989
  <ng-content />
1814
1990
  </span>
@@ -1817,7 +1993,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
1817
1993
  '[class]': 'rootClasses()',
1818
1994
  },
1819
1995
  }]
1820
- }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], optionGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroup", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], filterPredicate: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPredicate", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], panelMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelMaxHeight", required: false }] }], closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnSelect", required: false }] }], scrollStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollStrategy", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], openedChange: [{ type: i0.Output, args: ["openedChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], triggerTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectTriggerTemplateDirective), { isSignal: true }] }], optionTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectOptionTemplateDirective), { isSignal: true }] }], emptyTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectEmptyTemplateDirective), { isSignal: true }] }], headerTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectHeaderTemplateDirective), { isSignal: true }] }], footerTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectFooterTemplateDirective), { isSignal: true }] }] } });
1996
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], optionGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroup", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], filterPredicate: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPredicate", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], panelMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelMaxHeight", required: false }] }], closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnSelect", required: false }] }], scrollStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollStrategy", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], openedChange: [{ type: i0.Output, args: ["openedChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], triggerTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectTriggerTemplateDirective), { isSignal: true }] }], optionTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectOptionTemplateDirective), { isSignal: true }] }], emptyTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectEmptyTemplateDirective), { isSignal: true }] }], headerTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectHeaderTemplateDirective), { isSignal: true }] }], footerTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectFooterTemplateDirective), { isSignal: true }] }], triggerButtonRef: [{ type: i0.ViewChild, args: ['triggerButton', { isSignal: true }] }] } });
1821
1997
 
1822
1998
  /**
1823
1999
  * Generated bundle index. Do not edit.