@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.
- package/README.md +78 -15
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs +16 -5
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs +26 -15
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-badge.mjs +17 -5
- package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +27 -6
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-button.mjs +23 -11
- package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +5 -5
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +3 -3
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs +427 -122
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-card.mjs +21 -11
- package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs +215 -96
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +108 -64
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs +18 -6
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +55 -30
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs +218 -0
- package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs +236 -211
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs +200 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +34 -7
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-core.mjs +135 -15
- package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs +203 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +160 -102
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs +209 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +182 -107
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
- package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs +11 -2
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
- package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
- package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
- package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
- package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-item.mjs +18 -3
- package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
- package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
- package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
- package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
- package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
- package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
- package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
- package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
- package/fesm2022/{cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs → cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs} +3 -3
- package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
- package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs +23 -7
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
- package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
- package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
- package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
- package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
- package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
- package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
- package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
- package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
- package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
- package/fesm2022/{cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs → cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs} +107 -19
- package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs +21 -13
- package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
- package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
- package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
- package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
- package/index.json +1 -1
- package/package.json +63 -2
- package/theme/_base.css +13 -0
- package/theme/_dark.css +110 -36
- package/theme/_high-contrast-dark.css +367 -0
- package/theme/_high-contrast.css +13 -2
- package/theme/_light.css +276 -0
- package/theme/_semantic.css +47 -12
- package/theme/_typography.css +17 -0
- package/theme/index.css +86 -7
- package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
- package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
- package/types/cdevhub-ngx-tw-button.d.ts +1 -0
- package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
- package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
- package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
- package/types/cdevhub-ngx-tw-card.d.ts +11 -3
- package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
- package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
- package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
- package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
- package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
- package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
- package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
- package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
- package/types/cdevhub-ngx-tw-core.d.ts +152 -21
- package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
- package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
- package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
- package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
- package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
- package/types/cdevhub-ngx-tw-dialog.d.ts +12 -3
- package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
- package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
- package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
- package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
- package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
- package/types/cdevhub-ngx-tw-input.d.ts +5 -8
- package/types/cdevhub-ngx-tw-item.d.ts +2 -0
- package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
- package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
- package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
- package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
- package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
- package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
- package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
- package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
- package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
- package/types/cdevhub-ngx-tw-select.d.ts +104 -33
- package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
- package/types/cdevhub-ngx-tw-sheet.d.ts +19 -3
- package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
- package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
- package/types/cdevhub-ngx-tw-split.d.ts +32 -2
- package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
- package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
- package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
- package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
- package/types/cdevhub-ngx-tw-table.d.ts +53 -10
- package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
- package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
- package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
- package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
- package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
- package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
- package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
- package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
- package/types/cdevhub-ngx-tw-toast.d.ts +20 -6
- package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
- package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
- package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
- package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
- package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs.map +0 -1
- 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
|
|
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
|
|
7
|
+
import { isObservable } from 'rxjs';
|
|
7
8
|
import { tv } from 'tailwind-variants';
|
|
8
|
-
import {
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
}]
|