@cdevhub/ngx-tw 0.4.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-dialog-renderer-DoIhoV3d.mjs +198 -0
  47. package/fesm2022/cdevhub-ngx-tw-dialog-dialog-renderer-DoIhoV3d.mjs.map +1 -0
  48. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
  49. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
  50. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +199 -220
  51. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
  53. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  54. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
  55. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
  56. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
  57. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
  59. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
  61. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  62. package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
  63. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
  65. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-item.mjs +18 -3
  67. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  68. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
  69. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
  70. package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
  71. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
  73. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
  75. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
  77. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
  79. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
  81. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
  83. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  84. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
  85. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
  86. package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
  87. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  88. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs +256 -0
  89. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
  90. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
  91. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
  92. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +197 -278
  93. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
  95. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
  97. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
  99. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
  101. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  102. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
  103. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
  105. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
  107. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
  109. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
  111. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  112. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
  113. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
  114. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
  115. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
  116. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
  117. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  118. package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
  119. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  120. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
  121. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
  122. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
  123. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  124. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
  125. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  126. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs +616 -0
  127. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
  128. package/fesm2022/cdevhub-ngx-tw-toast.mjs +556 -945
  129. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  130. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
  131. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  132. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
  133. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
  134. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
  135. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
  136. package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
  137. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
  138. package/index.json +1 -1
  139. package/package.json +63 -2
  140. package/theme/_base.css +13 -0
  141. package/theme/_dark.css +110 -36
  142. package/theme/_high-contrast-dark.css +367 -0
  143. package/theme/_high-contrast.css +13 -2
  144. package/theme/_light.css +276 -0
  145. package/theme/_semantic.css +47 -12
  146. package/theme/_typography.css +17 -0
  147. package/theme/index.css +86 -7
  148. package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
  149. package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
  150. package/types/cdevhub-ngx-tw-button.d.ts +1 -0
  151. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
  152. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
  153. package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
  154. package/types/cdevhub-ngx-tw-card.d.ts +11 -3
  155. package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
  156. package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
  157. package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
  158. package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
  159. package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
  160. package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
  161. package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
  162. package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
  163. package/types/cdevhub-ngx-tw-core.d.ts +152 -21
  164. package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
  165. package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
  166. package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
  167. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
  168. package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
  169. package/types/cdevhub-ngx-tw-dialog.d.ts +89 -19
  170. package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
  171. package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
  172. package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
  173. package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
  174. package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
  175. package/types/cdevhub-ngx-tw-input.d.ts +5 -8
  176. package/types/cdevhub-ngx-tw-item.d.ts +2 -0
  177. package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
  178. package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
  179. package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
  180. package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
  181. package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
  182. package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
  183. package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
  184. package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
  185. package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
  186. package/types/cdevhub-ngx-tw-select.d.ts +104 -33
  187. package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
  188. package/types/cdevhub-ngx-tw-sheet.d.ts +96 -21
  189. package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
  190. package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
  191. package/types/cdevhub-ngx-tw-split.d.ts +32 -2
  192. package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
  193. package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
  194. package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
  195. package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
  196. package/types/cdevhub-ngx-tw-table.d.ts +53 -10
  197. package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
  198. package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
  199. package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
  200. package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
  201. package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
  202. package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
  203. package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
  204. package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
  205. package/types/cdevhub-ngx-tw-toast.d.ts +61 -12
  206. package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
  207. package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
  208. package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
  209. package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
@@ -1,12 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, TemplateRef, Directive, input, booleanAttribute, output, ElementRef, DestroyRef, signal, contentChild, viewChild, computed, afterNextRender, isDevMode, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { inject, TemplateRef, Directive, input, booleanAttribute, output, ElementRef, Injector, DestroyRef, signal, contentChild, viewChild, computed, afterNextRender, isDevMode, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { NgTemplateOutlet } from '@angular/common';
4
- import { NgControl, NgForm, FormGroupDirective, Validators } from '@angular/forms';
4
+ import { NgControl } from '@angular/forms';
5
5
  import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
6
6
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
- import { isObservable, merge } from 'rxjs';
7
+ import { isObservable } from 'rxjs';
8
8
  import { tv } from 'tailwind-variants';
9
- import { TW_ERROR_STATE_MATCHER } from '@cdevhub/ngx-tw/core';
9
+ import { wireErrorState } from '@cdevhub/ngx-tw/core';
10
10
  import { FormFieldControl, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
11
11
  import { ButtonDirective } from '@cdevhub/ngx-tw/button';
12
12
  import { IconComponent } from '@cdevhub/ngx-tw/icon';
@@ -89,11 +89,18 @@ const fileUploadVariants = tv({
89
89
  idle: {},
90
90
  // 1% scale-up — below Tailwind's smallest `scale-95` step but visually
91
91
  // correct for a subtle drag-over lift; the alternative is no lift.
92
+ // No `dark:` variant here, deliberately. The theme layer already
93
+ // inverts the ramp: `_dark.css` maps `--color-primary-50` onto
94
+ // `blue-950`, so `bg-primary-soft` IS the dark wash in dark mode. A
95
+ // `dark:bg-primary-900/20` override on top resolved to `blue-100` —
96
+ // near-white — and turned the drag-over affordance into a bright flash
97
+ // over a `gray-950` dropzone. These were the only two `dark:` utilities
98
+ // in the library, and both were wrong for this reason.
92
99
  draggingValid: {
93
- dropzone: 'border-primary-500 bg-primary-50 dark:bg-primary-900/20 scale-[1.01]',
100
+ dropzone: 'border-primary-500 bg-primary-soft scale-[1.01]',
94
101
  },
95
102
  draggingInvalid: {
96
- dropzone: 'border-error-500 bg-error-50 dark:bg-error-900/20',
103
+ dropzone: 'border-error-500 bg-error-soft',
97
104
  },
98
105
  error: { dropzone: 'border-error-500' },
99
106
  },
@@ -200,20 +207,20 @@ class FileUploadComponent extends FormFieldControl {
200
207
  /** Marks the control as required. Mirrors to `aria-required`. Also inferred from `Validators.required` on a bound `NgControl`. Defaults to `false`. */
201
208
  requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required',
202
209
  transform: booleanAttribute });
203
- /** Headline text rendered inside the drop zone (e.g., `'Drag files here'`). Projected `[twFileUploadHeadline]` content takes precedence over this input. */
210
+ /** Headline text rendered inside the drop zone (e.g., `'Drag files here'`). Projected `[twFileUploadHeadline]` content takes precedence over this input. Defaults to `undefined`. */
204
211
  label = input(undefined, /* @ts-ignore */
205
212
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
206
- /** Secondary text rendered under the label (e.g., `'PDF up to 10MB'`). Projected `[twFileUploadDescription]` content takes precedence. */
213
+ /** Secondary text rendered under the label (e.g., `'PDF up to 10MB'`). Projected `[twFileUploadDescription]` content takes precedence. Defaults to `undefined`. */
207
214
  description = input(undefined, /* @ts-ignore */
208
215
  ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
209
216
  /** Label rendered inside the trigger button. Defaults to `'Choose files'`. Use `'Choose file'` when `multiple` is `false` (consumer responsibility). */
210
217
  triggerLabel = input('Choose files', /* @ts-ignore */
211
218
  ...(ngDevMode ? [{ debugName: "triggerLabel" }] : /* istanbul ignore next */ []));
212
- /** Accessible name applied to the host when no visible label is projected. Mirrored to `aria-label`. */
219
+ /** Accessible name applied to the host when no visible label is projected. Mirrored to `aria-label`. Defaults to `undefined`. */
213
220
  ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
214
- /** ID of an external element that labels the control. Mirrored to `aria-labelledby`. */
221
+ /** ID of an external element that labels the control. Mirrored to `aria-labelledby`. Defaults to `undefined`. */
215
222
  ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
216
- /** ID of an external element that describes the control. Form-field merges its hint/error ids alongside. */
223
+ /** ID of an external element that describes the control. Form-field merges its hint/error ids alongside. Defaults to `undefined`. Alias: `aria-describedby`. */
217
224
  ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
218
225
  /** Optional `name` attribute on the hidden `<input type="file">` for native form submissions. */
219
226
  name = input(undefined, /* @ts-ignore */
@@ -234,13 +241,11 @@ class FileUploadComponent extends FormFieldControl {
234
241
  cleared = output();
235
242
  // ── Injections ──
236
243
  elementRef = inject((ElementRef));
244
+ injector = inject(Injector);
237
245
  focusMonitor = inject(FocusMonitor);
238
246
  liveAnnouncer = inject(LiveAnnouncer);
239
247
  destroyRef = inject(DestroyRef);
240
248
  ngControl = inject(NgControl, { optional: true, self: true });
241
- parentForm = inject(NgForm, { optional: true });
242
- parentFormGroup = inject(FormGroupDirective, { optional: true });
243
- defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
244
249
  // ── Internal mutable state ──
245
250
  itemsSignal = signal([], /* @ts-ignore */
246
251
  ...(ngDevMode ? [{ debugName: "itemsSignal" }] : /* istanbul ignore next */ []));
@@ -252,10 +257,13 @@ class FileUploadComponent extends FormFieldControl {
252
257
  ...(ngDevMode ? [{ debugName: "dragHasInvalidFilesSignal" }] : /* istanbul ignore next */ []));
253
258
  _focused = signal(false, /* @ts-ignore */
254
259
  ...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
255
- _ngControlRev = signal(0, /* @ts-ignore */
256
- ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
257
- _formSubmitRev = signal(0, /* @ts-ignore */
258
- ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
260
+ /** @internal Shared `errorState` / `required` / `errors` derivation — see `wireErrorState`. */
261
+ errorWiring = wireErrorState({
262
+ ngControl: () => this.ngControl,
263
+ matcher: () => this.errorStateMatcher(),
264
+ required: () => this.requiredInput(),
265
+ track: [() => this._focused()],
266
+ });
259
267
  describedByIdsSignal = signal([], /* @ts-ignore */
260
268
  ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
261
269
  labelledByIdsSignal = signal([], /* @ts-ignore */
@@ -311,36 +319,16 @@ class FileUploadComponent extends FormFieldControl {
311
319
  ...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
312
320
  /** @internal */
313
321
  disabled = computed(() => {
314
- this._ngControlRev();
322
+ this.errorWiring.rev();
315
323
  return this.disabledInput() || this.cvaDisabled() || !!this.ngControl?.disabled;
316
324
  }, /* @ts-ignore */
317
325
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
318
326
  /** @internal */
319
- required = computed(() => {
320
- this._ngControlRev();
321
- if (this.requiredInput())
322
- return true;
323
- const ctrl = this.ngControl?.control;
324
- return !!ctrl?.hasValidator(Validators.required);
325
- }, /* @ts-ignore */
326
- ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
327
- /** @internal */
328
- errorState = computed(() => {
329
- this._ngControlRev();
330
- this._formSubmitRev();
331
- this._focused();
332
- const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
333
- const form = this.parentFormGroup ??
334
- this.parentForm;
335
- return matcher.isErrorState(this.ngControl?.control ?? null, form);
336
- }, /* @ts-ignore */
337
- ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
327
+ required = this.errorWiring.required;
328
+ /** @internal */
329
+ errorState = this.errorWiring.errorState;
338
330
  /** @internal Validation errors map from the bound `NgControl`. */
339
- errors = computed(() => {
340
- this._ngControlRev();
341
- return this.ngControl?.control?.errors ?? null;
342
- }, /* @ts-ignore */
343
- ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
331
+ errors = this.errorWiring.errors;
344
332
  /** @internal */
345
333
  controlType = 'file-upload';
346
334
  /** @internal */
@@ -491,8 +479,21 @@ class FileUploadComponent extends FormFieldControl {
491
479
  this.emitChange();
492
480
  this.fileRemoved.emit(removed);
493
481
  this.liveAnnouncer.announce(`${removed.file.name} removed.`, 'polite');
494
- // Move focus to the next remove button, or to the trigger button when none remain.
495
- queueMicrotask(() => {
482
+ // Move focus to the next remove button, or to the trigger button when none
483
+ // remain.
484
+ //
485
+ // This MUST wait for the render, not merely for the microtask queue. A
486
+ // `queueMicrotask` here (what this used to be) runs before Angular's
487
+ // zoneless scheduler has re-rendered the list, so `nextButtons` was still
488
+ // the PRE-removal node list: the "next" button we focused was the one about
489
+ // to be destroyed, and focus fell to `<body>`. Removing the last item took
490
+ // the `length === 0` branch for the same reason — the count was stale, so
491
+ // the trigger never got focus either.
492
+ //
493
+ // It survived unit testing because a spec that calls `detectChanges()`
494
+ // between the click and the assertion has already re-rendered by the time
495
+ // the microtask runs, which is the opposite order from a real browser.
496
+ afterNextRender(() => {
496
497
  const host = this.elementRef.nativeElement;
497
498
  const nextButtons = host.querySelectorAll('[data-tw-file-upload-remove]');
498
499
  if (nextButtons.length === 0) {
@@ -502,7 +503,7 @@ class FileUploadComponent extends FormFieldControl {
502
503
  const fallback = nextButtons[Math.min(idx, nextButtons.length - 1)];
503
504
  fallback?.focus();
504
505
  }
505
- });
506
+ }, { injector: this.injector });
506
507
  }
507
508
  /** Removes all items. Emits `cleared` and calls `onChange([])`. No-op when already empty or disabled. */
508
509
  clear() {
@@ -839,28 +840,14 @@ class FileUploadComponent extends FormFieldControl {
839
840
  this._focused.set(!!origin);
840
841
  if (wasFocused && !origin) {
841
842
  this.onTouched();
842
- this._ngControlRev.update((n) => n + 1);
843
+ this.errorWiring.bump();
843
844
  }
844
845
  });
845
846
  }
846
847
  this.destroyRef.onDestroy(() => {
847
848
  this.focusMonitor.stopMonitoring(this.elementRef);
848
849
  });
849
- const ctrl = this.ngControl?.control;
850
- if (ctrl) {
851
- const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
852
- if (streams.length) {
853
- merge(...streams)
854
- .pipe(takeUntilDestroyed(this.destroyRef))
855
- .subscribe(() => this._ngControlRev.update((n) => n + 1));
856
- }
857
- }
858
- const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
859
- if (submit) {
860
- submit
861
- .pipe(takeUntilDestroyed(this.destroyRef))
862
- .subscribe(() => this._formSubmitRev.update((n) => n + 1));
863
- }
850
+ this.errorWiring.connect();
864
851
  }
865
852
  // ── Helpers ──
866
853
  hasAccessibleNameHint() {
@@ -870,12 +857,12 @@ class FileUploadComponent extends FormFieldControl {
870
857
  return !!host.textContent && host.textContent.trim().length > 0;
871
858
  }
872
859
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: FileUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
873
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: FileUploadComponent, isStandalone: true, selector: "tw-file-upload", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", 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 }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", 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 }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, idInput: { classPropertyName: "idInput", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filesAdded: "filesAdded", fileRemoved: "fileRemoved", fileRejected: "fileRejected", cleared: "cleared" }, host: { attributes: { "role": "group" }, properties: { "id": "id()", "class": "rootClasses()", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "effectiveAriaLabelledby() || null", "attr.aria-describedby": "effectiveAriaDescribedby() || null", "attr.aria-disabled": "disabled() || null", "attr.aria-required": "required() || null", "attr.aria-invalid": "errorState() || null" } }, providers: [
860
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: FileUploadComponent, isStandalone: true, selector: "tw-file-upload", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", 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 }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", 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 }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, idInput: { classPropertyName: "idInput", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filesAdded: "filesAdded", fileRemoved: "fileRemoved", fileRejected: "fileRejected", cleared: "cleared" }, host: { attributes: { "role": "group" }, properties: { "id": "id()", "class": "rootClasses()", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "effectiveAriaLabelledby() || null", "attr.aria-describedby": "effectiveAriaDescribedby() || null", "attr.aria-disabled": "disabled() || null" } }, providers: [
874
861
  {
875
862
  provide: TW_FORM_FIELD_CONTROL,
876
863
  useExisting: forwardRef(() => FileUploadComponent),
877
864
  },
878
- ], queries: [{ propertyName: "itemSlot", first: true, predicate: FileUploadItemDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "dropzoneRef", first: true, predicate: ["dropzone"], descendants: true, isSignal: true }, { propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "hiddenInputRef", first: true, predicate: ["hiddenInput"], descendants: true, isSignal: true }], exportAs: ["twFileUpload"], usesInheritance: true, ngImport: i0, template: "<input\n #hiddenInput\n type=\"file\"\n class=\"sr-only\"\n tabindex=\"-1\"\n [id]=\"hiddenInputId\"\n [attr.aria-label]=\"ariaLabel() ?? label() ?? triggerLabel()\"\n [attr.name]=\"name() || null\"\n [attr.accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n [required]=\"required()\"\n (change)=\"onNativeChange($event)\"\n/>\n\n<!-- The dropzone is a mouse click/drop convenience target, intentionally NOT a\n focusable control: the inner trigger <button> owns keyboard operability, so\n adding tabindex/keydown here would create a nested-interactive structure.\n No keyboard user is locked out, so the rules' concern does not apply. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #dropzone\n [class]=\"dropzoneClasses()\"\n (click)=\"open()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div [class]=\"dropzoneInnerClasses()\">\n <div [class]=\"illustrationClasses()\">\n <ng-content select=\"[twFileUploadIcon]\">\n <tw-icon name=\"upload-cloud\" [size]=\"iconSize()\" aria-hidden=\"true\" />\n </ng-content>\n </div>\n\n <span [id]=\"headlineId\" [class]=\"headlineClasses()\">\n <ng-content select=\"[twFileUploadHeadline]\">\n @if (label(); as l) {\n {{ l }}\n } @else {\n Drag files here or click to browse\n }\n </ng-content>\n </span>\n\n <span [id]=\"descriptionId\" [class]=\"descriptionClasses()\">\n <ng-content select=\"[twFileUploadDescription]\">\n @if (description(); as d) {\n {{ d }}\n }\n </ng-content>\n </span>\n\n <button\n #trigger\n type=\"button\"\n twButton\n variant=\"outline\"\n color=\"neutral\"\n [size]=\"size()\"\n [disabled]=\"disabled()\"\n [attr.aria-describedby]=\"effectiveAriaDescribedby() || null\"\n (click)=\"onTriggerClick($event)\"\n >\n {{ triggerLabel() }}\n </button>\n </div>\n</div>\n\n<ul [class]=\"listClasses()\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (itemSlot(); as slot) {\n <ng-container\n [ngTemplateOutlet]=\"slot.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: item, item: item }\"\n />\n } @else {\n <div [class]=\"itemClasses()\">\n <tw-icon\n [name]=\"iconNameFor(item)\"\n size=\"md\"\n [class]=\"itemIconClasses()\"\n aria-hidden=\"true\"\n />\n <div [class]=\"itemBodyClasses()\">\n <span [class]=\"itemNameClasses()\">{{ item.file.name }}</span>\n <span [class]=\"itemMetaClasses()\">{{ metaTextFor(item) }}</span>\n @if (shouldShowProgress(item)) {\n <tw-progress-bar\n [class]=\"itemProgressClasses()\"\n [value]=\"item.progress\"\n size=\"sm\"\n [color]=\"progressColorFor(item)\"\n />\n }\n </div>\n <div [class]=\"itemActionsClasses()\">\n <button\n type=\"button\"\n twButton\n variant=\"ghost\"\n color=\"neutral\"\n size=\"sm\"\n data-tw-file-upload-remove\n [attr.aria-label]=\"'Remove ' + item.file.name\"\n (click)=\"remove(item.id)\"\n >\n <tw-icon twButtonIcon name=\"x\" />\n </button>\n </div>\n </div>\n }\n </li>\n }\n</ul>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ButtonDirective, selector: "[twButton]", inputs: ["variant", "color", "size", "disabled", "loading"], exportAs: ["twButton"] }, { kind: "component", type: IconComponent, selector: "tw-icon", inputs: ["name", "img", "color", "size", "ariaLabel", "svg"] }, { kind: "component", type: ProgressBarComponent, selector: "tw-progress-bar", inputs: ["value", "variant", "color", "size", "label", "options"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
865
+ ], queries: [{ propertyName: "itemSlot", first: true, predicate: FileUploadItemDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "dropzoneRef", first: true, predicate: ["dropzone"], descendants: true, isSignal: true }, { propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "hiddenInputRef", first: true, predicate: ["hiddenInput"], descendants: true, isSignal: true }], exportAs: ["twFileUpload"], usesInheritance: true, ngImport: i0, template: "<input\n #hiddenInput\n type=\"file\"\n class=\"sr-only\"\n tabindex=\"-1\"\n [id]=\"hiddenInputId\"\n [attr.aria-label]=\"ariaLabel() ?? label() ?? triggerLabel()\"\n [attr.name]=\"name() || null\"\n [attr.accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n [required]=\"required()\"\n [attr.aria-invalid]=\"errorState() || null\"\n (change)=\"onNativeChange($event)\"\n/>\n\n<!-- The dropzone is a mouse click/drop convenience target, intentionally NOT a\n focusable control: the inner trigger <button> owns keyboard operability, so\n adding tabindex/keydown here would create a nested-interactive structure.\n No keyboard user is locked out, so the rules' concern does not apply. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #dropzone\n [class]=\"dropzoneClasses()\"\n (click)=\"open()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div [class]=\"dropzoneInnerClasses()\">\n <div [class]=\"illustrationClasses()\">\n <ng-content select=\"[twFileUploadIcon]\">\n <tw-icon name=\"upload-cloud\" [size]=\"iconSize()\" aria-hidden=\"true\" />\n </ng-content>\n </div>\n\n <span [id]=\"headlineId\" [class]=\"headlineClasses()\">\n <ng-content select=\"[twFileUploadHeadline]\">\n @if (label(); as l) {\n {{ l }}\n } @else {\n Drag files here or click to browse\n }\n </ng-content>\n </span>\n\n <span [id]=\"descriptionId\" [class]=\"descriptionClasses()\">\n <ng-content select=\"[twFileUploadDescription]\">\n @if (description(); as d) {\n {{ d }}\n }\n </ng-content>\n </span>\n\n <button\n #trigger\n type=\"button\"\n twButton\n variant=\"outline\"\n color=\"neutral\"\n [size]=\"size()\"\n [disabled]=\"disabled()\"\n [attr.aria-describedby]=\"effectiveAriaDescribedby() || null\"\n (click)=\"onTriggerClick($event)\"\n >\n {{ triggerLabel() }}\n </button>\n </div>\n</div>\n\n<ul [class]=\"listClasses()\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (itemSlot(); as slot) {\n <ng-container\n [ngTemplateOutlet]=\"slot.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: item, item: item }\"\n />\n } @else {\n <div [class]=\"itemClasses()\">\n <tw-icon\n [name]=\"iconNameFor(item)\"\n size=\"md\"\n [class]=\"itemIconClasses()\"\n aria-hidden=\"true\"\n />\n <div [class]=\"itemBodyClasses()\">\n <span [class]=\"itemNameClasses()\">{{ item.file.name }}</span>\n <span [class]=\"itemMetaClasses()\">{{ metaTextFor(item) }}</span>\n @if (shouldShowProgress(item)) {\n <tw-progress-bar\n [class]=\"itemProgressClasses()\"\n [value]=\"item.progress\"\n size=\"sm\"\n [color]=\"progressColorFor(item)\"\n />\n }\n </div>\n <div [class]=\"itemActionsClasses()\">\n <button\n type=\"button\"\n twButton\n variant=\"ghost\"\n color=\"neutral\"\n size=\"sm\"\n data-tw-file-upload-remove\n [attr.aria-label]=\"'Remove ' + item.file.name\"\n (click)=\"remove(item.id)\"\n >\n <tw-icon twButtonIcon name=\"x\" />\n </button>\n </div>\n </div>\n }\n </li>\n }\n</ul>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ButtonDirective, selector: "[twButton]", inputs: ["variant", "color", "size", "disabled", "loading"], exportAs: ["twButton"] }, { kind: "component", type: IconComponent, selector: "tw-icon", inputs: ["name", "img", "color", "size", "aria-label", "svg"] }, { kind: "component", type: ProgressBarComponent, selector: "tw-progress-bar", inputs: ["value", "variant", "color", "size", "label", "options"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
879
866
  }
880
867
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: FileUploadComponent, decorators: [{
881
868
  type: Component,
@@ -892,9 +879,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
892
879
  '[attr.aria-labelledby]': 'effectiveAriaLabelledby() || null',
893
880
  '[attr.aria-describedby]': 'effectiveAriaDescribedby() || null',
894
881
  '[attr.aria-disabled]': 'disabled() || null',
895
- '[attr.aria-required]': 'required() || null',
896
- '[attr.aria-invalid]': 'errorState() || null',
897
- }, template: "<input\n #hiddenInput\n type=\"file\"\n class=\"sr-only\"\n tabindex=\"-1\"\n [id]=\"hiddenInputId\"\n [attr.aria-label]=\"ariaLabel() ?? label() ?? triggerLabel()\"\n [attr.name]=\"name() || null\"\n [attr.accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n [required]=\"required()\"\n (change)=\"onNativeChange($event)\"\n/>\n\n<!-- The dropzone is a mouse click/drop convenience target, intentionally NOT a\n focusable control: the inner trigger <button> owns keyboard operability, so\n adding tabindex/keydown here would create a nested-interactive structure.\n No keyboard user is locked out, so the rules' concern does not apply. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #dropzone\n [class]=\"dropzoneClasses()\"\n (click)=\"open()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div [class]=\"dropzoneInnerClasses()\">\n <div [class]=\"illustrationClasses()\">\n <ng-content select=\"[twFileUploadIcon]\">\n <tw-icon name=\"upload-cloud\" [size]=\"iconSize()\" aria-hidden=\"true\" />\n </ng-content>\n </div>\n\n <span [id]=\"headlineId\" [class]=\"headlineClasses()\">\n <ng-content select=\"[twFileUploadHeadline]\">\n @if (label(); as l) {\n {{ l }}\n } @else {\n Drag files here or click to browse\n }\n </ng-content>\n </span>\n\n <span [id]=\"descriptionId\" [class]=\"descriptionClasses()\">\n <ng-content select=\"[twFileUploadDescription]\">\n @if (description(); as d) {\n {{ d }}\n }\n </ng-content>\n </span>\n\n <button\n #trigger\n type=\"button\"\n twButton\n variant=\"outline\"\n color=\"neutral\"\n [size]=\"size()\"\n [disabled]=\"disabled()\"\n [attr.aria-describedby]=\"effectiveAriaDescribedby() || null\"\n (click)=\"onTriggerClick($event)\"\n >\n {{ triggerLabel() }}\n </button>\n </div>\n</div>\n\n<ul [class]=\"listClasses()\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (itemSlot(); as slot) {\n <ng-container\n [ngTemplateOutlet]=\"slot.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: item, item: item }\"\n />\n } @else {\n <div [class]=\"itemClasses()\">\n <tw-icon\n [name]=\"iconNameFor(item)\"\n size=\"md\"\n [class]=\"itemIconClasses()\"\n aria-hidden=\"true\"\n />\n <div [class]=\"itemBodyClasses()\">\n <span [class]=\"itemNameClasses()\">{{ item.file.name }}</span>\n <span [class]=\"itemMetaClasses()\">{{ metaTextFor(item) }}</span>\n @if (shouldShowProgress(item)) {\n <tw-progress-bar\n [class]=\"itemProgressClasses()\"\n [value]=\"item.progress\"\n size=\"sm\"\n [color]=\"progressColorFor(item)\"\n />\n }\n </div>\n <div [class]=\"itemActionsClasses()\">\n <button\n type=\"button\"\n twButton\n variant=\"ghost\"\n color=\"neutral\"\n size=\"sm\"\n data-tw-file-upload-remove\n [attr.aria-label]=\"'Remove ' + item.file.name\"\n (click)=\"remove(item.id)\"\n >\n <tw-icon twButtonIcon name=\"x\" />\n </button>\n </div>\n </div>\n }\n </li>\n }\n</ul>\n" }]
882
+ // NOTE: `aria-required` and `aria-invalid` are NOT set here. ARIA 1.2 does
883
+ // not allow either on `role="group"`, and axe flags it as a *critical*
884
+ // `aria-allowed-attr` violation. Both belong on the control that carries
885
+ // the value: the hidden `<input type="file">` takes native `required` and
886
+ // `aria-invalid` in `file-upload.html`.
887
+ }, template: "<input\n #hiddenInput\n type=\"file\"\n class=\"sr-only\"\n tabindex=\"-1\"\n [id]=\"hiddenInputId\"\n [attr.aria-label]=\"ariaLabel() ?? label() ?? triggerLabel()\"\n [attr.name]=\"name() || null\"\n [attr.accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n [required]=\"required()\"\n [attr.aria-invalid]=\"errorState() || null\"\n (change)=\"onNativeChange($event)\"\n/>\n\n<!-- The dropzone is a mouse click/drop convenience target, intentionally NOT a\n focusable control: the inner trigger <button> owns keyboard operability, so\n adding tabindex/keydown here would create a nested-interactive structure.\n No keyboard user is locked out, so the rules' concern does not apply. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #dropzone\n [class]=\"dropzoneClasses()\"\n (click)=\"open()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div [class]=\"dropzoneInnerClasses()\">\n <div [class]=\"illustrationClasses()\">\n <ng-content select=\"[twFileUploadIcon]\">\n <tw-icon name=\"upload-cloud\" [size]=\"iconSize()\" aria-hidden=\"true\" />\n </ng-content>\n </div>\n\n <span [id]=\"headlineId\" [class]=\"headlineClasses()\">\n <ng-content select=\"[twFileUploadHeadline]\">\n @if (label(); as l) {\n {{ l }}\n } @else {\n Drag files here or click to browse\n }\n </ng-content>\n </span>\n\n <span [id]=\"descriptionId\" [class]=\"descriptionClasses()\">\n <ng-content select=\"[twFileUploadDescription]\">\n @if (description(); as d) {\n {{ d }}\n }\n </ng-content>\n </span>\n\n <button\n #trigger\n type=\"button\"\n twButton\n variant=\"outline\"\n color=\"neutral\"\n [size]=\"size()\"\n [disabled]=\"disabled()\"\n [attr.aria-describedby]=\"effectiveAriaDescribedby() || null\"\n (click)=\"onTriggerClick($event)\"\n >\n {{ triggerLabel() }}\n </button>\n </div>\n</div>\n\n<ul [class]=\"listClasses()\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (itemSlot(); as slot) {\n <ng-container\n [ngTemplateOutlet]=\"slot.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: item, item: item }\"\n />\n } @else {\n <div [class]=\"itemClasses()\">\n <tw-icon\n [name]=\"iconNameFor(item)\"\n size=\"md\"\n [class]=\"itemIconClasses()\"\n aria-hidden=\"true\"\n />\n <div [class]=\"itemBodyClasses()\">\n <span [class]=\"itemNameClasses()\">{{ item.file.name }}</span>\n <span [class]=\"itemMetaClasses()\">{{ metaTextFor(item) }}</span>\n @if (shouldShowProgress(item)) {\n <tw-progress-bar\n [class]=\"itemProgressClasses()\"\n [value]=\"item.progress\"\n size=\"sm\"\n [color]=\"progressColorFor(item)\"\n />\n }\n </div>\n <div [class]=\"itemActionsClasses()\">\n <button\n type=\"button\"\n twButton\n variant=\"ghost\"\n color=\"neutral\"\n size=\"sm\"\n data-tw-file-upload-remove\n [attr.aria-label]=\"'Remove ' + item.file.name\"\n (click)=\"remove(item.id)\"\n >\n <tw-icon twButtonIcon name=\"x\" />\n </button>\n </div>\n </div>\n }\n </li>\n }\n</ul>\n" }]
898
888
  }], ctorParameters: () => [], propDecorators: { multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", 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 }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], idInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], filesAdded: [{ type: i0.Output, args: ["filesAdded"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], fileRejected: [{ type: i0.Output, args: ["fileRejected"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], itemSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FileUploadItemDirective), { isSignal: true }] }], dropzoneRef: [{ type: i0.ViewChild, args: ['dropzone', { isSignal: true }] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], hiddenInputRef: [{ type: i0.ViewChild, args: ['hiddenInput', { isSignal: true }] }] } });
899
889
 
900
890
  /**