@cdevhub/ngx-tw 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +28 -4
- 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 +111 -36
- package/theme/_high-contrast-dark.css +367 -0
- package/theme/_high-contrast.css +13 -2
- package/theme/_light.css +332 -0
- package/theme/_semantic.css +60 -26
- 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,149 @@
|
|
|
1
|
+
import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Harness for a single attached file in a `tw-file-upload`'s list.
|
|
5
|
+
*
|
|
6
|
+
* Reach these through {@link FileUploadHarness.getFiles}.
|
|
7
|
+
*
|
|
8
|
+
* Everything here reads the *default* row rendering. A consumer who projects a
|
|
9
|
+
* `*twFileUploadItem` template replaces the row wholesale — including the
|
|
10
|
+
* remove control this harness drives — so {@link getName} and {@link remove}
|
|
11
|
+
* apply only to the built-in row.
|
|
12
|
+
*/
|
|
13
|
+
class FileUploadFileHarness extends ComponentHarness {
|
|
14
|
+
static hostSelector = 'ul[role="list"] > li';
|
|
15
|
+
removeButton = this.locatorFor('[data-tw-file-upload-remove]');
|
|
16
|
+
/** Predicate for `locatorFor` / `locatorForAll`. */
|
|
17
|
+
static with(options = {}) {
|
|
18
|
+
return new HarnessPredicate(FileUploadFileHarness, options).addOption('name', options.name, async (h, name) => HarnessPredicate.stringMatches(await h.getName(), name));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The attached file's name, read from the remove control's accessible name
|
|
22
|
+
* (`Remove <name>`). That is the one place the name is exposed through an
|
|
23
|
+
* accessibility contract rather than through presentational markup.
|
|
24
|
+
*/
|
|
25
|
+
async getName() {
|
|
26
|
+
const label = (await (await this.removeButton()).getAttribute('aria-label')) ?? '';
|
|
27
|
+
return label.replace(/^Remove /, '');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The row's full rendered text, whitespace-collapsed.
|
|
31
|
+
*
|
|
32
|
+
* The name and the meta line sit in adjacent `<span>`s with only a
|
|
33
|
+
* whitespace-only text node between them, which Angular strips, so they run
|
|
34
|
+
* together here (`notes.txt10 B`). Prefer {@link getName} and
|
|
35
|
+
* {@link getMetaText} for the parts; this is the raw read that still says
|
|
36
|
+
* something about a row rendered from a projected template.
|
|
37
|
+
*/
|
|
38
|
+
async getText() {
|
|
39
|
+
return (await (await this.host()).text()).replace(/\s+/g, ' ').trim();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The row's meta line — the formatted size, plus the status and error text
|
|
43
|
+
* once the item leaves `pending` (`10 B`, `10 B · Uploading`,
|
|
44
|
+
* `10 B · Failed — <error>`).
|
|
45
|
+
*
|
|
46
|
+
* This string is the only place per-item status and error surface in the
|
|
47
|
+
* DOM: there is no `data-status` hook, and the status otherwise shows up
|
|
48
|
+
* only in Tailwind classes, which a harness must not read. Derived by
|
|
49
|
+
* subtracting {@link getName} from {@link getText} for the same reason.
|
|
50
|
+
*/
|
|
51
|
+
async getMetaText() {
|
|
52
|
+
const text = await this.getText();
|
|
53
|
+
const name = await this.getName();
|
|
54
|
+
return text.startsWith(name) ? text.slice(name.length).trim() : text;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Clicks the row's remove control.
|
|
58
|
+
*
|
|
59
|
+
* The component retargets focus in an `afterNextRender` — deliberately, so
|
|
60
|
+
* that the list has re-rendered before focus moves — so the focus assertion
|
|
61
|
+
* a caller makes after this resolves is the post-render one.
|
|
62
|
+
*/
|
|
63
|
+
async remove() {
|
|
64
|
+
await (await this.removeButton()).click();
|
|
65
|
+
await this.waitForTasksOutsideAngular();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Harness for `tw-file-upload`.
|
|
71
|
+
*
|
|
72
|
+
* ## What this harness deliberately cannot do
|
|
73
|
+
*
|
|
74
|
+
* It cannot attach a file. Selecting a file means populating
|
|
75
|
+
* `HTMLInputElement.files`, which is read-only to script and which CDK's
|
|
76
|
+
* `TestElement` — the whole reason a harness runs unchanged against a real
|
|
77
|
+
* browser — has no operation for. The only way to do it is
|
|
78
|
+
* `Object.defineProperty` on the raw DOM node, which exists solely in the
|
|
79
|
+
* Testbed environment. Exposing that would make the harness silently
|
|
80
|
+
* unusable in every other environment, so attaching stays the caller's job:
|
|
81
|
+
* seed files through the bound form control (`writeValue` takes a `File[]`),
|
|
82
|
+
* or through the component's own API, and use this harness for everything
|
|
83
|
+
* downstream of the attachment.
|
|
84
|
+
*
|
|
85
|
+
* Nothing here needs a CDK overlay, so the ordinary fixture loader is enough.
|
|
86
|
+
*
|
|
87
|
+
* Note where the ARIA lives: the host is a `role="group"`, which ARIA 1.2 does
|
|
88
|
+
* not allow to carry `aria-required` / `aria-invalid`, so both sit on the
|
|
89
|
+
* hidden `<input type="file">` — the element that actually owns the value.
|
|
90
|
+
* {@link isRequired} and {@link isInvalid} read them from there;
|
|
91
|
+
* {@link isDisabled} reads `aria-disabled` from the group, which is allowed.
|
|
92
|
+
*/
|
|
93
|
+
class FileUploadHarness extends ComponentHarness {
|
|
94
|
+
static hostSelector = 'tw-file-upload';
|
|
95
|
+
/** The hidden native control. It carries the value-owning ARIA and state. */
|
|
96
|
+
nativeInput = this.locatorFor('input[type="file"]');
|
|
97
|
+
/** Predicate for `locatorFor` / `locatorForAll`. */
|
|
98
|
+
static with(options = {}) {
|
|
99
|
+
return new HarnessPredicate(FileUploadHarness, options)
|
|
100
|
+
.addOption('label', options.label, async (h, label) => HarnessPredicate.stringMatches(await h.getLabel(), label))
|
|
101
|
+
.addOption('disabled', options.disabled, async (h, disabled) => (await h.isDisabled()) === disabled);
|
|
102
|
+
}
|
|
103
|
+
/** The control's accessible name, from `aria-label` when one is set. */
|
|
104
|
+
async getLabel() {
|
|
105
|
+
return (await this.host()).getAttribute('aria-label');
|
|
106
|
+
}
|
|
107
|
+
/** Whether the group reports `aria-disabled="true"`. */
|
|
108
|
+
async isDisabled() {
|
|
109
|
+
return (await (await this.host()).getAttribute('aria-disabled')) === 'true';
|
|
110
|
+
}
|
|
111
|
+
/** Whether the native file input is marked required. */
|
|
112
|
+
async isRequired() {
|
|
113
|
+
return (await this.nativeInput()).getProperty('required');
|
|
114
|
+
}
|
|
115
|
+
/** Whether the native file input reports `aria-invalid="true"`. */
|
|
116
|
+
async isInvalid() {
|
|
117
|
+
return (await (await this.nativeInput()).getAttribute('aria-invalid')) === 'true';
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Every attached file, in list order. Returns `[]` when nothing is attached
|
|
121
|
+
* — the list element is always rendered, just empty.
|
|
122
|
+
*/
|
|
123
|
+
async getFiles(filters = {}) {
|
|
124
|
+
return this.locatorForAll(FileUploadFileHarness.with(filters))();
|
|
125
|
+
}
|
|
126
|
+
/** The name of every attached file, in list order. */
|
|
127
|
+
async getFileNames() {
|
|
128
|
+
const files = await this.getFiles();
|
|
129
|
+
return Promise.all(files.map((file) => file.getName()));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Removes the first attached file whose name matches. Throws when nothing
|
|
133
|
+
* matches, rather than failing silently.
|
|
134
|
+
*/
|
|
135
|
+
async removeFile(name) {
|
|
136
|
+
const matches = await this.getFiles({ name });
|
|
137
|
+
if (matches.length === 0) {
|
|
138
|
+
throw new Error(`FileUploadHarness.removeFile: no attached file matching ${String(name)}.`);
|
|
139
|
+
}
|
|
140
|
+
await matches[0].remove();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generated bundle index. Do not edit.
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
export { FileUploadFileHarness, FileUploadHarness };
|
|
149
|
+
//# sourceMappingURL=cdevhub-ngx-tw-file-upload-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdevhub-ngx-tw-file-upload-testing.mjs","sources":["../../../projects/ngx-tw/file-upload/testing/file-upload-file-harness.ts","../../../projects/ngx-tw/file-upload/testing/file-upload-harness.ts","../../../projects/ngx-tw/file-upload/testing/cdevhub-ngx-tw-file-upload-testing.ts"],"sourcesContent":["import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\nimport type { BaseHarnessFilters } from '@angular/cdk/testing';\n\n/** Filters accepted by `FileUploadFileHarness.with`. */\nexport interface FileUploadFileHarnessFilters extends BaseHarnessFilters {\n /** Match by the attached file's name. */\n name?: string | RegExp;\n}\n\n/**\n * Harness for a single attached file in a `tw-file-upload`'s list.\n *\n * Reach these through {@link FileUploadHarness.getFiles}.\n *\n * Everything here reads the *default* row rendering. A consumer who projects a\n * `*twFileUploadItem` template replaces the row wholesale — including the\n * remove control this harness drives — so {@link getName} and {@link remove}\n * apply only to the built-in row.\n */\nexport class FileUploadFileHarness extends ComponentHarness {\n static hostSelector = 'ul[role=\"list\"] > li';\n\n private readonly removeButton = this.locatorFor('[data-tw-file-upload-remove]');\n\n /** Predicate for `locatorFor` / `locatorForAll`. */\n static with(\n options: FileUploadFileHarnessFilters = {},\n ): HarnessPredicate<FileUploadFileHarness> {\n return new HarnessPredicate(FileUploadFileHarness, options).addOption(\n 'name',\n options.name,\n async (h, name) => HarnessPredicate.stringMatches(await h.getName(), name),\n );\n }\n\n /**\n * The attached file's name, read from the remove control's accessible name\n * (`Remove <name>`). That is the one place the name is exposed through an\n * accessibility contract rather than through presentational markup.\n */\n async getName(): Promise<string> {\n const label = (await (await this.removeButton()).getAttribute('aria-label')) ?? '';\n return label.replace(/^Remove /, '');\n }\n\n /**\n * The row's full rendered text, whitespace-collapsed.\n *\n * The name and the meta line sit in adjacent `<span>`s with only a\n * whitespace-only text node between them, which Angular strips, so they run\n * together here (`notes.txt10 B`). Prefer {@link getName} and\n * {@link getMetaText} for the parts; this is the raw read that still says\n * something about a row rendered from a projected template.\n */\n async getText(): Promise<string> {\n return (await (await this.host()).text()).replace(/\\s+/g, ' ').trim();\n }\n\n /**\n * The row's meta line — the formatted size, plus the status and error text\n * once the item leaves `pending` (`10 B`, `10 B · Uploading`,\n * `10 B · Failed — <error>`).\n *\n * This string is the only place per-item status and error surface in the\n * DOM: there is no `data-status` hook, and the status otherwise shows up\n * only in Tailwind classes, which a harness must not read. Derived by\n * subtracting {@link getName} from {@link getText} for the same reason.\n */\n async getMetaText(): Promise<string> {\n const text = await this.getText();\n const name = await this.getName();\n return text.startsWith(name) ? text.slice(name.length).trim() : text;\n }\n\n /**\n * Clicks the row's remove control.\n *\n * The component retargets focus in an `afterNextRender` — deliberately, so\n * that the list has re-rendered before focus moves — so the focus assertion\n * a caller makes after this resolves is the post-render one.\n */\n async remove(): Promise<void> {\n await (await this.removeButton()).click();\n await this.waitForTasksOutsideAngular();\n }\n}\n","import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\nimport type { BaseHarnessFilters } from '@angular/cdk/testing';\nimport {\n FileUploadFileHarness,\n type FileUploadFileHarnessFilters,\n} from './file-upload-file-harness';\n\n/** Filters accepted by `FileUploadHarness.with`. */\nexport interface FileUploadHarnessFilters 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-file-upload`.\n *\n * ## What this harness deliberately cannot do\n *\n * It cannot attach a file. Selecting a file means populating\n * `HTMLInputElement.files`, which is read-only to script and which CDK's\n * `TestElement` — the whole reason a harness runs unchanged against a real\n * browser — has no operation for. The only way to do it is\n * `Object.defineProperty` on the raw DOM node, which exists solely in the\n * Testbed environment. Exposing that would make the harness silently\n * unusable in every other environment, so attaching stays the caller's job:\n * seed files through the bound form control (`writeValue` takes a `File[]`),\n * or through the component's own API, and use this harness for everything\n * downstream of the attachment.\n *\n * Nothing here needs a CDK overlay, so 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 both sit on the\n * hidden `<input type=\"file\">` — the element that actually owns the 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 FileUploadHarness extends ComponentHarness {\n static hostSelector = 'tw-file-upload';\n\n /** The hidden native control. It carries the value-owning ARIA and state. */\n private readonly nativeInput = this.locatorFor('input[type=\"file\"]');\n\n /** Predicate for `locatorFor` / `locatorForAll`. */\n static with(\n options: FileUploadHarnessFilters = {},\n ): HarnessPredicate<FileUploadHarness> {\n return new HarnessPredicate(FileUploadHarness, 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 native file input is marked required. */\n async isRequired(): Promise<boolean> {\n return (await this.nativeInput()).getProperty<boolean>('required');\n }\n\n /** Whether the native file input reports `aria-invalid=\"true\"`. */\n async isInvalid(): Promise<boolean> {\n return (await (await this.nativeInput()).getAttribute('aria-invalid')) === 'true';\n }\n\n /**\n * Every attached file, in list order. Returns `[]` when nothing is attached\n * — the list element is always rendered, just empty.\n */\n async getFiles(\n filters: FileUploadFileHarnessFilters = {},\n ): Promise<FileUploadFileHarness[]> {\n return this.locatorForAll(FileUploadFileHarness.with(filters))();\n }\n\n /** The name of every attached file, in list order. */\n async getFileNames(): Promise<string[]> {\n const files = await this.getFiles();\n return Promise.all(files.map((file) => file.getName()));\n }\n\n /**\n * Removes the first attached file whose name matches. Throws when nothing\n * matches, rather than failing silently.\n */\n async removeFile(name: string | RegExp): Promise<void> {\n const matches = await this.getFiles({ name });\n if (matches.length === 0) {\n throw new Error(\n `FileUploadHarness.removeFile: no attached file matching ${String(name)}.`,\n );\n }\n await matches[0].remove();\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, TemplateRef, Directive, input, booleanAttribute, output, ElementRef, DestroyRef, signal, contentChild, viewChild, computed, afterNextRender, isDevMode, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
2
|
+
import { inject, TemplateRef, Directive, input, booleanAttribute, output, ElementRef, Injector, DestroyRef, signal, contentChild, viewChild, computed, afterNextRender, isDevMode, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
3
|
import { NgTemplateOutlet } from '@angular/common';
|
|
4
|
-
import { NgControl
|
|
4
|
+
import { NgControl } from '@angular/forms';
|
|
5
5
|
import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
|
|
6
6
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
7
|
-
import { isObservable
|
|
7
|
+
import { isObservable } from 'rxjs';
|
|
8
8
|
import { tv } from 'tailwind-variants';
|
|
9
|
-
import {
|
|
9
|
+
import { wireErrorState } from '@cdevhub/ngx-tw/core';
|
|
10
10
|
import { FormFieldControl, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
|
|
11
11
|
import { ButtonDirective } from '@cdevhub/ngx-tw/button';
|
|
12
12
|
import { IconComponent } from '@cdevhub/ngx-tw/icon';
|
|
@@ -89,11 +89,18 @@ const fileUploadVariants = tv({
|
|
|
89
89
|
idle: {},
|
|
90
90
|
// 1% scale-up — below Tailwind's smallest `scale-95` step but visually
|
|
91
91
|
// correct for a subtle drag-over lift; the alternative is no lift.
|
|
92
|
+
// No `dark:` variant here, deliberately. The theme layer already
|
|
93
|
+
// inverts the ramp: `_dark.css` maps `--color-primary-50` onto
|
|
94
|
+
// `blue-950`, so `bg-primary-soft` IS the dark wash in dark mode. A
|
|
95
|
+
// `dark:bg-primary-900/20` override on top resolved to `blue-100` —
|
|
96
|
+
// near-white — and turned the drag-over affordance into a bright flash
|
|
97
|
+
// over a `gray-950` dropzone. These were the only two `dark:` utilities
|
|
98
|
+
// in the library, and both were wrong for this reason.
|
|
92
99
|
draggingValid: {
|
|
93
|
-
dropzone: 'border-primary-500 bg-primary-
|
|
100
|
+
dropzone: 'border-primary-500 bg-primary-soft scale-[1.01]',
|
|
94
101
|
},
|
|
95
102
|
draggingInvalid: {
|
|
96
|
-
dropzone: 'border-error-500 bg-error-
|
|
103
|
+
dropzone: 'border-error-500 bg-error-soft',
|
|
97
104
|
},
|
|
98
105
|
error: { dropzone: 'border-error-500' },
|
|
99
106
|
},
|
|
@@ -200,20 +207,20 @@ class FileUploadComponent extends FormFieldControl {
|
|
|
200
207
|
/** Marks the control as required. Mirrors to `aria-required`. Also inferred from `Validators.required` on a bound `NgControl`. Defaults to `false`. */
|
|
201
208
|
requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required',
|
|
202
209
|
transform: booleanAttribute });
|
|
203
|
-
/** Headline text rendered inside the drop zone (e.g., `'Drag files here'`). Projected `[twFileUploadHeadline]` content takes precedence over this input. */
|
|
210
|
+
/** Headline text rendered inside the drop zone (e.g., `'Drag files here'`). Projected `[twFileUploadHeadline]` content takes precedence over this input. Defaults to `undefined`. */
|
|
204
211
|
label = input(undefined, /* @ts-ignore */
|
|
205
212
|
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
206
|
-
/** Secondary text rendered under the label (e.g., `'PDF up to 10MB'`). Projected `[twFileUploadDescription]` content takes precedence. */
|
|
213
|
+
/** Secondary text rendered under the label (e.g., `'PDF up to 10MB'`). Projected `[twFileUploadDescription]` content takes precedence. Defaults to `undefined`. */
|
|
207
214
|
description = input(undefined, /* @ts-ignore */
|
|
208
215
|
...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
|
|
209
216
|
/** Label rendered inside the trigger button. Defaults to `'Choose files'`. Use `'Choose file'` when `multiple` is `false` (consumer responsibility). */
|
|
210
217
|
triggerLabel = input('Choose files', /* @ts-ignore */
|
|
211
218
|
...(ngDevMode ? [{ debugName: "triggerLabel" }] : /* istanbul ignore next */ []));
|
|
212
|
-
/** Accessible name applied to the host when no visible label is projected. Mirrored to `aria-label`. */
|
|
219
|
+
/** Accessible name applied to the host when no visible label is projected. Mirrored to `aria-label`. Defaults to `undefined`. */
|
|
213
220
|
ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
|
|
214
|
-
/** ID of an external element that labels the control. Mirrored to `aria-labelledby`. */
|
|
221
|
+
/** ID of an external element that labels the control. Mirrored to `aria-labelledby`. Defaults to `undefined`. */
|
|
215
222
|
ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
|
|
216
|
-
/** ID of an external element that describes the control. Form-field merges its hint/error ids alongside. */
|
|
223
|
+
/** ID of an external element that describes the control. Form-field merges its hint/error ids alongside. Defaults to `undefined`. Alias: `aria-describedby`. */
|
|
217
224
|
ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
|
|
218
225
|
/** Optional `name` attribute on the hidden `<input type="file">` for native form submissions. */
|
|
219
226
|
name = input(undefined, /* @ts-ignore */
|
|
@@ -234,13 +241,11 @@ class FileUploadComponent extends FormFieldControl {
|
|
|
234
241
|
cleared = output();
|
|
235
242
|
// ── Injections ──
|
|
236
243
|
elementRef = inject((ElementRef));
|
|
244
|
+
injector = inject(Injector);
|
|
237
245
|
focusMonitor = inject(FocusMonitor);
|
|
238
246
|
liveAnnouncer = inject(LiveAnnouncer);
|
|
239
247
|
destroyRef = inject(DestroyRef);
|
|
240
248
|
ngControl = inject(NgControl, { optional: true, self: true });
|
|
241
|
-
parentForm = inject(NgForm, { optional: true });
|
|
242
|
-
parentFormGroup = inject(FormGroupDirective, { optional: true });
|
|
243
|
-
defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
|
|
244
249
|
// ── Internal mutable state ──
|
|
245
250
|
itemsSignal = signal([], /* @ts-ignore */
|
|
246
251
|
...(ngDevMode ? [{ debugName: "itemsSignal" }] : /* istanbul ignore next */ []));
|
|
@@ -252,10 +257,13 @@ class FileUploadComponent extends FormFieldControl {
|
|
|
252
257
|
...(ngDevMode ? [{ debugName: "dragHasInvalidFilesSignal" }] : /* istanbul ignore next */ []));
|
|
253
258
|
_focused = signal(false, /* @ts-ignore */
|
|
254
259
|
...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
/** @internal Shared `errorState` / `required` / `errors` derivation — see `wireErrorState`. */
|
|
261
|
+
errorWiring = wireErrorState({
|
|
262
|
+
ngControl: () => this.ngControl,
|
|
263
|
+
matcher: () => this.errorStateMatcher(),
|
|
264
|
+
required: () => this.requiredInput(),
|
|
265
|
+
track: [() => this._focused()],
|
|
266
|
+
});
|
|
259
267
|
describedByIdsSignal = signal([], /* @ts-ignore */
|
|
260
268
|
...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
|
|
261
269
|
labelledByIdsSignal = signal([], /* @ts-ignore */
|
|
@@ -311,36 +319,16 @@ class FileUploadComponent extends FormFieldControl {
|
|
|
311
319
|
...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
|
|
312
320
|
/** @internal */
|
|
313
321
|
disabled = computed(() => {
|
|
314
|
-
this.
|
|
322
|
+
this.errorWiring.rev();
|
|
315
323
|
return this.disabledInput() || this.cvaDisabled() || !!this.ngControl?.disabled;
|
|
316
324
|
}, /* @ts-ignore */
|
|
317
325
|
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
318
326
|
/** @internal */
|
|
319
|
-
required =
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return true;
|
|
323
|
-
const ctrl = this.ngControl?.control;
|
|
324
|
-
return !!ctrl?.hasValidator(Validators.required);
|
|
325
|
-
}, /* @ts-ignore */
|
|
326
|
-
...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
327
|
-
/** @internal */
|
|
328
|
-
errorState = computed(() => {
|
|
329
|
-
this._ngControlRev();
|
|
330
|
-
this._formSubmitRev();
|
|
331
|
-
this._focused();
|
|
332
|
-
const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
|
|
333
|
-
const form = this.parentFormGroup ??
|
|
334
|
-
this.parentForm;
|
|
335
|
-
return matcher.isErrorState(this.ngControl?.control ?? null, form);
|
|
336
|
-
}, /* @ts-ignore */
|
|
337
|
-
...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
|
|
327
|
+
required = this.errorWiring.required;
|
|
328
|
+
/** @internal */
|
|
329
|
+
errorState = this.errorWiring.errorState;
|
|
338
330
|
/** @internal Validation errors map from the bound `NgControl`. */
|
|
339
|
-
errors =
|
|
340
|
-
this._ngControlRev();
|
|
341
|
-
return this.ngControl?.control?.errors ?? null;
|
|
342
|
-
}, /* @ts-ignore */
|
|
343
|
-
...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
331
|
+
errors = this.errorWiring.errors;
|
|
344
332
|
/** @internal */
|
|
345
333
|
controlType = 'file-upload';
|
|
346
334
|
/** @internal */
|
|
@@ -491,8 +479,21 @@ class FileUploadComponent extends FormFieldControl {
|
|
|
491
479
|
this.emitChange();
|
|
492
480
|
this.fileRemoved.emit(removed);
|
|
493
481
|
this.liveAnnouncer.announce(`${removed.file.name} removed.`, 'polite');
|
|
494
|
-
// Move focus to the next remove button, or to the trigger button when none
|
|
495
|
-
|
|
482
|
+
// Move focus to the next remove button, or to the trigger button when none
|
|
483
|
+
// remain.
|
|
484
|
+
//
|
|
485
|
+
// This MUST wait for the render, not merely for the microtask queue. A
|
|
486
|
+
// `queueMicrotask` here (what this used to be) runs before Angular's
|
|
487
|
+
// zoneless scheduler has re-rendered the list, so `nextButtons` was still
|
|
488
|
+
// the PRE-removal node list: the "next" button we focused was the one about
|
|
489
|
+
// to be destroyed, and focus fell to `<body>`. Removing the last item took
|
|
490
|
+
// the `length === 0` branch for the same reason — the count was stale, so
|
|
491
|
+
// the trigger never got focus either.
|
|
492
|
+
//
|
|
493
|
+
// It survived unit testing because a spec that calls `detectChanges()`
|
|
494
|
+
// between the click and the assertion has already re-rendered by the time
|
|
495
|
+
// the microtask runs, which is the opposite order from a real browser.
|
|
496
|
+
afterNextRender(() => {
|
|
496
497
|
const host = this.elementRef.nativeElement;
|
|
497
498
|
const nextButtons = host.querySelectorAll('[data-tw-file-upload-remove]');
|
|
498
499
|
if (nextButtons.length === 0) {
|
|
@@ -502,7 +503,7 @@ class FileUploadComponent extends FormFieldControl {
|
|
|
502
503
|
const fallback = nextButtons[Math.min(idx, nextButtons.length - 1)];
|
|
503
504
|
fallback?.focus();
|
|
504
505
|
}
|
|
505
|
-
});
|
|
506
|
+
}, { injector: this.injector });
|
|
506
507
|
}
|
|
507
508
|
/** Removes all items. Emits `cleared` and calls `onChange([])`. No-op when already empty or disabled. */
|
|
508
509
|
clear() {
|
|
@@ -839,28 +840,14 @@ class FileUploadComponent extends FormFieldControl {
|
|
|
839
840
|
this._focused.set(!!origin);
|
|
840
841
|
if (wasFocused && !origin) {
|
|
841
842
|
this.onTouched();
|
|
842
|
-
this.
|
|
843
|
+
this.errorWiring.bump();
|
|
843
844
|
}
|
|
844
845
|
});
|
|
845
846
|
}
|
|
846
847
|
this.destroyRef.onDestroy(() => {
|
|
847
848
|
this.focusMonitor.stopMonitoring(this.elementRef);
|
|
848
849
|
});
|
|
849
|
-
|
|
850
|
-
if (ctrl) {
|
|
851
|
-
const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
|
|
852
|
-
if (streams.length) {
|
|
853
|
-
merge(...streams)
|
|
854
|
-
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
855
|
-
.subscribe(() => this._ngControlRev.update((n) => n + 1));
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
|
|
859
|
-
if (submit) {
|
|
860
|
-
submit
|
|
861
|
-
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
862
|
-
.subscribe(() => this._formSubmitRev.update((n) => n + 1));
|
|
863
|
-
}
|
|
850
|
+
this.errorWiring.connect();
|
|
864
851
|
}
|
|
865
852
|
// ── Helpers ──
|
|
866
853
|
hasAccessibleNameHint() {
|
|
@@ -870,12 +857,12 @@ class FileUploadComponent extends FormFieldControl {
|
|
|
870
857
|
return !!host.textContent && host.textContent.trim().length > 0;
|
|
871
858
|
}
|
|
872
859
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: FileUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
873
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: FileUploadComponent, isStandalone: true, selector: "tw-file-upload", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requiredInput: { classPropertyName: "requiredInput", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, idInput: { classPropertyName: "idInput", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filesAdded: "filesAdded", fileRemoved: "fileRemoved", fileRejected: "fileRejected", cleared: "cleared" }, host: { attributes: { "role": "group" }, properties: { "id": "id()", "class": "rootClasses()", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "effectiveAriaLabelledby() || null", "attr.aria-describedby": "effectiveAriaDescribedby() || null", "attr.aria-disabled": "disabled() || null"
|
|
860
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: FileUploadComponent, isStandalone: true, selector: "tw-file-upload", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requiredInput: { classPropertyName: "requiredInput", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, idInput: { classPropertyName: "idInput", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filesAdded: "filesAdded", fileRemoved: "fileRemoved", fileRejected: "fileRejected", cleared: "cleared" }, host: { attributes: { "role": "group" }, properties: { "id": "id()", "class": "rootClasses()", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "effectiveAriaLabelledby() || null", "attr.aria-describedby": "effectiveAriaDescribedby() || null", "attr.aria-disabled": "disabled() || null" } }, providers: [
|
|
874
861
|
{
|
|
875
862
|
provide: TW_FORM_FIELD_CONTROL,
|
|
876
863
|
useExisting: forwardRef(() => FileUploadComponent),
|
|
877
864
|
},
|
|
878
|
-
], queries: [{ propertyName: "itemSlot", first: true, predicate: FileUploadItemDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "dropzoneRef", first: true, predicate: ["dropzone"], descendants: true, isSignal: true }, { propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "hiddenInputRef", first: true, predicate: ["hiddenInput"], descendants: true, isSignal: true }], exportAs: ["twFileUpload"], usesInheritance: true, ngImport: i0, template: "<input\n #hiddenInput\n type=\"file\"\n class=\"sr-only\"\n tabindex=\"-1\"\n [id]=\"hiddenInputId\"\n [attr.aria-label]=\"ariaLabel() ?? label() ?? triggerLabel()\"\n [attr.name]=\"name() || null\"\n [attr.accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n [required]=\"required()\"\n (change)=\"onNativeChange($event)\"\n/>\n\n<!-- The dropzone is a mouse click/drop convenience target, intentionally NOT a\n focusable control: the inner trigger <button> owns keyboard operability, so\n adding tabindex/keydown here would create a nested-interactive structure.\n No keyboard user is locked out, so the rules' concern does not apply. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #dropzone\n [class]=\"dropzoneClasses()\"\n (click)=\"open()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div [class]=\"dropzoneInnerClasses()\">\n <div [class]=\"illustrationClasses()\">\n <ng-content select=\"[twFileUploadIcon]\">\n <tw-icon name=\"upload-cloud\" [size]=\"iconSize()\" aria-hidden=\"true\" />\n </ng-content>\n </div>\n\n <span [id]=\"headlineId\" [class]=\"headlineClasses()\">\n <ng-content select=\"[twFileUploadHeadline]\">\n @if (label(); as l) {\n {{ l }}\n } @else {\n Drag files here or click to browse\n }\n </ng-content>\n </span>\n\n <span [id]=\"descriptionId\" [class]=\"descriptionClasses()\">\n <ng-content select=\"[twFileUploadDescription]\">\n @if (description(); as d) {\n {{ d }}\n }\n </ng-content>\n </span>\n\n <button\n #trigger\n type=\"button\"\n twButton\n variant=\"outline\"\n color=\"neutral\"\n [size]=\"size()\"\n [disabled]=\"disabled()\"\n [attr.aria-describedby]=\"effectiveAriaDescribedby() || null\"\n (click)=\"onTriggerClick($event)\"\n >\n {{ triggerLabel() }}\n </button>\n </div>\n</div>\n\n<ul [class]=\"listClasses()\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (itemSlot(); as slot) {\n <ng-container\n [ngTemplateOutlet]=\"slot.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: item, item: item }\"\n />\n } @else {\n <div [class]=\"itemClasses()\">\n <tw-icon\n [name]=\"iconNameFor(item)\"\n size=\"md\"\n [class]=\"itemIconClasses()\"\n aria-hidden=\"true\"\n />\n <div [class]=\"itemBodyClasses()\">\n <span [class]=\"itemNameClasses()\">{{ item.file.name }}</span>\n <span [class]=\"itemMetaClasses()\">{{ metaTextFor(item) }}</span>\n @if (shouldShowProgress(item)) {\n <tw-progress-bar\n [class]=\"itemProgressClasses()\"\n [value]=\"item.progress\"\n size=\"sm\"\n [color]=\"progressColorFor(item)\"\n />\n }\n </div>\n <div [class]=\"itemActionsClasses()\">\n <button\n type=\"button\"\n twButton\n variant=\"ghost\"\n color=\"neutral\"\n size=\"sm\"\n data-tw-file-upload-remove\n [attr.aria-label]=\"'Remove ' + item.file.name\"\n (click)=\"remove(item.id)\"\n >\n <tw-icon twButtonIcon name=\"x\" />\n </button>\n </div>\n </div>\n }\n </li>\n }\n</ul>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ButtonDirective, selector: "[twButton]", inputs: ["variant", "color", "size", "disabled", "loading"], exportAs: ["twButton"] }, { kind: "component", type: IconComponent, selector: "tw-icon", inputs: ["name", "img", "color", "size", "
|
|
865
|
+
], queries: [{ propertyName: "itemSlot", first: true, predicate: FileUploadItemDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "dropzoneRef", first: true, predicate: ["dropzone"], descendants: true, isSignal: true }, { propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "hiddenInputRef", first: true, predicate: ["hiddenInput"], descendants: true, isSignal: true }], exportAs: ["twFileUpload"], usesInheritance: true, ngImport: i0, template: "<input\n #hiddenInput\n type=\"file\"\n class=\"sr-only\"\n tabindex=\"-1\"\n [id]=\"hiddenInputId\"\n [attr.aria-label]=\"ariaLabel() ?? label() ?? triggerLabel()\"\n [attr.name]=\"name() || null\"\n [attr.accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n [required]=\"required()\"\n [attr.aria-invalid]=\"errorState() || null\"\n (change)=\"onNativeChange($event)\"\n/>\n\n<!-- The dropzone is a mouse click/drop convenience target, intentionally NOT a\n focusable control: the inner trigger <button> owns keyboard operability, so\n adding tabindex/keydown here would create a nested-interactive structure.\n No keyboard user is locked out, so the rules' concern does not apply. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #dropzone\n [class]=\"dropzoneClasses()\"\n (click)=\"open()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div [class]=\"dropzoneInnerClasses()\">\n <div [class]=\"illustrationClasses()\">\n <ng-content select=\"[twFileUploadIcon]\">\n <tw-icon name=\"upload-cloud\" [size]=\"iconSize()\" aria-hidden=\"true\" />\n </ng-content>\n </div>\n\n <span [id]=\"headlineId\" [class]=\"headlineClasses()\">\n <ng-content select=\"[twFileUploadHeadline]\">\n @if (label(); as l) {\n {{ l }}\n } @else {\n Drag files here or click to browse\n }\n </ng-content>\n </span>\n\n <span [id]=\"descriptionId\" [class]=\"descriptionClasses()\">\n <ng-content select=\"[twFileUploadDescription]\">\n @if (description(); as d) {\n {{ d }}\n }\n </ng-content>\n </span>\n\n <button\n #trigger\n type=\"button\"\n twButton\n variant=\"outline\"\n color=\"neutral\"\n [size]=\"size()\"\n [disabled]=\"disabled()\"\n [attr.aria-describedby]=\"effectiveAriaDescribedby() || null\"\n (click)=\"onTriggerClick($event)\"\n >\n {{ triggerLabel() }}\n </button>\n </div>\n</div>\n\n<ul [class]=\"listClasses()\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (itemSlot(); as slot) {\n <ng-container\n [ngTemplateOutlet]=\"slot.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: item, item: item }\"\n />\n } @else {\n <div [class]=\"itemClasses()\">\n <tw-icon\n [name]=\"iconNameFor(item)\"\n size=\"md\"\n [class]=\"itemIconClasses()\"\n aria-hidden=\"true\"\n />\n <div [class]=\"itemBodyClasses()\">\n <span [class]=\"itemNameClasses()\">{{ item.file.name }}</span>\n <span [class]=\"itemMetaClasses()\">{{ metaTextFor(item) }}</span>\n @if (shouldShowProgress(item)) {\n <tw-progress-bar\n [class]=\"itemProgressClasses()\"\n [value]=\"item.progress\"\n size=\"sm\"\n [color]=\"progressColorFor(item)\"\n />\n }\n </div>\n <div [class]=\"itemActionsClasses()\">\n <button\n type=\"button\"\n twButton\n variant=\"ghost\"\n color=\"neutral\"\n size=\"sm\"\n data-tw-file-upload-remove\n [attr.aria-label]=\"'Remove ' + item.file.name\"\n (click)=\"remove(item.id)\"\n >\n <tw-icon twButtonIcon name=\"x\" />\n </button>\n </div>\n </div>\n }\n </li>\n }\n</ul>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ButtonDirective, selector: "[twButton]", inputs: ["variant", "color", "size", "disabled", "loading"], exportAs: ["twButton"] }, { kind: "component", type: IconComponent, selector: "tw-icon", inputs: ["name", "img", "color", "size", "aria-label", "svg"] }, { kind: "component", type: ProgressBarComponent, selector: "tw-progress-bar", inputs: ["value", "variant", "color", "size", "label", "options"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
879
866
|
}
|
|
880
867
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: FileUploadComponent, decorators: [{
|
|
881
868
|
type: Component,
|
|
@@ -892,9 +879,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
892
879
|
'[attr.aria-labelledby]': 'effectiveAriaLabelledby() || null',
|
|
893
880
|
'[attr.aria-describedby]': 'effectiveAriaDescribedby() || null',
|
|
894
881
|
'[attr.aria-disabled]': 'disabled() || null',
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
882
|
+
// NOTE: `aria-required` and `aria-invalid` are NOT set here. ARIA 1.2 does
|
|
883
|
+
// not allow either on `role="group"`, and axe flags it as a *critical*
|
|
884
|
+
// `aria-allowed-attr` violation. Both belong on the control that carries
|
|
885
|
+
// the value: the hidden `<input type="file">` takes native `required` and
|
|
886
|
+
// `aria-invalid` in `file-upload.html`.
|
|
887
|
+
}, template: "<input\n #hiddenInput\n type=\"file\"\n class=\"sr-only\"\n tabindex=\"-1\"\n [id]=\"hiddenInputId\"\n [attr.aria-label]=\"ariaLabel() ?? label() ?? triggerLabel()\"\n [attr.name]=\"name() || null\"\n [attr.accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n [required]=\"required()\"\n [attr.aria-invalid]=\"errorState() || null\"\n (change)=\"onNativeChange($event)\"\n/>\n\n<!-- The dropzone is a mouse click/drop convenience target, intentionally NOT a\n focusable control: the inner trigger <button> owns keyboard operability, so\n adding tabindex/keydown here would create a nested-interactive structure.\n No keyboard user is locked out, so the rules' concern does not apply. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #dropzone\n [class]=\"dropzoneClasses()\"\n (click)=\"open()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div [class]=\"dropzoneInnerClasses()\">\n <div [class]=\"illustrationClasses()\">\n <ng-content select=\"[twFileUploadIcon]\">\n <tw-icon name=\"upload-cloud\" [size]=\"iconSize()\" aria-hidden=\"true\" />\n </ng-content>\n </div>\n\n <span [id]=\"headlineId\" [class]=\"headlineClasses()\">\n <ng-content select=\"[twFileUploadHeadline]\">\n @if (label(); as l) {\n {{ l }}\n } @else {\n Drag files here or click to browse\n }\n </ng-content>\n </span>\n\n <span [id]=\"descriptionId\" [class]=\"descriptionClasses()\">\n <ng-content select=\"[twFileUploadDescription]\">\n @if (description(); as d) {\n {{ d }}\n }\n </ng-content>\n </span>\n\n <button\n #trigger\n type=\"button\"\n twButton\n variant=\"outline\"\n color=\"neutral\"\n [size]=\"size()\"\n [disabled]=\"disabled()\"\n [attr.aria-describedby]=\"effectiveAriaDescribedby() || null\"\n (click)=\"onTriggerClick($event)\"\n >\n {{ triggerLabel() }}\n </button>\n </div>\n</div>\n\n<ul [class]=\"listClasses()\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (itemSlot(); as slot) {\n <ng-container\n [ngTemplateOutlet]=\"slot.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: item, item: item }\"\n />\n } @else {\n <div [class]=\"itemClasses()\">\n <tw-icon\n [name]=\"iconNameFor(item)\"\n size=\"md\"\n [class]=\"itemIconClasses()\"\n aria-hidden=\"true\"\n />\n <div [class]=\"itemBodyClasses()\">\n <span [class]=\"itemNameClasses()\">{{ item.file.name }}</span>\n <span [class]=\"itemMetaClasses()\">{{ metaTextFor(item) }}</span>\n @if (shouldShowProgress(item)) {\n <tw-progress-bar\n [class]=\"itemProgressClasses()\"\n [value]=\"item.progress\"\n size=\"sm\"\n [color]=\"progressColorFor(item)\"\n />\n }\n </div>\n <div [class]=\"itemActionsClasses()\">\n <button\n type=\"button\"\n twButton\n variant=\"ghost\"\n color=\"neutral\"\n size=\"sm\"\n data-tw-file-upload-remove\n [attr.aria-label]=\"'Remove ' + item.file.name\"\n (click)=\"remove(item.id)\"\n >\n <tw-icon twButtonIcon name=\"x\" />\n </button>\n </div>\n </div>\n }\n </li>\n }\n</ul>\n" }]
|
|
898
888
|
}], ctorParameters: () => [], propDecorators: { multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], idInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], filesAdded: [{ type: i0.Output, args: ["filesAdded"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], fileRejected: [{ type: i0.Output, args: ["fileRejected"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], itemSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FileUploadItemDirective), { isSignal: true }] }], dropzoneRef: [{ type: i0.ViewChild, args: ['dropzone', { isSignal: true }] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], hiddenInputRef: [{ type: i0.ViewChild, args: ['hiddenInput', { isSignal: true }] }] } });
|
|
899
889
|
|
|
900
890
|
/**
|