@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
@@ -0,0 +1,172 @@
1
+ import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
2
+
3
+ /**
4
+ * Harness for a single committed chip inside a `tw-tags-input`.
5
+ *
6
+ * Chips are rendered by composing `[twBadge]`, so that attribute is the host
7
+ * selector. Reach these through {@link TagsInputHarness.getTags} rather than
8
+ * locating them directly against a fixture — located from a document root the
9
+ * selector would also match every unrelated badge on the page.
10
+ */
11
+ class TagsInputTagHarness extends ComponentHarness {
12
+ static hostSelector = '[twBadge]';
13
+ /**
14
+ * The chip's remove control. It is the only `<button>` a chip contains: the
15
+ * badge's own `dismissible` button is not enabled here, so this always
16
+ * resolves to the tags-input remove control.
17
+ */
18
+ removeButton = this.locatorFor('button');
19
+ /** Predicate for `locatorFor` / `locatorForAll`. */
20
+ static with(options = {}) {
21
+ return new HarnessPredicate(TagsInputTagHarness, options).addOption('text', options.text, async (h, text) => HarnessPredicate.stringMatches(await h.getText(), text));
22
+ }
23
+ /**
24
+ * The chip's visible label, trimmed. This is `tagLabel(tag)`, not the tag
25
+ * value — for object tags the two differ.
26
+ */
27
+ async getText() {
28
+ return (await (await this.host()).text()).trim();
29
+ }
30
+ /**
31
+ * The remove control's accessible name (`Remove <label>`). Useful for
32
+ * asserting the control is announced, which is the only signal a screen
33
+ * reader gets for a chip.
34
+ */
35
+ async getRemoveLabel() {
36
+ return (await this.removeButton()).getAttribute('aria-label');
37
+ }
38
+ /**
39
+ * Clicks the chip's remove control. The component restores focus to the
40
+ * neighbouring chip (or the text input when none remain), so the caller may
41
+ * assert focus immediately afterwards.
42
+ */
43
+ async remove() {
44
+ await (await this.removeButton()).click();
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Harness for `tw-tags-input`.
50
+ *
51
+ * The control renders committed tags as chips followed by a single text input;
52
+ * everything lives inside the `tw-tags-input` host, so no overlay handling is
53
+ * needed and the ordinary fixture loader is enough.
54
+ *
55
+ * Note where the ARIA lives: the host is a `role="group"`, which ARIA 1.2 does
56
+ * not allow to carry `aria-required` / `aria-invalid`, so those sit on the
57
+ * inner text input — the element that actually owns the pending value.
58
+ * {@link isRequired} and {@link isInvalid} read them from there;
59
+ * {@link isDisabled} reads `aria-disabled` from the group, which is allowed.
60
+ */
61
+ class TagsInputHarness extends ComponentHarness {
62
+ static hostSelector = 'tw-tags-input';
63
+ textInput = this.locatorFor('input[type="text"]');
64
+ /** Predicate for `locatorFor` / `locatorForAll`. */
65
+ static with(options = {}) {
66
+ return new HarnessPredicate(TagsInputHarness, options)
67
+ .addOption('label', options.label, async (h, label) => HarnessPredicate.stringMatches(await h.getLabel(), label))
68
+ .addOption('disabled', options.disabled, async (h, disabled) => (await h.isDisabled()) === disabled);
69
+ }
70
+ /** The control's accessible name, from `aria-label` when one is set. */
71
+ async getLabel() {
72
+ return (await this.host()).getAttribute('aria-label');
73
+ }
74
+ /** Whether the group reports `aria-disabled="true"`. */
75
+ async isDisabled() {
76
+ return (await (await this.host()).getAttribute('aria-disabled')) === 'true';
77
+ }
78
+ /** Whether the text input reports `aria-required="true"`. */
79
+ async isRequired() {
80
+ return (await (await this.textInput()).getAttribute('aria-required')) === 'true';
81
+ }
82
+ /** Whether the text input reports `aria-invalid="true"`. */
83
+ async isInvalid() {
84
+ return (await (await this.textInput()).getAttribute('aria-invalid')) === 'true';
85
+ }
86
+ /**
87
+ * Every committed chip, in render order. Remove by index by indexing this
88
+ * array — `(await input.getTags())[1].remove()` — which is why no
89
+ * `removeTagAt(index)` exists.
90
+ */
91
+ async getTags(filters = {}) {
92
+ return this.locatorForAll(TagsInputTagHarness.with(filters))();
93
+ }
94
+ /** The visible label of every committed chip, in render order. */
95
+ async getTagTexts() {
96
+ const tags = await this.getTags();
97
+ return Promise.all(tags.map((tag) => tag.getText()));
98
+ }
99
+ /** The in-progress (uncommitted) text currently in the input. */
100
+ async getInputValue() {
101
+ return (await this.textInput()).getProperty('value');
102
+ }
103
+ /**
104
+ * Types `text` into the input and commits it with Enter. Typing appends, so
105
+ * any pending text already in the input is committed along with it.
106
+ *
107
+ * Enter is the commit gesture only while `separatorKeys` contains `'Enter'`
108
+ * (the default). A control configured with, say, `[separatorKeys]="[';']"`
109
+ * commits through {@link typeInput} instead — `typeInput('alpha;')`.
110
+ *
111
+ * The commit may be dropped by the component (empty, duplicate, or `maxTags`
112
+ * reached); assert against {@link getTagTexts} rather than assuming it
113
+ * landed.
114
+ */
115
+ async addTag(text) {
116
+ const input = await this.textInput();
117
+ await input.sendKeys(text);
118
+ await input.sendKeys(TestKey.ENTER);
119
+ }
120
+ /**
121
+ * Types `text` into the input character by character, appending to whatever
122
+ * is already there. Use it to stage pending text that {@link addTag} would
123
+ * commit for you — reading it back, discarding it, or letting `addOnBlur`
124
+ * pick it up.
125
+ *
126
+ * A separator character in `text` does commit the text before it, but CDK's
127
+ * synthetic typing appends every character to the input regardless of the
128
+ * component's `preventDefault()`, so the separator itself is left behind in
129
+ * the input where a real browser swallows it. Commit with {@link addTag},
130
+ * and treat a trailing separator here as staging, not as a commit gesture.
131
+ */
132
+ async typeInput(text) {
133
+ await (await this.textInput()).sendKeys(text);
134
+ }
135
+ /**
136
+ * Removes the first chip whose label matches. Throws when nothing matches,
137
+ * rather than failing silently.
138
+ */
139
+ async removeTag(text) {
140
+ const matches = await this.getTags({ text });
141
+ if (matches.length === 0) {
142
+ throw new Error(`TagsInputHarness.removeTag: no tag matching ${String(text)}.`);
143
+ }
144
+ await matches[0].remove();
145
+ }
146
+ /**
147
+ * Discards the in-progress text with Escape — the control's own clear
148
+ * gesture. A no-op when the input is already empty. Does not touch committed
149
+ * chips.
150
+ */
151
+ async clearInput() {
152
+ await (await this.textInput()).sendKeys(TestKey.ESCAPE);
153
+ }
154
+ /** Moves focus to the text input. */
155
+ async focus() {
156
+ await (await this.textInput()).focus();
157
+ }
158
+ /**
159
+ * Moves focus out of the control. This is the gesture that marks the control
160
+ * touched and, with `addOnBlur`, commits the pending text.
161
+ */
162
+ async blur() {
163
+ await (await this.textInput()).blur();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Generated bundle index. Do not edit.
169
+ */
170
+
171
+ export { TagsInputHarness, TagsInputTagHarness };
172
+ //# sourceMappingURL=cdevhub-ngx-tw-tags-input-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdevhub-ngx-tw-tags-input-testing.mjs","sources":["../../../projects/ngx-tw/tags-input/testing/tags-input-tag-harness.ts","../../../projects/ngx-tw/tags-input/testing/tags-input-harness.ts","../../../projects/ngx-tw/tags-input/testing/cdevhub-ngx-tw-tags-input-testing.ts"],"sourcesContent":["import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\nimport type { BaseHarnessFilters } from '@angular/cdk/testing';\n\n/** Filters accepted by `TagsInputTagHarness.with`. */\nexport interface TagsInputTagHarnessFilters extends BaseHarnessFilters {\n /** Match by the chip's visible label. */\n text?: string | RegExp;\n}\n\n/**\n * Harness for a single committed chip inside a `tw-tags-input`.\n *\n * Chips are rendered by composing `[twBadge]`, so that attribute is the host\n * selector. Reach these through {@link TagsInputHarness.getTags} rather than\n * locating them directly against a fixture — located from a document root the\n * selector would also match every unrelated badge on the page.\n */\nexport class TagsInputTagHarness extends ComponentHarness {\n static hostSelector = '[twBadge]';\n\n /**\n * The chip's remove control. It is the only `<button>` a chip contains: the\n * badge's own `dismissible` button is not enabled here, so this always\n * resolves to the tags-input remove control.\n */\n private readonly removeButton = this.locatorFor('button');\n\n /** Predicate for `locatorFor` / `locatorForAll`. */\n static with(\n options: TagsInputTagHarnessFilters = {},\n ): HarnessPredicate<TagsInputTagHarness> {\n return new HarnessPredicate(TagsInputTagHarness, options).addOption(\n 'text',\n options.text,\n async (h, text) => HarnessPredicate.stringMatches(await h.getText(), text),\n );\n }\n\n /**\n * The chip's visible label, trimmed. This is `tagLabel(tag)`, not the tag\n * value — for object tags the two differ.\n */\n async getText(): Promise<string> {\n return (await (await this.host()).text()).trim();\n }\n\n /**\n * The remove control's accessible name (`Remove <label>`). Useful for\n * asserting the control is announced, which is the only signal a screen\n * reader gets for a chip.\n */\n async getRemoveLabel(): Promise<string | null> {\n return (await this.removeButton()).getAttribute('aria-label');\n }\n\n /**\n * Clicks the chip's remove control. The component restores focus to the\n * neighbouring chip (or the text input when none remain), so the caller may\n * assert focus immediately afterwards.\n */\n async remove(): Promise<void> {\n await (await this.removeButton()).click();\n }\n}\n","import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';\nimport type { BaseHarnessFilters } from '@angular/cdk/testing';\nimport {\n TagsInputTagHarness,\n type TagsInputTagHarnessFilters,\n} from './tags-input-tag-harness';\n\n/** Filters accepted by `TagsInputHarness.with`. */\nexport interface TagsInputHarnessFilters extends BaseHarnessFilters {\n /** Match by the control's accessible name (its `aria-label`). */\n label?: string | RegExp;\n /** Match disabled / enabled controls. */\n disabled?: boolean;\n}\n\n/**\n * Harness for `tw-tags-input`.\n *\n * The control renders committed tags as chips followed by a single text input;\n * everything lives inside the `tw-tags-input` host, so no overlay handling is\n * needed and the ordinary fixture loader is enough.\n *\n * Note where the ARIA lives: the host is a `role=\"group\"`, which ARIA 1.2 does\n * not allow to carry `aria-required` / `aria-invalid`, so those sit on the\n * inner text input — the element that actually owns the pending value.\n * {@link isRequired} and {@link isInvalid} read them from there;\n * {@link isDisabled} reads `aria-disabled` from the group, which is allowed.\n */\nexport class TagsInputHarness extends ComponentHarness {\n static hostSelector = 'tw-tags-input';\n\n private readonly textInput = this.locatorFor('input[type=\"text\"]');\n\n /** Predicate for `locatorFor` / `locatorForAll`. */\n static with(options: TagsInputHarnessFilters = {}): HarnessPredicate<TagsInputHarness> {\n return new HarnessPredicate(TagsInputHarness, options)\n .addOption('label', options.label, async (h, label) =>\n HarnessPredicate.stringMatches(await h.getLabel(), label),\n )\n .addOption(\n 'disabled',\n options.disabled,\n async (h, disabled) => (await h.isDisabled()) === disabled,\n );\n }\n\n /** The control's accessible name, from `aria-label` when one is set. */\n async getLabel(): Promise<string | null> {\n return (await this.host()).getAttribute('aria-label');\n }\n\n /** Whether the group reports `aria-disabled=\"true\"`. */\n async isDisabled(): Promise<boolean> {\n return (await (await this.host()).getAttribute('aria-disabled')) === 'true';\n }\n\n /** Whether the text input reports `aria-required=\"true\"`. */\n async isRequired(): Promise<boolean> {\n return (await (await this.textInput()).getAttribute('aria-required')) === 'true';\n }\n\n /** Whether the text input reports `aria-invalid=\"true\"`. */\n async isInvalid(): Promise<boolean> {\n return (await (await this.textInput()).getAttribute('aria-invalid')) === 'true';\n }\n\n /**\n * Every committed chip, in render order. Remove by index by indexing this\n * array — `(await input.getTags())[1].remove()` — which is why no\n * `removeTagAt(index)` exists.\n */\n async getTags(\n filters: TagsInputTagHarnessFilters = {},\n ): Promise<TagsInputTagHarness[]> {\n return this.locatorForAll(TagsInputTagHarness.with(filters))();\n }\n\n /** The visible label of every committed chip, in render order. */\n async getTagTexts(): Promise<string[]> {\n const tags = await this.getTags();\n return Promise.all(tags.map((tag) => tag.getText()));\n }\n\n /** The in-progress (uncommitted) text currently in the input. */\n async getInputValue(): Promise<string> {\n return (await this.textInput()).getProperty<string>('value');\n }\n\n /**\n * Types `text` into the input and commits it with Enter. Typing appends, so\n * any pending text already in the input is committed along with it.\n *\n * Enter is the commit gesture only while `separatorKeys` contains `'Enter'`\n * (the default). A control configured with, say, `[separatorKeys]=\"[';']\"`\n * commits through {@link typeInput} instead — `typeInput('alpha;')`.\n *\n * The commit may be dropped by the component (empty, duplicate, or `maxTags`\n * reached); assert against {@link getTagTexts} rather than assuming it\n * landed.\n */\n async addTag(text: string): Promise<void> {\n const input = await this.textInput();\n await input.sendKeys(text);\n await input.sendKeys(TestKey.ENTER);\n }\n\n /**\n * Types `text` into the input character by character, appending to whatever\n * is already there. Use it to stage pending text that {@link addTag} would\n * commit for you — reading it back, discarding it, or letting `addOnBlur`\n * pick it up.\n *\n * A separator character in `text` does commit the text before it, but CDK's\n * synthetic typing appends every character to the input regardless of the\n * component's `preventDefault()`, so the separator itself is left behind in\n * the input where a real browser swallows it. Commit with {@link addTag},\n * and treat a trailing separator here as staging, not as a commit gesture.\n */\n async typeInput(text: string): Promise<void> {\n await (await this.textInput()).sendKeys(text);\n }\n\n /**\n * Removes the first chip whose label matches. Throws when nothing matches,\n * rather than failing silently.\n */\n async removeTag(text: string | RegExp): Promise<void> {\n const matches = await this.getTags({ text });\n if (matches.length === 0) {\n throw new Error(`TagsInputHarness.removeTag: no tag matching ${String(text)}.`);\n }\n await matches[0].remove();\n }\n\n /**\n * Discards the in-progress text with Escape — the control's own clear\n * gesture. A no-op when the input is already empty. Does not touch committed\n * chips.\n */\n async clearInput(): Promise<void> {\n await (await this.textInput()).sendKeys(TestKey.ESCAPE);\n }\n\n /** Moves focus to the text input. */\n async focus(): Promise<void> {\n await (await this.textInput()).focus();\n }\n\n /**\n * Moves focus out of the control. This is the gesture that marks the control\n * touched and, with `addOnBlur`, commits the pending text.\n */\n async blur(): Promise<void> {\n await (await this.textInput()).blur();\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -1,11 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { input, output, inject, ElementRef, DestroyRef, viewChild, viewChildren, signal, computed, effect, untracked, afterNextRender, isDevMode, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { NgControl, NgForm, FormGroupDirective, Validators } from '@angular/forms';
3
+ import { NgControl } from '@angular/forms';
4
4
  import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
5
+ import { Directionality } from '@angular/cdk/bidi';
5
6
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
- import { isObservable, merge } from 'rxjs';
7
+ import { isObservable } from 'rxjs';
7
8
  import { tv } from 'tailwind-variants';
8
- import { TW_ERROR_STATE_MATCHER } from '@cdevhub/ngx-tw/core';
9
+ import { wireErrorState } from '@cdevhub/ngx-tw/core';
9
10
  import { FormFieldControl, TW_FORM_FIELD, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
10
11
  import { BadgeComponent } from '@cdevhub/ngx-tw/badge';
11
12
 
@@ -68,12 +69,43 @@ const tagsInputVariants = tv({
68
69
  },
69
70
  },
70
71
  compoundVariants: [
71
- // Standalone density × size — inline padding scale (mirrors combobox trigger).
72
- { inFormField: false, size: 'xs', class: { root: 'px-2 py-1' } },
73
- { inFormField: false, size: 'sm', class: { root: 'px-3 py-1.5' } },
74
- { inFormField: false, size: 'md', class: { root: 'px-4 py-2' } },
75
- { inFormField: false, size: 'lg', class: { root: 'px-5 py-2.5' } },
76
- { inFormField: false, size: 'xl', class: { root: 'px-6 py-3' } },
72
+ // Standalone density × size — inline padding scale (mirrors combobox trigger)
73
+ // plus the control-height FLOOR. Per docs/vertical-rhythm.md §2 the chip
74
+ // strip is a `min-h` control, not a pinned one: chips wrap, so `py-*` stays
75
+ // exactly as it is and `min-h-*` only guarantees the box never sits below
76
+ // the control scale. Standalone-only, alongside the padding: inside a
77
+ // `tw-form-field` the root drops its border and padding and the field owns
78
+ // the box, so a floor here would stack on top of the field's own padding.
79
+ // Vertical padding is set so the `min-h-*` floor actually BINDS at rest.
80
+ //
81
+ // The tallest resting content is a CHIP, not the bare input line box. A
82
+ // chip is a badge: its own padding and text size make it taller than the
83
+ // input beside it at every size, so the box arithmetic that decides
84
+ // whether the floor binds is
85
+ //
86
+ // chip + 2*py + 2px border <= floor
87
+ //
88
+ // Sizing the padding against the input line box instead left the floor
89
+ // inert the moment a single tag existed. Empty, the control measured
90
+ // correctly at all five sizes; populated, it measured 26 / 32 / 38 / 50 /
91
+ // 54 against a 24 / 32 / 36 / 44 / 48 scale — so a filter bar was on the
92
+ // grid until the user typed the first tag, then jumped 2px at xs and md
93
+ // and 6px at lg and xl. Only sm was ever correct, which is why the
94
+ // isolated slot reading looked like a one-off rather than a broken rule.
95
+ //
96
+ // Resting appearance does not change: once the floor binds, `items-center`
97
+ // centres the chip and the padding is no longer what sets the height. The
98
+ // padding still governs the multi-line case, where chips wrap and the box
99
+ // legitimately grows past the floor.
100
+ //
101
+ // Chip heights are 20 / 20 / 24 / 32 / 32, so the padding ceiling is
102
+ // 1 / 5 / 5 / 5 / 7px. `py-px` at xs is not decorative restraint: a 20px
103
+ // chip inside a 24px control with a 1px border leaves exactly 1px a side.
104
+ { inFormField: false, size: 'xs', class: { root: 'px-2 py-px min-h-6' } },
105
+ { inFormField: false, size: 'sm', class: { root: 'px-3 py-1 min-h-8' } },
106
+ { inFormField: false, size: 'md', class: { root: 'px-4 py-1 min-h-9' } },
107
+ { inFormField: false, size: 'lg', class: { root: 'px-5 py-1 min-h-11' } },
108
+ { inFormField: false, size: 'xl', class: { root: 'px-6 py-1.5 min-h-12' } },
77
109
  // Standalone error state — colored border + focus ring.
78
110
  {
79
111
  inFormField: false,
@@ -115,7 +147,7 @@ class TagsInputComponent extends FormFieldControl {
115
147
  disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled' });
116
148
  /** Marks the control as required. Mirrored to the input's `aria-required`. Also inferred from `Validators.required` on a bound control. Defaults to `false`. */
117
149
  requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required' });
118
- /** Placeholder shown in the text input only while there are no chips and the input is empty. */
150
+ /** Placeholder shown in the text input only while there are no chips and the input is empty. Defaults to `undefined`. */
119
151
  placeholder = input(undefined, /* @ts-ignore */
120
152
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
121
153
  /** Keys that commit the in-progress text as a tag. Each entry is a `KeyboardEvent.key` value (`'Enter'`) or a single separator character (`','`). Single-character separators also split pasted text. Defaults to `['Enter', ',']`. */
@@ -139,21 +171,32 @@ class TagsInputComponent extends FormFieldControl {
139
171
  /** Equality comparator used for dedup when `allowDuplicates` is false. Defaults to `Object.is` (reference / value identity). String tags dedupe case-sensitively by default; pass `(a, b) => a.toLowerCase() === b.toLowerCase()` for case-insensitive dedup. */
140
172
  compareWith = input((a, b) => Object.is(a, b), /* @ts-ignore */
141
173
  ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
142
- /** Applied to the text input for labeling and identification only; does not submit the tag array via native (non-Angular) form posting. */
174
+ /** Applied to the text input for labeling and identification only; does not submit the tag array via native (non-Angular) form posting. Defaults to `undefined`. */
143
175
  name = input(undefined, /* @ts-ignore */
144
176
  ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
145
177
  /** Id on the host element. Auto-generated as `tw-tags-input-N` when not provided. Used by the form-field's `<label for>` association. */
146
178
  idInput = input(undefined, { ...(ngDevMode ? { debugName: "idInput" } : /* istanbul ignore next */ {}), alias: 'id' });
147
- /** Accessible name applied to the control when no visible label is wired. Mirrored to `aria-label`. */
179
+ /** Accessible name applied to the control when no visible label is wired. Mirrored to `aria-label`. Defaults to `undefined`. */
148
180
  ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
149
- /** ID of an external element that labels the control. Mirrored to `aria-labelledby`. */
181
+ /** ID of an external element that labels the control. Mirrored to `aria-labelledby`. Defaults to `undefined`. */
150
182
  ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
151
- /** ID of an external element that describes the control. Form-field merges its hint / error ids alongside. */
183
+ /** ID of an external element that describes the control. Form-field merges its hint / error ids alongside. Defaults to `undefined`. Alias: `aria-describedby`. */
152
184
  ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
153
185
  /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, uses the `TW_ERROR_STATE_MATCHER` token's value. */
154
186
  errorStateMatcher = input(undefined, /* @ts-ignore */
155
187
  ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
156
- /** Fires when the tag array changes through user interaction (add, remove, clear). Emits a fresh array reference. Does not fire on `writeValue`. */
188
+ /**
189
+ * Fires when the tag array changes through user interaction (add, remove,
190
+ * clear). Emits a fresh array reference. Does **not** fire on `writeValue` /
191
+ * `FormControl.setValue` / `ngModel`.
192
+ *
193
+ * This is a hand-written, user-gesture-only output. The control exposes no
194
+ * `value` input and therefore no two-way binding, so — unlike
195
+ * `tw-checkbox` / `tw-switch` / `tw-radio-group`, where `(valueChange)` /
196
+ * `(checkedChange)` is minted by a `model()` and fires on programmatic writes
197
+ * too — there is no any-change channel here. Observe programmatic writes
198
+ * through the bound form control's own `valueChanges`.
199
+ */
157
200
  valueChange = output();
158
201
  /** Fires when a tag is committed via Enter, a separator key, paste, or `addTag()`. Does not fire for dropped duplicates, blocked-by-max commits, empty commits, or `writeValue`. */
159
202
  tagAdded = output();
@@ -164,10 +207,14 @@ class TagsInputComponent extends FormFieldControl {
164
207
  elementRef = inject((ElementRef));
165
208
  destroyRef = inject(DestroyRef);
166
209
  ngControl = inject(NgControl, { optional: true, self: true });
167
- parentForm = inject(NgForm, { optional: true });
168
- parentFormGroup = inject(FormGroupDirective, { optional: true });
169
- defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
170
210
  formField = inject(TW_FORM_FIELD, { optional: true });
211
+ /**
212
+ * Optional so a consumer that never imports `BidiModule` still gets a working
213
+ * control — chip navigation falls back to LTR when the token is absent. Read
214
+ * imperatively inside the keydown handlers; nothing here needs to re-render
215
+ * on a direction change.
216
+ */
217
+ directionality = inject(Directionality, { optional: true });
171
218
  textInputRef = viewChild('textInput', /* @ts-ignore */
172
219
  ...(ngDevMode ? [{ debugName: "textInputRef" }] : /* istanbul ignore next */ []));
173
220
  removeButtons = viewChildren('removeBtn', /* @ts-ignore */
@@ -186,10 +233,13 @@ class TagsInputComponent extends FormFieldControl {
186
233
  ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
187
234
  _focused = signal(false, /* @ts-ignore */
188
235
  ...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
189
- _ngControlRev = signal(0, /* @ts-ignore */
190
- ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
191
- _formSubmitRev = signal(0, /* @ts-ignore */
192
- ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
236
+ /** @internal Shared `errorState` / `required` / `errors` derivation — see `wireErrorState`. */
237
+ errorWiring = wireErrorState({
238
+ ngControl: () => this.ngControl,
239
+ matcher: () => this.errorStateMatcher(),
240
+ required: () => this.requiredInput(),
241
+ track: [() => this._focused()],
242
+ });
193
243
  describedByIdsSignal = signal([], /* @ts-ignore */
194
244
  ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
195
245
  labelledByIdsSignal = signal([], /* @ts-ignore */
@@ -216,30 +266,11 @@ class TagsInputComponent extends FormFieldControl {
216
266
  disabled = computed(() => this.disabledInput() || this.cvaDisabled(), /* @ts-ignore */
217
267
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
218
268
  /** @internal */
219
- required = computed(() => {
220
- this._ngControlRev();
221
- if (this.requiredInput())
222
- return true;
223
- return !!this.ngControl?.control?.hasValidator(Validators.required);
224
- }, /* @ts-ignore */
225
- ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
269
+ required = this.errorWiring.required;
226
270
  /** @internal */
227
- errorState = computed(() => {
228
- this._ngControlRev();
229
- this._formSubmitRev();
230
- this._focused();
231
- const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
232
- const form = this.parentFormGroup ??
233
- this.parentForm;
234
- return matcher.isErrorState(this.ngControl?.control ?? null, form);
235
- }, /* @ts-ignore */
236
- ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
271
+ errorState = this.errorWiring.errorState;
237
272
  /** @internal Active validation errors map from the bound control, for `[twError match="…"]` filtering. */
238
- errors = computed(() => {
239
- this._ngControlRev();
240
- return this.ngControl?.control?.errors ?? null;
241
- }, /* @ts-ignore */
242
- ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
273
+ errors = this.errorWiring.errors;
243
274
  /** @internal */
244
275
  controlType = 'tags-input';
245
276
  /** @internal */
@@ -470,7 +501,13 @@ class TagsInputComponent extends FormFieldControl {
470
501
  }
471
502
  return;
472
503
  }
473
- if (key === 'ArrowLeft') {
504
+ // Stepping out of the input into the last chip is a *backwards* move: the
505
+ // chip row is rendered before the input, so it is ArrowLeft in LTR and
506
+ // ArrowRight in an RTL locale, where the row lays out right-to-left. The
507
+ // caret test is direction-independent — `selectionStart === 0` is the
508
+ // logical start of the text in both directions.
509
+ const backKey = this.directionality?.value === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
510
+ if (key === backKey) {
474
511
  const el = event.target;
475
512
  const atStart = (el.selectionStart ?? 0) === 0 && (el.selectionEnd ?? 0) === 0;
476
513
  if (atStart && this.tags().length > 0) {
@@ -498,17 +535,26 @@ class TagsInputComponent extends FormFieldControl {
498
535
  const last = this.tags().length - 1;
499
536
  switch (event.key) {
500
537
  case 'ArrowLeft':
538
+ case 'ArrowRight': {
539
+ // Horizontal arrows follow the layout direction. Chips sit on a
540
+ // flex row that lays out right-to-left in an RTL locale, so ArrowLeft
541
+ // is the *forward* key there. The chip host is a custom element, not a
542
+ // native control, so the browser does no flipping for us. Home/End stay
543
+ // logical — CDK's ListKeyManager does not flip them either.
501
544
  event.preventDefault();
502
- if (index > 0)
545
+ const rtl = this.directionality?.value === 'rtl';
546
+ const forward = rtl ? event.key === 'ArrowLeft' : event.key === 'ArrowRight';
547
+ if (forward) {
548
+ if (index < last)
549
+ this.focusChip(index + 1);
550
+ else
551
+ this.focusInput();
552
+ }
553
+ else if (index > 0) {
503
554
  this.focusChip(index - 1);
555
+ }
504
556
  return;
505
- case 'ArrowRight':
506
- event.preventDefault();
507
- if (index < last)
508
- this.focusChip(index + 1);
509
- else
510
- this.focusInput();
511
- return;
557
+ }
512
558
  case 'Home':
513
559
  event.preventDefault();
514
560
  this.focusChip(0);
@@ -605,28 +651,14 @@ class TagsInputComponent extends FormFieldControl {
605
651
  this.commitInput();
606
652
  }
607
653
  this.onTouched();
608
- this._ngControlRev.update((n) => n + 1);
654
+ this.errorWiring.bump();
609
655
  // Reset roving so the next Tab into the control lands on the input.
610
656
  this.activeChipIndex.set(null);
611
657
  }
612
658
  });
613
659
  }
614
660
  this.destroyRef.onDestroy(() => this.focusMonitor.stopMonitoring(this.elementRef));
615
- const ctrl = this.ngControl?.control;
616
- if (ctrl) {
617
- const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
618
- if (streams.length) {
619
- merge(...streams)
620
- .pipe(takeUntilDestroyed(this.destroyRef))
621
- .subscribe(() => this._ngControlRev.update((n) => n + 1));
622
- }
623
- }
624
- const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
625
- if (submit) {
626
- submit
627
- .pipe(takeUntilDestroyed(this.destroyRef))
628
- .subscribe(() => this._formSubmitRev.update((n) => n + 1));
629
- }
661
+ this.errorWiring.connect();
630
662
  }
631
663
  // ── Helpers ──
632
664
  focusChip(index) {
@@ -667,7 +699,7 @@ class TagsInputComponent extends FormFieldControl {
667
699
  this.labelledByIdsSignal().length > 0);
668
700
  }
669
701
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TagsInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
670
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: TagsInputComponent, isStandalone: true, selector: "tw-tags-input", inputs: { color: { classPropertyName: "color", publicName: "color", 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 }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, separatorKeys: { classPropertyName: "separatorKeys", publicName: "separatorKeys", isSignal: true, isRequired: false, transformFunction: null }, addOnBlur: { classPropertyName: "addOnBlur", publicName: "addOnBlur", isSignal: true, isRequired: false, transformFunction: null }, maxTags: { classPropertyName: "maxTags", publicName: "maxTags", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, createTag: { classPropertyName: "createTag", publicName: "createTag", isSignal: true, isRequired: false, transformFunction: null }, tagLabel: { classPropertyName: "tagLabel", publicName: "tagLabel", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", 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", tagAdded: "tagAdded", tagRemoved: "tagRemoved" }, 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: [
702
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: TagsInputComponent, isStandalone: true, selector: "tw-tags-input", inputs: { color: { classPropertyName: "color", publicName: "color", 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 }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, separatorKeys: { classPropertyName: "separatorKeys", publicName: "separatorKeys", isSignal: true, isRequired: false, transformFunction: null }, addOnBlur: { classPropertyName: "addOnBlur", publicName: "addOnBlur", isSignal: true, isRequired: false, transformFunction: null }, maxTags: { classPropertyName: "maxTags", publicName: "maxTags", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, createTag: { classPropertyName: "createTag", publicName: "createTag", isSignal: true, isRequired: false, transformFunction: null }, tagLabel: { classPropertyName: "tagLabel", publicName: "tagLabel", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", 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", tagAdded: "tagAdded", tagRemoved: "tagRemoved" }, 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: [
671
703
  {
672
704
  provide: TW_FORM_FIELD_CONTROL,
673
705
  useExisting: forwardRef(() => TagsInputComponent),
@@ -784,7 +816,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
784
816
  '[attr.aria-labelledby]': 'resolvedLabelledBy() || null',
785
817
  '[attr.aria-describedby]': 'resolvedDescribedBy() || null',
786
818
  '[attr.aria-disabled]': 'disabled() || null',
787
- '[attr.aria-invalid]': 'errorState() || null',
819
+ // NOTE: no `aria-invalid` here. ARIA 1.2 does not allow it on
820
+ // `role="group"` (axe: critical `aria-allowed-attr`). The inner
821
+ // `<input type="text">` already carries it — that is the control with the
822
+ // value. Latent rather than observed: it only fires once a consumer puts
823
+ // the control into an error state.
788
824
  '[attr.data-focused]': 'focused() || null',
789
825
  },
790
826
  }]