@cdevhub/ngx-tw 0.5.0 → 0.6.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 (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 +18 -3
  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 +110 -36
  140. package/theme/_high-contrast-dark.css +367 -0
  141. package/theme/_high-contrast.css +13 -2
  142. package/theme/_light.css +276 -0
  143. package/theme/_semantic.css +47 -12
  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,14 +1,13 @@
1
1
  import { NgTemplateOutlet } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
3
  import { inject, TemplateRef, Directive, input, output, contentChild, ElementRef, Injector, DestroyRef, signal, computed, linkedSignal, afterNextRender, isDevMode, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
4
- import { NgControl, NgForm, FormGroupDirective, Validators } from '@angular/forms';
4
+ import { NgControl } from '@angular/forms';
5
5
  import { CdkListbox, CdkOption } from '@angular/cdk/listbox';
6
6
  import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
7
7
  import { Platform } from '@angular/cdk/platform';
8
8
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
- import { merge } from 'rxjs';
10
9
  import { tv } from 'tailwind-variants';
11
- import { TW_ERROR_STATE_MATCHER } from '@cdevhub/ngx-tw/core';
10
+ import { wireErrorState } from '@cdevhub/ngx-tw/core';
12
11
  import { FormFieldControl, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
13
12
  import { CheckboxComponent } from '@cdevhub/ngx-tw/checkbox';
14
13
  import { InputDirective } from '@cdevhub/ngx-tw/input';
@@ -65,6 +64,12 @@ const DISPLAY_DEFAULTS = {
65
64
  };
66
65
  /** Replaces `{name}` tokens in a template with values from `vars`. */
67
66
  function formatLabel(template, vars) {
67
+ // Defence in depth. `resolvedLabels()` already filters explicitly-undefined
68
+ // consumer keys, so a non-string cannot reach here through the public
69
+ // `labels` input today. The guard exists so a future regression in that
70
+ // merge degrades to a missing label instead of throwing.
71
+ if (typeof template !== 'string')
72
+ return '';
68
73
  return template.replace(/\{(\w+)\}/g, (match, key) => key in vars ? String(vars[key]) : match);
69
74
  }
70
75
  // ── tv() config ───────────────────────────────────────────────────────
@@ -97,6 +102,13 @@ const transferVariants = tv({
97
102
  // size; not the [twButton] directive (its size padding would fight a square
98
103
  // override across two class bindings).
99
104
  moveButton: 'inline-flex items-center justify-center shrink-0 rounded-md border border-border bg-surface text-fg-muted cursor-pointer transition-colors duration-200 motion-reduce:transition-none hover:bg-surface-muted hover:text-fg hover:border-border-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed',
105
+ // Glyph inside the square move button. Glyph sub-scale saturating at
106
+ // `lg`: 3 / 4 / 5 / 6 / 6, matching `optionCheck`. Previously 4/4/5/5/5,
107
+ // which left three of the five advertised steps dead. `xl` stops at
108
+ // 24px rather than the scale's 32px because the glyph lives inside a
109
+ // 36px `moveButton` (the square-interactive scale saturates at `lg`
110
+ // too, per CLAUDE.md) — a 32px glyph in a 36px target leaves no optical
111
+ // padding. Raising one means raising the other.
100
112
  moveIcon: 'shrink-0',
101
113
  empty: 'flex items-center justify-center p-4 text-sm text-fg-subtle text-center',
102
114
  },
@@ -106,9 +118,9 @@ const transferVariants = tv({
106
118
  panelHeader: 'px-2 py-1',
107
119
  search: 'px-2 py-1',
108
120
  option: 'px-2 py-1 text-xs gap-1.5',
109
- optionCheck: 'size-4',
121
+ optionCheck: 'size-3',
110
122
  moveButton: 'size-6',
111
- moveIcon: 'size-4',
123
+ moveIcon: 'size-3',
112
124
  empty: 'text-xs',
113
125
  },
114
126
  sm: {
@@ -131,17 +143,17 @@ const transferVariants = tv({
131
143
  panelHeader: 'px-5 py-2.5',
132
144
  search: 'px-4 py-2.5',
133
145
  option: 'px-4 py-2.5 text-base',
134
- optionCheck: 'size-5',
146
+ optionCheck: 'size-6',
135
147
  moveButton: 'size-9',
136
- moveIcon: 'size-5',
148
+ moveIcon: 'size-6',
137
149
  },
138
150
  xl: {
139
151
  panelHeader: 'px-6 py-3',
140
152
  search: 'px-4 py-3',
141
153
  option: 'px-4 py-3 text-base',
142
- optionCheck: 'size-5',
154
+ optionCheck: 'size-6',
143
155
  moveButton: 'size-9',
144
- moveIcon: 'size-5',
156
+ moveIcon: 'size-6',
145
157
  },
146
158
  },
147
159
  checked: {
@@ -233,13 +245,13 @@ class TransferComponent extends FormFieldControl {
233
245
  /** Reserved for control identification (e.g. analytics, future native-submission parity). The transfer has no single native value element, so it is not currently reflected to the DOM. Defaults to `undefined`. */
234
246
  name = input(undefined, /* @ts-ignore */
235
247
  ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
236
- /** Id on the host element. Auto-generated as `tw-transfer-N` when unset. Used by the form-field's `<label for>` association. */
248
+ /** Id on the host element. Auto-generated as `tw-transfer-N` when unset. Used by the form-field's `<label for>` association. Alias: `id`. */
237
249
  idInput = input(undefined, { ...(ngDevMode ? { debugName: "idInput" } : /* istanbul ignore next */ {}), alias: 'id' });
238
250
  /** Accessible name applied to the control when no visible label is wired. Mirrored to `aria-label`. Defaults to `undefined`. */
239
251
  ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
240
252
  /** ID of an external element that labels the control. Mirrored to `aria-labelledby`. Defaults to `undefined`. */
241
253
  ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
242
- /** ID of an external element that describes the control. The form-field merges its hint / error ids alongside. Defaults to `undefined`. */
254
+ /** ID of an external element that describes the control. The form-field merges its hint / error ids alongside. Defaults to `undefined`. Alias: `aria-describedby`. */
243
255
  ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
244
256
  /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, uses the `TW_ERROR_STATE_MATCHER` token's value. Defaults to `undefined`. */
245
257
  errorStateMatcher = input(undefined, /* @ts-ignore */
@@ -260,9 +272,6 @@ class TransferComponent extends FormFieldControl {
260
272
  focusMonitor = inject(FocusMonitor);
261
273
  liveAnnouncer = inject(LiveAnnouncer);
262
274
  ngControl = inject(NgControl, { optional: true, self: true });
263
- parentForm = inject(NgForm, { optional: true });
264
- parentFormGroup = inject(FormGroupDirective, { optional: true });
265
- defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
266
275
  platform = inject(Platform);
267
276
  // ── Identity ──
268
277
  uid = nextId++;
@@ -283,10 +292,13 @@ class TransferComponent extends FormFieldControl {
283
292
  ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
284
293
  _focused = signal(false, /* @ts-ignore */
285
294
  ...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
286
- _ngControlRev = signal(0, /* @ts-ignore */
287
- ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
288
- _formSubmitRev = signal(0, /* @ts-ignore */
289
- ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
295
+ /** @internal Shared `errorState` / `required` / `errors` derivation — see `wireErrorState`. */
296
+ errorWiring = wireErrorState({
297
+ ngControl: () => this.ngControl,
298
+ matcher: () => this.errorStateMatcher(),
299
+ required: () => this.requiredInput(),
300
+ track: [() => this._focused()],
301
+ });
290
302
  describedByIdsSignal = signal([], /* @ts-ignore */
291
303
  ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
292
304
  labelledByIdsSignal = signal([], /* @ts-ignore */
@@ -296,16 +308,32 @@ class TransferComponent extends FormFieldControl {
296
308
  /** @internal Stable comparator for `[cdkListboxCompareWith]`. Keys are identity-comparable. */
297
309
  compareKeys = (a, b) => Object.is(a, b);
298
310
  // ── Resolved config ──
299
- /** @internal */
300
- resolvedLabels = computed(() => ({
301
- ...LABELS_DEFAULTS,
302
- ...this.labels(),
303
- }), /* @ts-ignore */
311
+ /**
312
+ * @internal Resolved label record.
313
+ *
314
+ * Explicitly-undefined keys are dropped before merging. Root `tsconfig.json`
315
+ * does not set `exactOptionalPropertyTypes`, so
316
+ * `[labels]="{ moveAnnouncement: i18n.moved }"` type-checks even when the
317
+ * i18n bundle has no such key — and a plain spread would then overwrite the
318
+ * default with `undefined`, which reaches `formatLabel()` and throws
319
+ * `Cannot read properties of undefined (reading 'replace')`. Same filter
320
+ * `table.ts` and `timeline.ts` already use; the `Required<>` annotation only
321
+ * tells the truth once the filter is in place.
322
+ */
323
+ resolvedLabels = computed(() => {
324
+ const overrides = Object.fromEntries(Object.entries(this.labels() ?? {}).filter(([, value]) => value !== undefined));
325
+ return { ...LABELS_DEFAULTS, ...overrides };
326
+ }, /* @ts-ignore */
304
327
  ...(ngDevMode ? [{ debugName: "resolvedLabels" }] : /* istanbul ignore next */ []));
305
- resolvedDisplay = computed(() => ({
306
- ...DISPLAY_DEFAULTS,
307
- ...this.display(),
308
- }), /* @ts-ignore */
328
+ /**
329
+ * @internal Resolved display config. Same explicitly-undefined filter as
330
+ * `resolvedLabels` — a plain spread let `[display]="{ listHeight: undefined }"`
331
+ * reach the list's inline `height` as `undefined`, collapsing the panes.
332
+ */
333
+ resolvedDisplay = computed(() => {
334
+ const overrides = Object.fromEntries(Object.entries(this.display() ?? {}).filter(([, value]) => value !== undefined));
335
+ return { ...DISPLAY_DEFAULTS, ...overrides };
336
+ }, /* @ts-ignore */
309
337
  ...(ngDevMode ? [{ debugName: "resolvedDisplay" }] : /* istanbul ignore next */ []));
310
338
  // Behaviour is resolved inline (not via a `Required<>` const) because `filterFn`
311
339
  // is legitimately optional and both predicates are generic over `T`.
@@ -701,30 +729,11 @@ class TransferComponent extends FormFieldControl {
701
729
  disabled = computed(() => this.disabledInput() || this.cvaDisabled(), /* @ts-ignore */
702
730
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
703
731
  /** @internal */
704
- required = computed(() => {
705
- this._ngControlRev();
706
- if (this.requiredInput())
707
- return true;
708
- return !!this.ngControl?.control?.hasValidator(Validators.required);
709
- }, /* @ts-ignore */
710
- ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
732
+ required = this.errorWiring.required;
711
733
  /** @internal */
712
- errorState = computed(() => {
713
- this._ngControlRev();
714
- this._formSubmitRev();
715
- this._focused();
716
- const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
717
- const form = this.parentFormGroup ??
718
- this.parentForm;
719
- return matcher.isErrorState(this.ngControl?.control ?? null, form);
720
- }, /* @ts-ignore */
721
- ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
734
+ errorState = this.errorWiring.errorState;
722
735
  /** @internal Active validation errors from the bound control, for `[twError match="…"]` filtering. */
723
- errors = computed(() => {
724
- this._ngControlRev();
725
- return this.ngControl?.control?.errors ?? null;
726
- }, /* @ts-ignore */
727
- ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
736
+ errors = this.errorWiring.errors;
728
737
  /** @internal */
729
738
  controlType = 'transfer';
730
739
  /** @internal */
@@ -804,29 +813,15 @@ class TransferComponent extends FormFieldControl {
804
813
  this._focused.set(!!origin);
805
814
  if (wasFocused && !origin) {
806
815
  this.onTouched();
807
- this._ngControlRev.update((n) => n + 1);
816
+ this.errorWiring.bump();
808
817
  }
809
818
  });
810
819
  this.destroyRef.onDestroy(() => this.focusMonitor.stopMonitoring(this.elementRef));
811
820
  // React to status/value changes on the bound control so errorState recomputes.
812
- const ctrl = this.ngControl?.control;
813
- if (ctrl) {
814
- const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
815
- if (streams.length) {
816
- merge(...streams)
817
- .pipe(takeUntilDestroyed(this.destroyRef))
818
- .subscribe(() => this._ngControlRev.update((n) => n + 1));
819
- }
820
- }
821
- const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
822
- if (submit) {
823
- submit
824
- .pipe(takeUntilDestroyed(this.destroyRef))
825
- .subscribe(() => this._formSubmitRev.update((n) => n + 1));
826
- }
821
+ this.errorWiring.connect();
827
822
  }
828
823
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TransferComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
829
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: TransferComponent, isStandalone: true, selector: "tw-transfer", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, keyFn: { classPropertyName: "keyFn", publicName: "keyFn", isSignal: true, isRequired: true, transformFunction: null }, labelFn: { classPropertyName: "labelFn", publicName: "labelFn", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, behavior: { classPropertyName: "behavior", publicName: "behavior", 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 }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, idInput: { classPropertyName: "idInput", publicName: "id", 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 } }, outputs: { valueChange: "valueChange", moved: "moved" }, host: { attributes: { "role": "group" }, properties: { "id": "id()", "class": "rootClasses()", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "resolvedLabelledBy() || null", "attr.aria-describedby": "resolvedDescribedBy() || null", "attr.aria-disabled": "disabled() || null", "attr.aria-invalid": "errorState() || null", "attr.data-focused": "focused() || null" } }, providers: [
824
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: TransferComponent, isStandalone: true, selector: "tw-transfer", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, keyFn: { classPropertyName: "keyFn", publicName: "keyFn", isSignal: true, isRequired: true, transformFunction: null }, labelFn: { classPropertyName: "labelFn", publicName: "labelFn", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, behavior: { classPropertyName: "behavior", publicName: "behavior", 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 }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, idInput: { classPropertyName: "idInput", publicName: "id", 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 } }, outputs: { valueChange: "valueChange", moved: "moved" }, host: { attributes: { "role": "group" }, properties: { "id": "id()", "class": "rootClasses()", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "resolvedLabelledBy() || null", "attr.aria-describedby": "resolvedDescribedBy() || null", "attr.aria-disabled": "disabled() || null", "attr.data-focused": "focused() || null" } }, providers: [
830
825
  {
831
826
  provide: TW_FORM_FIELD_CONTROL,
832
827
  useExisting: forwardRef(() => TransferComponent),
@@ -878,6 +873,7 @@ class TransferComponent extends FormFieldControl {
878
873
  [class]="listClasses()"
879
874
  [style.height.px]="listHeightPx()"
880
875
  [attr.aria-labelledby]="panel.titleId"
876
+ [attr.aria-invalid]="panel.side === 'target' && errorState() ? 'true' : null"
881
877
  (cdkListboxValueChange)="onCheckedChange(panel.side, $event)"
882
878
  >
883
879
  @for (row of panel.rows; track row.key) {
@@ -911,7 +907,16 @@ class TransferComponent extends FormFieldControl {
911
907
  }
912
908
  </div>
913
909
  } @else {
914
- <div [class]="emptyClasses()" [style.height.px]="listHeightPx()">{{ panel.emptyText }}</div>
910
+ <!--
911
+ Also carries aria-invalid: an empty target is exactly when a required
912
+ transfer is invalid, and the listbox above does not exist to carry it
913
+ (an empty role=listbox would violate aria-required-children).
914
+ -->
915
+ <div
916
+ [class]="emptyClasses()"
917
+ [style.height.px]="listHeightPx()"
918
+ [attr.aria-invalid]="panel.side === 'target' && errorState() ? 'true' : null"
919
+ >{{ panel.emptyText }}</div>
915
920
  }
916
921
  </div>
917
922
  </ng-template>
@@ -984,7 +989,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
984
989
  '[attr.aria-labelledby]': 'resolvedLabelledBy() || null',
985
990
  '[attr.aria-describedby]': 'resolvedDescribedBy() || null',
986
991
  '[attr.aria-disabled]': 'disabled() || null',
987
- '[attr.aria-invalid]': 'errorState() || null',
992
+ // NOTE: no `aria-invalid` here. ARIA 1.2 does not allow it on `role="group"`
993
+ // (axe: critical `aria-allowed-attr`). It is carried by the target panel's
994
+ // content region instead — `value` is `targetKeys`, so that panel is the one
995
+ // holding the value. `tags-input` and `file-upload` were corrected the same
996
+ // way in an earlier pass; this component was missed.
988
997
  '[attr.data-focused]': 'focused() || null',
989
998
  },
990
999
  template: `
@@ -1034,6 +1043,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
1034
1043
  [class]="listClasses()"
1035
1044
  [style.height.px]="listHeightPx()"
1036
1045
  [attr.aria-labelledby]="panel.titleId"
1046
+ [attr.aria-invalid]="panel.side === 'target' && errorState() ? 'true' : null"
1037
1047
  (cdkListboxValueChange)="onCheckedChange(panel.side, $event)"
1038
1048
  >
1039
1049
  @for (row of panel.rows; track row.key) {
@@ -1067,7 +1077,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
1067
1077
  }
1068
1078
  </div>
1069
1079
  } @else {
1070
- <div [class]="emptyClasses()" [style.height.px]="listHeightPx()">{{ panel.emptyText }}</div>
1080
+ <!--
1081
+ Also carries aria-invalid: an empty target is exactly when a required
1082
+ transfer is invalid, and the listbox above does not exist to carry it
1083
+ (an empty role=listbox would violate aria-required-children).
1084
+ -->
1085
+ <div
1086
+ [class]="emptyClasses()"
1087
+ [style.height.px]="listHeightPx()"
1088
+ [attr.aria-invalid]="panel.side === 'target' && errorState() ? 'true' : null"
1089
+ >{{ panel.emptyText }}</div>
1071
1090
  }
1072
1091
  </div>
1073
1092
  </ng-template>