@truenas/ui-components 0.7.0 → 0.7.2
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
|
|
2
2
|
import { TemplatePortal, ComponentPortal, PortalModule } from '@angular/cdk/portal';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { InjectionToken, inject, computed, isDevMode, effect, input, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, Renderer2, ElementRef, Directive,
|
|
4
|
+
import { InjectionToken, inject, computed, isSignal, isDevMode, effect, signal, input, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, Renderer2, ElementRef, Directive, ViewContainerRef, output, viewChild, untracked, forwardRef, DestroyRef, NgZone, afterNextRender, model, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, TemplateRef, viewChildren, ChangeDetectorRef, afterEveryRender, IterableDiffers, booleanAttribute, LOCALE_ID, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
|
|
5
5
|
import * as i1$3 from '@angular/forms';
|
|
6
6
|
import { NgControl, NG_VALUE_ACCESSOR, FormsModule, Validators } from '@angular/forms';
|
|
7
7
|
import { ComponentHarness, HarnessPredicate, parallel, TestKey } from '@angular/cdk/testing';
|
|
@@ -73,18 +73,20 @@ function tnAccessibleName(config) {
|
|
|
73
73
|
// raises the warning rather than being treated as an answer.
|
|
74
74
|
const hasLabelledby = computed(() => (config.ariaLabelledby() ?? '').trim() !== '', ...(ngDevMode ? [{ debugName: "hasLabelledby" }] : []));
|
|
75
75
|
const named = computed(() => (config.ariaLabel() ?? '').trim() !== '' || hasLabelledby(), ...(ngDevMode ? [{ debugName: "named" }] : []));
|
|
76
|
+
const fallback = config.fallback;
|
|
77
|
+
const fallbackName = computed(() => (isSignal(fallback) ? fallback() : fallback), ...(ngDevMode ? [{ debugName: "fallbackName" }] : []));
|
|
76
78
|
const resolved = computed(() => {
|
|
77
79
|
const label = config.ariaLabel();
|
|
78
80
|
if ((label ?? '').trim() !== '') {
|
|
79
81
|
return label;
|
|
80
82
|
}
|
|
81
|
-
return hasLabelledby() ? null :
|
|
83
|
+
return hasLabelledby() ? null : fallbackName();
|
|
82
84
|
}, ...(ngDevMode ? [{ debugName: "resolved" }] : []));
|
|
83
|
-
if (isDevMode()) {
|
|
85
|
+
if (isDevMode() && !config.fallbackIsConfigured) {
|
|
84
86
|
effect(() => {
|
|
85
87
|
if (!named()) {
|
|
86
88
|
console.warn(`[${config.selector}] No ariaLabel or ariaLabelledby was set, so it falls back to `
|
|
87
|
-
+ `"${
|
|
89
|
+
+ `"${fallbackName()}". Assistive technology cannot say WHAT is ${config.activity} `
|
|
88
90
|
+ `— pass ariaLabel, or ariaLabelledby pointing at visible text.`
|
|
89
91
|
+ (config.hint ? ` ${config.hint}` : ''));
|
|
90
92
|
}
|
|
@@ -96,17 +98,175 @@ function tnAccessibleName(config) {
|
|
|
96
98
|
/**
|
|
97
99
|
* The accessible name a spinner falls back to when the caller names neither
|
|
98
100
|
* `ariaLabel` nor `ariaLabelledby` (#202). Same reasoning as
|
|
99
|
-
*
|
|
100
|
-
* defaults to indeterminate mode, so its unnamed default rendering
|
|
101
|
-
* assistive technology as a progressbar with neither a name nor a
|
|
101
|
+
* {@link TN_PROGRESS_BAR_DEFAULT_LABEL}, and the case is sharper here: the
|
|
102
|
+
* spinner defaults to indeterminate mode, so its unnamed default rendering
|
|
103
|
+
* reached assistive technology as a progressbar with neither a name nor a
|
|
104
|
+
* value.
|
|
102
105
|
*
|
|
103
|
-
* `branded-spinner.component.ts`
|
|
106
|
+
* `branded-spinner.component.ts` already fell back this way —
|
|
104
107
|
* `ariaLabel() || "Loading..."` inline — so a fallback is the shape this
|
|
105
108
|
* library had already settled on; what it lacked was the warning. It has both
|
|
106
|
-
* since #206, through the same `tnAccessibleName`
|
|
109
|
+
* since #206, through the same `tnAccessibleName` that component uses, which is
|
|
107
110
|
* why the two constants sit side by side and differ.
|
|
108
111
|
*/
|
|
109
112
|
const TN_SPINNER_DEFAULT_LABEL = 'Loading';
|
|
113
|
+
/**
|
|
114
|
+
* The name `tn-branded-spinner` falls back to when the caller names neither
|
|
115
|
+
* `ariaLabel` nor `ariaLabelledby`. Same reasoning as
|
|
116
|
+
* {@link TN_SPINNER_DEFAULT_LABEL} and {@link TN_PROGRESS_BAR_DEFAULT_LABEL}.
|
|
117
|
+
*
|
|
118
|
+
* It differs from `TN_SPINNER_DEFAULT_LABEL` — `"Loading..."` against
|
|
119
|
+
* `"Loading"` — only because this is the string the component already rendered,
|
|
120
|
+
* inline in its host binding, before #206 gave it the shared resolver. Aligning
|
|
121
|
+
* the two would change what a screen reader announces for every unnamed branded
|
|
122
|
+
* spinner already in the wild, which is a louder change than the consistency
|
|
123
|
+
* fix it would be part of. Exported so specs assert against it by name rather
|
|
124
|
+
* than by a copied string literal.
|
|
125
|
+
*/
|
|
126
|
+
const TN_BRANDED_SPINNER_DEFAULT_LABEL = 'Loading...';
|
|
127
|
+
/**
|
|
128
|
+
* The accessible name a bar falls back to when the caller names neither
|
|
129
|
+
* `ariaLabel` nor `ariaLabelledby` (#202).
|
|
130
|
+
*
|
|
131
|
+
* Deliberately generic, and deliberately not silent: the host carries
|
|
132
|
+
* `role="progressbar"` unconditionally, so without a fallback the default
|
|
133
|
+
* rendering is a progressbar assistive technology announces with no name at all
|
|
134
|
+
* — "progress bar, 40%", with nothing to say what is progressing. The
|
|
135
|
+
* alternative fix, withholding the role until there is a name for it, trades
|
|
136
|
+
* that for no announcement whatever, which is worse: a screen reader would not
|
|
137
|
+
* learn that anything is in progress, and on a determinate bar it would lose
|
|
138
|
+
* the value too.
|
|
139
|
+
*
|
|
140
|
+
* A generic name is still a poor one, so it is paired with the dev-mode warning
|
|
141
|
+
* `tnAccessibleName` raises. Exported so specs assert against it by name rather
|
|
142
|
+
* than by a copied string literal.
|
|
143
|
+
*/
|
|
144
|
+
const TN_PROGRESS_BAR_DEFAULT_LABEL = 'Progress';
|
|
145
|
+
/**
|
|
146
|
+
* The accessible name `tn-particle-progress-bar` falls back to when the caller
|
|
147
|
+
* names neither `ariaLabel` nor `ariaLabelledby` (#209).
|
|
148
|
+
*
|
|
149
|
+
* The same string and the same reasoning as
|
|
150
|
+
* {@link TN_PROGRESS_BAR_DEFAULT_LABEL} next door, and deliberately a separate
|
|
151
|
+
* constant rather than an alias of it: `tnAccessibleName` takes a fallback PER
|
|
152
|
+
* COMPONENT because the least-bad generic name differs by what the component is
|
|
153
|
+
* ("Progress" for a bar, "Loading" for a spinner), and sharing the binding would
|
|
154
|
+
* make a future divergence in one a silent change to the other. Exported so
|
|
155
|
+
* specs assert against it by name rather than by a copied string literal.
|
|
156
|
+
*/
|
|
157
|
+
const TN_PARTICLE_PROGRESS_BAR_DEFAULT_LABEL = 'Progress';
|
|
158
|
+
/**
|
|
159
|
+
* The accessible name a dialog falls back to when it renders no `title` and the
|
|
160
|
+
* caller named it through neither this component nor the `DialogConfig` (#219).
|
|
161
|
+
*
|
|
162
|
+
* `title` defaults to `''`, so the DEFAULT rendering of this component put an
|
|
163
|
+
* empty `<h2>` in the header and left the CDK container with no naming
|
|
164
|
+
* attribute at all — measured as `empty-heading` on the heading and
|
|
165
|
+
* `aria-dialog-name` on the dialog. A dialog with no name is announced as
|
|
166
|
+
* "dialog" and nothing else, which is the whole of what a screen-reader user is
|
|
167
|
+
* told about a surface that just took over the page and trapped their focus.
|
|
168
|
+
*
|
|
169
|
+
* "Dialog" is a poor name, and says almost exactly what the role already says.
|
|
170
|
+
* It is still better than the two alternatives: leaving the surface unnamed, or
|
|
171
|
+
* withholding `role="dialog"` until there is a name — the latter would move a
|
|
172
|
+
* listener into a focus trap with no announcement that anything had opened. So
|
|
173
|
+
* it is paired with the dev-mode warning `tnAccessibleName` raises, which is
|
|
174
|
+
* what keeps the fallback from becoming a quiet way to ship a nameless dialog.
|
|
175
|
+
*
|
|
176
|
+
* Exported so specs assert against it by name rather than by a copied literal.
|
|
177
|
+
*/
|
|
178
|
+
const TN_DIALOG_SHELL_DEFAULT_LABEL = 'Dialog';
|
|
179
|
+
/**
|
|
180
|
+
* The accessible name an open panel falls back to when it has no `title` and the
|
|
181
|
+
* caller named neither `ariaLabel` nor `ariaLabelledby` (#214).
|
|
182
|
+
*
|
|
183
|
+
* `title` defaults to `''`, so the DEFAULT rendering of this component was a
|
|
184
|
+
* `role="dialog"` with `aria-labelledby` pointing at an empty `<h2>` — measured
|
|
185
|
+
* as an `aria-dialog-name` violation, alongside `empty-heading`. A dialog with no
|
|
186
|
+
* name is announced as "dialog" and nothing else, which is the whole of what a
|
|
187
|
+
* screen-reader user gets told about a surface that just covered the page.
|
|
188
|
+
*
|
|
189
|
+
* Withholding `role="dialog"` until there is a name would be the other way to
|
|
190
|
+
* clear the rule, and it is worse: the panel traps focus either way, so a
|
|
191
|
+
* listener would be moved into a region with no announcement that anything had
|
|
192
|
+
* opened. A generic name is still a poor one, so it is paired with the dev-mode
|
|
193
|
+
* warning `tnAccessibleName` raises.
|
|
194
|
+
*
|
|
195
|
+
* Exported so specs assert against it by name rather than by a copied literal.
|
|
196
|
+
*/
|
|
197
|
+
const TN_SIDE_PANEL_DEFAULT_LABEL = 'Side panel';
|
|
198
|
+
/**
|
|
199
|
+
* The accessible name a drawer falls back to when the caller names neither
|
|
200
|
+
* `ariaLabel` nor `ariaLabelledby` (#214).
|
|
201
|
+
*
|
|
202
|
+
* `ariaLabel` defaults to `undefined`, so the DEFAULT rendering in `over` mode
|
|
203
|
+
* was a `role="dialog"` with `aria-modal="true"` and no name — measured as an
|
|
204
|
+
* `aria-dialog-name` violation. In `side` mode the same omission leaves a
|
|
205
|
+
* `role="navigation"` landmark unnamed, which axe does not report while there is
|
|
206
|
+
* only one of them on the page, and which stops telling them apart the moment
|
|
207
|
+
* there are two.
|
|
208
|
+
*
|
|
209
|
+
* One fallback for both modes rather than one per mode: the drawer is the same
|
|
210
|
+
* surface either way, the name answers the same question ("what is this?"), and
|
|
211
|
+
* a rule that changes with the mode is one more thing for a caller to be wrong
|
|
212
|
+
* about. A generic name is still a poor one, so it is paired with the dev-mode
|
|
213
|
+
* warning `tnAccessibleName` raises.
|
|
214
|
+
*
|
|
215
|
+
* Exported so specs assert against it by name rather than by a copied literal.
|
|
216
|
+
*/
|
|
217
|
+
const TN_DRAWER_DEFAULT_LABEL = 'Drawer';
|
|
218
|
+
/** English defaults used when no {@link TN_FALLBACK_LABELS} provider is registered. */
|
|
219
|
+
const TN_DEFAULT_FALLBACK_LABELS = {
|
|
220
|
+
spinner: TN_SPINNER_DEFAULT_LABEL,
|
|
221
|
+
brandedSpinner: TN_BRANDED_SPINNER_DEFAULT_LABEL,
|
|
222
|
+
progressBar: TN_PROGRESS_BAR_DEFAULT_LABEL,
|
|
223
|
+
particleProgressBar: TN_PARTICLE_PROGRESS_BAR_DEFAULT_LABEL,
|
|
224
|
+
dialog: TN_DIALOG_SHELL_DEFAULT_LABEL,
|
|
225
|
+
sidePanel: TN_SIDE_PANEL_DEFAULT_LABEL,
|
|
226
|
+
drawer: TN_DRAWER_DEFAULT_LABEL,
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* What the token resolves to when nobody provided it — a COPY of
|
|
230
|
+
* {@link TN_DEFAULT_FALLBACK_LABELS} rather than that object itself, and the
|
|
231
|
+
* difference is the whole of how `configured` below is decided.
|
|
232
|
+
*
|
|
233
|
+
* Comparing against the exported constant would make a consumer who provides it
|
|
234
|
+
* deliberately — `{ provide: TN_FALLBACK_LABELS, useValue: TN_DEFAULT_FALLBACK_LABELS }`,
|
|
235
|
+
* which is what a test environment mirroring an app's providers writes — read as
|
|
236
|
+
* nobody having provided anything. Held privately, this object is reachable only
|
|
237
|
+
* by not providing one, so identity answers exactly the question being asked.
|
|
238
|
+
*/
|
|
239
|
+
const builtInLabels = { ...TN_DEFAULT_FALLBACK_LABELS };
|
|
240
|
+
/**
|
|
241
|
+
* DI token for the app-wide fallback names above. Provide either a static
|
|
242
|
+
* object or a `Signal<TnFallbackLabels>` — the latter lets every component
|
|
243
|
+
* react to language changes when the consumer wires it up to an i18n service.
|
|
244
|
+
*
|
|
245
|
+
* An `ariaLabel` on a particular instance still wins over these, and is still
|
|
246
|
+
* what to reach for when a name can say what is loading or what is open.
|
|
247
|
+
*/
|
|
248
|
+
const TN_FALLBACK_LABELS = new InjectionToken('TN_FALLBACK_LABELS', { providedIn: 'root', factory: () => builtInLabels });
|
|
249
|
+
/**
|
|
250
|
+
* Reads one component's fallback name out of {@link TN_FALLBACK_LABELS}.
|
|
251
|
+
*
|
|
252
|
+
* Not `injectTnLabels`, which normalizes the token to a Signal and drops the
|
|
253
|
+
* value it started from: telling a configured bundle from the built-in one is an
|
|
254
|
+
* identity check against `builtInLabels`, so this needs the raw injected value.
|
|
255
|
+
* A consumer who provides a bundle equal to the defaults — the exported constant
|
|
256
|
+
* included — still counts as configured, which is the right way round: they said
|
|
257
|
+
* so on purpose.
|
|
258
|
+
*
|
|
259
|
+
* **Must be called from an injection context.**
|
|
260
|
+
*/
|
|
261
|
+
function injectTnFallbackName(key) {
|
|
262
|
+
const provided = inject(TN_FALLBACK_LABELS);
|
|
263
|
+
const labels = isSignal(provided) ? provided : signal(provided).asReadonly();
|
|
264
|
+
return {
|
|
265
|
+
label: computed(() => labels()[key]),
|
|
266
|
+
configured: provided !== builtInLabels,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
110
270
|
class TnSpinnerComponent {
|
|
111
271
|
mode = input('indeterminate', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
112
272
|
value = input(0, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
@@ -114,6 +274,7 @@ class TnSpinnerComponent {
|
|
|
114
274
|
strokeWidth = input(4, ...(ngDevMode ? [{ debugName: "strokeWidth" }] : []));
|
|
115
275
|
ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
116
276
|
ariaLabelledby = input(null, ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : []));
|
|
277
|
+
fallbackName = injectTnFallbackName('spinner');
|
|
117
278
|
/**
|
|
118
279
|
* The name to render, or `null` to render no `aria-label` attribute — and the
|
|
119
280
|
* dev-mode warning when the caller named neither input.
|
|
@@ -127,7 +288,8 @@ class TnSpinnerComponent {
|
|
|
127
288
|
*/
|
|
128
289
|
resolvedAriaLabel = tnAccessibleName({
|
|
129
290
|
selector: 'tn-spinner',
|
|
130
|
-
fallback:
|
|
291
|
+
fallback: this.fallbackName.label,
|
|
292
|
+
fallbackIsConfigured: this.fallbackName.configured,
|
|
131
293
|
activity: 'loading',
|
|
132
294
|
ariaLabel: this.ariaLabel,
|
|
133
295
|
ariaLabelledby: this.ariaLabelledby
|
|
@@ -490,7 +652,7 @@ function injectTnLabels(token) {
|
|
|
490
652
|
return isSignal(provided) ? provided : signal(provided).asReadonly();
|
|
491
653
|
}
|
|
492
654
|
|
|
493
|
-
let nextId$
|
|
655
|
+
let nextId$6 = 0;
|
|
494
656
|
/** English defaults used when no `TN_AUTOCOMPLETE_LABELS` provider is registered. */
|
|
495
657
|
const TN_AUTOCOMPLETE_DEFAULT_LABELS = {
|
|
496
658
|
placeholder: 'Type to search...',
|
|
@@ -510,7 +672,7 @@ class TnAutocompleteComponent {
|
|
|
510
672
|
overlay = inject(Overlay);
|
|
511
673
|
viewContainerRef = inject(ViewContainerRef);
|
|
512
674
|
/** Unique instance ID for ARIA linkage */
|
|
513
|
-
uid = `tn-autocomplete-${nextId$
|
|
675
|
+
uid = `tn-autocomplete-${nextId$6++}`;
|
|
514
676
|
/**
|
|
515
677
|
* All available options. The `label` is displayed; the `value` is committed
|
|
516
678
|
* to the form control. A written value is resolved back to its option's
|
|
@@ -2126,26 +2288,6 @@ function tnScrollableRegion(region) {
|
|
|
2126
2288
|
return reachable.asReadonly();
|
|
2127
2289
|
}
|
|
2128
2290
|
|
|
2129
|
-
/**
|
|
2130
|
-
* The accessible name a drawer falls back to when the caller names neither
|
|
2131
|
-
* `ariaLabel` nor `ariaLabelledby` (#214).
|
|
2132
|
-
*
|
|
2133
|
-
* `ariaLabel` defaults to `undefined`, so the DEFAULT rendering in `over` mode
|
|
2134
|
-
* was a `role="dialog"` with `aria-modal="true"` and no name — measured as an
|
|
2135
|
-
* `aria-dialog-name` violation. In `side` mode the same omission leaves a
|
|
2136
|
-
* `role="navigation"` landmark unnamed, which axe does not report while there is
|
|
2137
|
-
* only one of them on the page, and which stops telling them apart the moment
|
|
2138
|
-
* there are two.
|
|
2139
|
-
*
|
|
2140
|
-
* One fallback for both modes rather than one per mode: the drawer is the same
|
|
2141
|
-
* surface either way, the name answers the same question ("what is this?"), and
|
|
2142
|
-
* a rule that changes with the mode is one more thing for a caller to be wrong
|
|
2143
|
-
* about. A generic name is still a poor one, so it is paired with the dev-mode
|
|
2144
|
-
* warning `tnAccessibleName` raises.
|
|
2145
|
-
*
|
|
2146
|
-
* Exported so specs assert against it by name rather than by a copied literal.
|
|
2147
|
-
*/
|
|
2148
|
-
const TN_DRAWER_DEFAULT_LABEL = 'Drawer';
|
|
2149
2291
|
/**
|
|
2150
2292
|
* A drawer, which is two different things by `mode`: in `side` it is
|
|
2151
2293
|
* persistent navigation beside the page's content, and in `over` it is a modal
|
|
@@ -2255,6 +2397,7 @@ class TnDrawerComponent {
|
|
|
2255
2397
|
trapFocus = computed(() => this.mode() === 'over' && this.opened(), ...(ngDevMode ? [{ debugName: "trapFocus" }] : []));
|
|
2256
2398
|
/** Role depends on mode: navigation for side, dialog for over */
|
|
2257
2399
|
panelRole = computed(() => this.mode() === 'over' ? 'dialog' : 'navigation', ...(ngDevMode ? [{ debugName: "panelRole" }] : []));
|
|
2400
|
+
fallbackName = injectTnFallbackName('drawer');
|
|
2258
2401
|
/**
|
|
2259
2402
|
* The name to render as `aria-label`, or `null` to render none — and the
|
|
2260
2403
|
* dev-mode warning when the caller named neither input.
|
|
@@ -2269,7 +2412,8 @@ class TnDrawerComponent {
|
|
|
2269
2412
|
*/
|
|
2270
2413
|
resolvedAriaLabel = tnAccessibleName({
|
|
2271
2414
|
selector: 'tn-drawer',
|
|
2272
|
-
fallback:
|
|
2415
|
+
fallback: this.fallbackName.label,
|
|
2416
|
+
fallbackIsConfigured: this.fallbackName.configured,
|
|
2273
2417
|
activity: 'open',
|
|
2274
2418
|
ariaLabel: computed(() => this.ariaLabel() ?? null),
|
|
2275
2419
|
ariaLabelledby: this.ariaLabelledby,
|
|
@@ -6040,7 +6184,7 @@ function libIconMarker(iconName) {
|
|
|
6040
6184
|
// Module-level counter for deterministic, unique instance ids (matches the
|
|
6041
6185
|
// tn-autocomplete convention). Deterministic ids are SSR/hydration-safe and
|
|
6042
6186
|
// stable across snapshots, unlike a Math.random() seed.
|
|
6043
|
-
let nextId$
|
|
6187
|
+
let nextId$5 = 0;
|
|
6044
6188
|
class TnInputComponent {
|
|
6045
6189
|
inputEl = viewChild.required('inputEl');
|
|
6046
6190
|
inputType = input(InputType.PlainText, ...(ngDevMode ? [{ debugName: "inputType" }] : []));
|
|
@@ -6217,7 +6361,7 @@ class TnInputComponent {
|
|
|
6217
6361
|
// Unique per instance: a hard-coded id produced duplicate ids when multiple
|
|
6218
6362
|
// tn-inputs share a page (invalid HTML, and it breaks any future label `for=`,
|
|
6219
6363
|
// aria-describedby, or getElementById targeting this input).
|
|
6220
|
-
id = `tn-input-${nextId$
|
|
6364
|
+
id = `tn-input-${nextId$5++}`;
|
|
6221
6365
|
// Protected: the display string is owned by the component and driven through the
|
|
6222
6366
|
// CVA flow (writeValue / onValueChange). External writes would bypass that flow.
|
|
6223
6367
|
// A signal so the `[value]` binding reflects writeValue reactively — a plain field
|
|
@@ -7484,7 +7628,7 @@ class TnChipHarness extends ComponentHarness {
|
|
|
7484
7628
|
}
|
|
7485
7629
|
}
|
|
7486
7630
|
|
|
7487
|
-
let nextId$
|
|
7631
|
+
let nextId$4 = 0;
|
|
7488
7632
|
/**
|
|
7489
7633
|
* An editable, multi-value chip input — tokenized entry where typed text
|
|
7490
7634
|
* becomes removable `tn-chip`s alongside an inline text field. Text is
|
|
@@ -7529,7 +7673,7 @@ class TnChipInputComponent {
|
|
|
7529
7673
|
overlay = inject(Overlay);
|
|
7530
7674
|
viewContainerRef = inject(ViewContainerRef);
|
|
7531
7675
|
/** Unique instance id for ARIA linkage between the input and its dropdown. */
|
|
7532
|
-
uid = `tn-chip-input-${nextId$
|
|
7676
|
+
uid = `tn-chip-input-${nextId$4++}`;
|
|
7533
7677
|
/** Placeholder shown in the text field when it is empty. */
|
|
7534
7678
|
placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
7535
7679
|
/** Disables the whole control — chips become non-removable and the field read-only. */
|
|
@@ -9618,6 +9762,403 @@ class TnCheckboxHarness extends ComponentHarness {
|
|
|
9618
9762
|
}
|
|
9619
9763
|
}
|
|
9620
9764
|
|
|
9765
|
+
let nextId$3 = 0;
|
|
9766
|
+
/**
|
|
9767
|
+
* A `role="group"` wrapper that owns the selection for a set of `tn-checkbox`es, as one control
|
|
9768
|
+
* whose value is the ARRAY of checked option values.
|
|
9769
|
+
*
|
|
9770
|
+
* `tn-checkbox` is a boolean `ControlValueAccessor` — one control per box. A multi-select field
|
|
9771
|
+
* ("which USB devices to pass through", "which catalog trains to offer") has a single
|
|
9772
|
+
* array-valued control instead, and nothing in the library spoke that shape: consumers either
|
|
9773
|
+
* exploded it into one boolean control per option and reassembled the array by hand on submit, or
|
|
9774
|
+
* kept a bespoke component outside the library. This is that shape, with the same
|
|
9775
|
+
* `options`/`compareWith`/`testId` surface as {@link TnRadioGroupComponent} so the single-select
|
|
9776
|
+
* and multi-select forms of the same field read alike.
|
|
9777
|
+
*
|
|
9778
|
+
* ```html
|
|
9779
|
+
* <tn-form-field label="USB Devices" tooltip="Devices to pass through to the container">
|
|
9780
|
+
* <tn-checkbox-group formControlName="usb_devices" [options]="usbOptions" />
|
|
9781
|
+
* </tn-form-field>
|
|
9782
|
+
* ```
|
|
9783
|
+
*
|
|
9784
|
+
* Inside a `tn-form-field` the group is a normal control: the field names it via
|
|
9785
|
+
* `aria-labelledby`, infers the required indicator from its validators, and renders its validation
|
|
9786
|
+
* message — none of which a loose pile of checkboxes gets.
|
|
9787
|
+
*
|
|
9788
|
+
* Options come from the `options` input only. Unlike `tn-radio`, `tn-checkbox` is not group-aware,
|
|
9789
|
+
* so a projected child would render as an independent boolean control that silently ignores the
|
|
9790
|
+
* group's value; the group therefore projects nothing rather than accepting content it cannot
|
|
9791
|
+
* drive.
|
|
9792
|
+
*/
|
|
9793
|
+
class TnCheckboxGroupComponent {
|
|
9794
|
+
/**
|
|
9795
|
+
* Options to render, one checkbox each.
|
|
9796
|
+
*
|
|
9797
|
+
* Options are tracked by `value`, so object values must be referentially stable — rebuilding
|
|
9798
|
+
* them as fresh literals on every change detection pass re-creates the whole list. Same
|
|
9799
|
+
* requirement the `compareWith` case implies; hold the objects in a field.
|
|
9800
|
+
*/
|
|
9801
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
9802
|
+
/**
|
|
9803
|
+
* Lays the options out in a wrapping row of equal columns instead of stacking them. Column width
|
|
9804
|
+
* comes from `--tn-checkbox-group-inline-basis` (default `50%`), and collapses to one column on
|
|
9805
|
+
* a narrow viewport.
|
|
9806
|
+
*/
|
|
9807
|
+
inline = input(false, ...(ngDevMode ? [{ debugName: "inline" }] : []));
|
|
9808
|
+
/**
|
|
9809
|
+
* Explicit accessible name for the group. Inside a `tn-form-field` with a label this is
|
|
9810
|
+
* unnecessary — the field names the group automatically — but a group with neither is announced
|
|
9811
|
+
* as an unlabeled group.
|
|
9812
|
+
*
|
|
9813
|
+
* A required group appends "required" to whatever this says, since that is the only channel
|
|
9814
|
+
* `role="group"` leaves open for it — see the `required` input.
|
|
9815
|
+
*/
|
|
9816
|
+
ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
9817
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
9818
|
+
/**
|
|
9819
|
+
* Whether the group announces itself as required.
|
|
9820
|
+
*
|
|
9821
|
+
* Deliberately NOT propagated to the options' native `required`, which is the opposite of the
|
|
9822
|
+
* choice `tn-radio-group` makes — and the difference is in the HTML, not in taste. A required
|
|
9823
|
+
* radio is satisfied by any one option in the `name` group; a required checkbox is satisfied
|
|
9824
|
+
* only by ITSELF being checked, so propagating it here would have the browser demand every box.
|
|
9825
|
+
* "At least one" is a validator's job.
|
|
9826
|
+
*
|
|
9827
|
+
* Nor does it become `aria-required`, which is the OTHER place `tn-radio-group` cannot be
|
|
9828
|
+
* copied: WAI-ARIA 1.2 supports only the globals plus `aria-activedescendant`/`aria-disabled`
|
|
9829
|
+
* on `role="group"`, so an `aria-required` there is dropped by assistive tech and flagged by
|
|
9830
|
+
* axe, whereas `role="radiogroup"` genuinely supports it. The state instead rides in the
|
|
9831
|
+
* group's accessible NAME, as a "required" suffix — see {@link labelledBy}.
|
|
9832
|
+
*/
|
|
9833
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
9834
|
+
/**
|
|
9835
|
+
* Test-id base for the group and, scoped by the option label, for every option: base
|
|
9836
|
+
* `usb_devices` yields `checkbox-group-usb-devices` on the group and
|
|
9837
|
+
* `checkbox-usb-devices-web-cam` on its options. Falls back to the bound control name.
|
|
9838
|
+
*/
|
|
9839
|
+
testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
9840
|
+
/**
|
|
9841
|
+
* Custom comparator for matching option values against the selected ones. Provide it when option
|
|
9842
|
+
* values are objects — the default is identity, so a structurally-equal-but-distinct object
|
|
9843
|
+
* would leave the group rendering nothing as checked.
|
|
9844
|
+
*
|
|
9845
|
+
* Called with an option value on the right and a currently-selected value on the left, and must
|
|
9846
|
+
* tolerate `null`/`undefined` on either side rather than dereferencing straight into a property.
|
|
9847
|
+
*
|
|
9848
|
+
* @example
|
|
9849
|
+
* ```ts
|
|
9850
|
+
* compareWith = (a, b) => a?.id === b?.id;
|
|
9851
|
+
* ```
|
|
9852
|
+
*/
|
|
9853
|
+
compareWith = input(...(ngDevMode ? [undefined, { debugName: "compareWith" }] : []));
|
|
9854
|
+
/** Emits the new value array on every user toggle (not on programmatic writes). */
|
|
9855
|
+
change = output();
|
|
9856
|
+
/**
|
|
9857
|
+
* ARIA wiring from an enclosing `tn-form-field` (label, error/hint, invalid, required).
|
|
9858
|
+
* All-null standalone, or when `ariaLabel` overrides the field's label.
|
|
9859
|
+
*/
|
|
9860
|
+
fieldAria = injectTnFormFieldAria(this.ariaLabel);
|
|
9861
|
+
/** Test-id base, falling back to the bound control name when `testId` is unset. */
|
|
9862
|
+
resolvedTestId = controlTestId(this.testId);
|
|
9863
|
+
selectedValues = signal([], ...(ngDevMode ? [{ debugName: "selectedValues" }] : []));
|
|
9864
|
+
formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
|
|
9865
|
+
/**
|
|
9866
|
+
* Combined disabled state (own input plus a disabled bound control). The native `disabled` on
|
|
9867
|
+
* each option is what blocks interaction; `aria-disabled` on the group is what tells a screen
|
|
9868
|
+
* reader landing on the container about it, and gives styling a hook for the group as a whole.
|
|
9869
|
+
*/
|
|
9870
|
+
isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
9871
|
+
/** Whether the group announces itself as required: its own input, or the field's inferred state. */
|
|
9872
|
+
ariaRequired = computed(() => this.required() || this.fieldAria.required() === true, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
|
|
9873
|
+
/** Id of the visually hidden "required" text this group contributes to its own name. */
|
|
9874
|
+
requiredTextId = `tn-checkbox-group-required-${nextId$3++}`;
|
|
9875
|
+
/**
|
|
9876
|
+
* `aria-label` for the group: the `ariaLabel` input with the required-ness folded in, since
|
|
9877
|
+
* `role="group"` gives it nowhere else to go (see the `required` input).
|
|
9878
|
+
*/
|
|
9879
|
+
resolvedAriaLabel = computed(() => {
|
|
9880
|
+
const label = this.ariaLabel();
|
|
9881
|
+
if (!label) {
|
|
9882
|
+
return null;
|
|
9883
|
+
}
|
|
9884
|
+
return this.ariaRequired() ? `${label} required` : label;
|
|
9885
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedAriaLabel" }] : []));
|
|
9886
|
+
/**
|
|
9887
|
+
* `aria-labelledby` for the group: the enclosing field's label, plus this group's own hidden
|
|
9888
|
+
* "required" text when required. A multi-id `aria-labelledby` concatenates, so the field keeps
|
|
9889
|
+
* naming the group and the required-ness reaches a screen reader as part of that name.
|
|
9890
|
+
*
|
|
9891
|
+
* Null when the name comes from `ariaLabel` instead — that path carries the suffix itself,
|
|
9892
|
+
* because `aria-labelledby` would otherwise win over the very label it is meant to extend.
|
|
9893
|
+
*/
|
|
9894
|
+
labelledBy = computed(() => {
|
|
9895
|
+
const fieldLabel = this.fieldAria.labelledby();
|
|
9896
|
+
if (!fieldLabel) {
|
|
9897
|
+
return null;
|
|
9898
|
+
}
|
|
9899
|
+
return this.ariaRequired() ? `${fieldLabel} ${this.requiredTextId}` : fieldLabel;
|
|
9900
|
+
}, ...(ngDevMode ? [{ debugName: "labelledBy" }] : []));
|
|
9901
|
+
onChange = () => { };
|
|
9902
|
+
onTouched = () => { };
|
|
9903
|
+
/** Whether `value` is among the currently selected values. */
|
|
9904
|
+
isSelected(value) {
|
|
9905
|
+
const comparator = this.compareWith();
|
|
9906
|
+
if (comparator) {
|
|
9907
|
+
return this.selectedValues().some((selected) => comparator(selected, value));
|
|
9908
|
+
}
|
|
9909
|
+
return this.selectedValues().includes(value);
|
|
9910
|
+
}
|
|
9911
|
+
/**
|
|
9912
|
+
* Adds or removes an option's value.
|
|
9913
|
+
*
|
|
9914
|
+
* The emitted array is rebuilt in `options` order rather than appended to in click order, so the
|
|
9915
|
+
* value a given set of checked boxes produces is the same however the user got there — which is
|
|
9916
|
+
* what makes a payload diff (and a spec asserting one) stable.
|
|
9917
|
+
*
|
|
9918
|
+
* Ordering is all the rebuild changes: an already-selected value is re-emitted as the object the
|
|
9919
|
+
* consumer wrote, not as the equal-under-`compareWith` object the option holds. A control loaded
|
|
9920
|
+
* with `{ id: 2, name: 'usb-cam' }` against an option of `{ id: 2 }` keeps its `name` when an
|
|
9921
|
+
* unrelated box is toggled; only a newly checked option contributes the option's own value.
|
|
9922
|
+
*/
|
|
9923
|
+
toggle(value, checked) {
|
|
9924
|
+
const isCurrentlySelected = this.isSelected(value);
|
|
9925
|
+
if (checked === isCurrentlySelected) {
|
|
9926
|
+
return;
|
|
9927
|
+
}
|
|
9928
|
+
const next = this.options()
|
|
9929
|
+
.map((option) => option.value)
|
|
9930
|
+
.filter((optionValue) => (this.matches(optionValue, value) ? checked : this.isSelected(optionValue)))
|
|
9931
|
+
.map((optionValue) => this.selectedValues().find((selected) => this.matches(optionValue, selected)) ?? optionValue);
|
|
9932
|
+
// Values written by the consumer that no longer correspond to an option are kept: a control
|
|
9933
|
+
// loaded with a device that has since been unplugged would otherwise be silently pruned by an
|
|
9934
|
+
// unrelated toggle.
|
|
9935
|
+
const unknownValues = this.selectedValues().filter((selected) => !this.options().some((option) => this.matches(option.value, selected)));
|
|
9936
|
+
this.selectedValues.set([...next, ...unknownValues]);
|
|
9937
|
+
this.onChange(this.selectedValues());
|
|
9938
|
+
this.onTouched();
|
|
9939
|
+
this.change.emit(this.selectedValues());
|
|
9940
|
+
}
|
|
9941
|
+
matches(a, b) {
|
|
9942
|
+
const comparator = this.compareWith();
|
|
9943
|
+
return comparator ? comparator(a, b) : a === b;
|
|
9944
|
+
}
|
|
9945
|
+
// ── ControlValueAccessor ──
|
|
9946
|
+
writeValue(value) {
|
|
9947
|
+
this.selectedValues.set(value ?? []);
|
|
9948
|
+
}
|
|
9949
|
+
registerOnChange(fn) {
|
|
9950
|
+
this.onChange = fn;
|
|
9951
|
+
}
|
|
9952
|
+
registerOnTouched(fn) {
|
|
9953
|
+
this.onTouched = fn;
|
|
9954
|
+
}
|
|
9955
|
+
setDisabledState(isDisabled) {
|
|
9956
|
+
this.formDisabled.set(isDisabled);
|
|
9957
|
+
}
|
|
9958
|
+
/**
|
|
9959
|
+
* Marks the control touched when focus leaves the group. `focusout` also fires while moving
|
|
9960
|
+
* between the group's own options, which is harmless — the user has interacted either way — and
|
|
9961
|
+
* it is the only blur signal available, since a checkbox's blur does not bubble.
|
|
9962
|
+
*/
|
|
9963
|
+
onFocusOut() {
|
|
9964
|
+
this.onTouched();
|
|
9965
|
+
}
|
|
9966
|
+
/** Per-option test-id segments: the group's base scoped by the option label. */
|
|
9967
|
+
optionTestId(option) {
|
|
9968
|
+
return scopeTestId(this.resolvedTestId(), option.label);
|
|
9969
|
+
}
|
|
9970
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCheckboxGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9971
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCheckboxGroupComponent, isStandalone: true, selector: "tn-checkbox-group", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change" }, host: { classAttribute: "tn-checkbox-group-host" }, providers: [
|
|
9972
|
+
{
|
|
9973
|
+
provide: NG_VALUE_ACCESSOR,
|
|
9974
|
+
useExisting: forwardRef(() => TnCheckboxGroupComponent),
|
|
9975
|
+
multi: true
|
|
9976
|
+
}
|
|
9977
|
+
], ngImport: i0, template: "<div\n class=\"tn-checkbox-group\"\n role=\"group\"\n tnTestIdType=\"checkbox-group\"\n [class.tn-checkbox-group--inline]=\"inline()\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-disabled]=\"isDisabled() ? true : null\"\n [tnTestId]=\"resolvedTestId()\"\n (focusout)=\"onFocusOut()\"\n>\n @for (option of options(); track option.value) {\n <tn-checkbox\n class=\"tn-checkbox-group__option\"\n [label]=\"option.label\"\n [checked]=\"isSelected(option.value)\"\n [disabled]=\"isDisabled() || !!option.disabled\"\n [testId]=\"optionTestId(option)\"\n (change)=\"toggle(option.value, $event)\"\n />\n }\n</div>\n\n<!-- The required-ness rides in the group's accessible NAME, because `role=\"group\"` does not\n support `aria-required` \u2014 see the `required` input's docblock. It sits OUTSIDE the group so\n the text is not also read as group content on the way past the options; `aria-labelledby`\n resolves ids document-wide, so it names the group from here just the same. -->\n@if (ariaRequired()) {\n <span class=\"cdk-visually-hidden\" [attr.id]=\"requiredTextId\">required</span>\n}\n", styles: [".tn-checkbox-group-host{display:block}.tn-checkbox-group{display:flex;flex-direction:column;gap:var(--tn-checkbox-group-gap, 8px)}.tn-checkbox-group--inline{flex-direction:row;flex-wrap:wrap;column-gap:var(--tn-checkbox-group-inline-gap, 16px)}.tn-checkbox-group--inline .tn-checkbox-group__option{flex:1 1 calc(var(--tn-checkbox-group-inline-basis, 50%) - var(--tn-checkbox-group-inline-gap, 16px));box-sizing:border-box}@media(max-width:768px){.tn-checkbox-group--inline .tn-checkbox-group__option{flex-basis:100%}}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"], dependencies: [{ kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
9978
|
+
}
|
|
9979
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCheckboxGroupComponent, decorators: [{
|
|
9980
|
+
type: Component,
|
|
9981
|
+
args: [{ selector: 'tn-checkbox-group', standalone: true, imports: [TnCheckboxComponent, TnTestIdDirective], providers: [
|
|
9982
|
+
{
|
|
9983
|
+
provide: NG_VALUE_ACCESSOR,
|
|
9984
|
+
useExisting: forwardRef(() => TnCheckboxGroupComponent),
|
|
9985
|
+
multi: true
|
|
9986
|
+
}
|
|
9987
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
9988
|
+
'class': 'tn-checkbox-group-host'
|
|
9989
|
+
}, template: "<div\n class=\"tn-checkbox-group\"\n role=\"group\"\n tnTestIdType=\"checkbox-group\"\n [class.tn-checkbox-group--inline]=\"inline()\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-disabled]=\"isDisabled() ? true : null\"\n [tnTestId]=\"resolvedTestId()\"\n (focusout)=\"onFocusOut()\"\n>\n @for (option of options(); track option.value) {\n <tn-checkbox\n class=\"tn-checkbox-group__option\"\n [label]=\"option.label\"\n [checked]=\"isSelected(option.value)\"\n [disabled]=\"isDisabled() || !!option.disabled\"\n [testId]=\"optionTestId(option)\"\n (change)=\"toggle(option.value, $event)\"\n />\n }\n</div>\n\n<!-- The required-ness rides in the group's accessible NAME, because `role=\"group\"` does not\n support `aria-required` \u2014 see the `required` input's docblock. It sits OUTSIDE the group so\n the text is not also read as group content on the way past the options; `aria-labelledby`\n resolves ids document-wide, so it names the group from here just the same. -->\n@if (ariaRequired()) {\n <span class=\"cdk-visually-hidden\" [attr.id]=\"requiredTextId\">required</span>\n}\n", styles: [".tn-checkbox-group-host{display:block}.tn-checkbox-group{display:flex;flex-direction:column;gap:var(--tn-checkbox-group-gap, 8px)}.tn-checkbox-group--inline{flex-direction:row;flex-wrap:wrap;column-gap:var(--tn-checkbox-group-inline-gap, 16px)}.tn-checkbox-group--inline .tn-checkbox-group__option{flex:1 1 calc(var(--tn-checkbox-group-inline-basis, 50%) - var(--tn-checkbox-group-inline-gap, 16px));box-sizing:border-box}@media(max-width:768px){.tn-checkbox-group--inline .tn-checkbox-group__option{flex-basis:100%}}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"] }]
|
|
9990
|
+
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], change: [{ type: i0.Output, args: ["change"] }] } });
|
|
9991
|
+
|
|
9992
|
+
/**
|
|
9993
|
+
* Harness for interacting with `tn-checkbox-group` in tests.
|
|
9994
|
+
* Drives the group as a whole — reading the checked set, replacing it wholesale, toggling one
|
|
9995
|
+
* option by label — rather than reaching for each `tn-checkbox` individually.
|
|
9996
|
+
*
|
|
9997
|
+
* @example
|
|
9998
|
+
* ```typescript
|
|
9999
|
+
* const group = await loader.getHarness(TnCheckboxGroupHarness.with({ testId: 'checkbox-group-trains' }));
|
|
10000
|
+
* // Labels, not option values — the DOM only carries what the user can read.
|
|
10001
|
+
* await group.setValue(['Stable', 'Enterprise']);
|
|
10002
|
+
* expect(await group.getValue()).toEqual(['Stable', 'Enterprise']);
|
|
10003
|
+
* ```
|
|
10004
|
+
*/
|
|
10005
|
+
class TnCheckboxGroupHarness extends ComponentHarness {
|
|
10006
|
+
/** The selector for the host element of a `TnCheckboxGroupComponent` instance. */
|
|
10007
|
+
static hostSelector = 'tn-checkbox-group';
|
|
10008
|
+
_root = this.locatorFor('.tn-checkbox-group');
|
|
10009
|
+
_checkboxes = this.locatorForAll(TnCheckboxHarness);
|
|
10010
|
+
/**
|
|
10011
|
+
* Gets a `HarnessPredicate` that can be used to search for a checkbox group with specific
|
|
10012
|
+
* attributes.
|
|
10013
|
+
*
|
|
10014
|
+
* @param options Options for filtering which checkbox group instances are considered a match.
|
|
10015
|
+
* @returns A `HarnessPredicate` configured with the given options.
|
|
10016
|
+
*
|
|
10017
|
+
* @example
|
|
10018
|
+
* ```typescript
|
|
10019
|
+
* // Find by accessible name
|
|
10020
|
+
* const group = await loader.getHarness(TnCheckboxGroupHarness.with({ ariaLabel: 'USB Devices' }));
|
|
10021
|
+
*
|
|
10022
|
+
* // Find by testId
|
|
10023
|
+
* const group = await loader.getHarness(TnCheckboxGroupHarness.with({ testId: 'checkbox-group-usb-devices' }));
|
|
10024
|
+
* ```
|
|
10025
|
+
*/
|
|
10026
|
+
static with(options = {}) {
|
|
10027
|
+
return new HarnessPredicate(TnCheckboxGroupHarness, options)
|
|
10028
|
+
.addOption('ariaLabel', options.ariaLabel, (harness, ariaLabel) => HarnessPredicate.stringMatches(harness.getAriaLabel(), ariaLabel))
|
|
10029
|
+
.addOption('testId', options.testId, async (harness, testId) => {
|
|
10030
|
+
return (await harness.getTestId()) === testId;
|
|
10031
|
+
});
|
|
10032
|
+
}
|
|
10033
|
+
/**
|
|
10034
|
+
* Gets the harnesses for the group's options, in DOM order.
|
|
10035
|
+
*
|
|
10036
|
+
* @example
|
|
10037
|
+
* ```typescript
|
|
10038
|
+
* const options = await group.getOptions();
|
|
10039
|
+
* expect(options).toHaveLength(3);
|
|
10040
|
+
* ```
|
|
10041
|
+
*/
|
|
10042
|
+
async getOptions() {
|
|
10043
|
+
return this._checkboxes();
|
|
10044
|
+
}
|
|
10045
|
+
/**
|
|
10046
|
+
* Gets the label text of every option, in DOM order.
|
|
10047
|
+
*
|
|
10048
|
+
* @example
|
|
10049
|
+
* ```typescript
|
|
10050
|
+
* expect(await group.getOptionLabels()).toEqual(['Stable', 'Enterprise']);
|
|
10051
|
+
* ```
|
|
10052
|
+
*/
|
|
10053
|
+
async getOptionLabels() {
|
|
10054
|
+
const checkboxes = await this._checkboxes();
|
|
10055
|
+
return Promise.all(checkboxes.map((checkbox) => checkbox.getLabelText()));
|
|
10056
|
+
}
|
|
10057
|
+
/**
|
|
10058
|
+
* Gets the labels of the currently checked options, in DOM order.
|
|
10059
|
+
*
|
|
10060
|
+
* Labels rather than values, because the values live in the bound control and the DOM only ever
|
|
10061
|
+
* carries what the user can read.
|
|
10062
|
+
*
|
|
10063
|
+
* @example
|
|
10064
|
+
* ```typescript
|
|
10065
|
+
* expect(await group.getValue()).toEqual(['Stable']);
|
|
10066
|
+
* ```
|
|
10067
|
+
*/
|
|
10068
|
+
async getValue() {
|
|
10069
|
+
const checkboxes = await this._checkboxes();
|
|
10070
|
+
const checked = [];
|
|
10071
|
+
for (const checkbox of checkboxes) {
|
|
10072
|
+
if (await checkbox.isChecked()) {
|
|
10073
|
+
checked.push(await checkbox.getLabelText());
|
|
10074
|
+
}
|
|
10075
|
+
}
|
|
10076
|
+
return checked;
|
|
10077
|
+
}
|
|
10078
|
+
/**
|
|
10079
|
+
* Makes the checked set exactly the options with the given labels — checking the ones listed and
|
|
10080
|
+
* unchecking every other.
|
|
10081
|
+
*
|
|
10082
|
+
* @param labels Label text of the options that should end up checked.
|
|
10083
|
+
*
|
|
10084
|
+
* @example
|
|
10085
|
+
* ```typescript
|
|
10086
|
+
* await group.setValue(['Stable']);
|
|
10087
|
+
* ```
|
|
10088
|
+
*/
|
|
10089
|
+
async setValue(labels) {
|
|
10090
|
+
const checkboxes = await this._checkboxes();
|
|
10091
|
+
for (const checkbox of checkboxes) {
|
|
10092
|
+
const label = await checkbox.getLabelText();
|
|
10093
|
+
if (labels.includes(label)) {
|
|
10094
|
+
await checkbox.check();
|
|
10095
|
+
}
|
|
10096
|
+
else {
|
|
10097
|
+
await checkbox.uncheck();
|
|
10098
|
+
}
|
|
10099
|
+
}
|
|
10100
|
+
}
|
|
10101
|
+
/**
|
|
10102
|
+
* Toggles the option with the given label. Throws when the group has no such option, so a
|
|
10103
|
+
* renamed or missing option fails loudly instead of silently leaving the selection unchanged.
|
|
10104
|
+
*
|
|
10105
|
+
* @param label Label text of the option to toggle.
|
|
10106
|
+
*
|
|
10107
|
+
* @example
|
|
10108
|
+
* ```typescript
|
|
10109
|
+
* await group.toggle('Enterprise');
|
|
10110
|
+
* ```
|
|
10111
|
+
*/
|
|
10112
|
+
async toggle(label) {
|
|
10113
|
+
const checkbox = await this.locatorForOptional(TnCheckboxHarness.with({ label }))();
|
|
10114
|
+
if (!checkbox) {
|
|
10115
|
+
const available = (await this.getOptionLabels()).join(', ');
|
|
10116
|
+
throw new Error(`No option labelled "${label}" in this tn-checkbox-group (available: ${available}).`);
|
|
10117
|
+
}
|
|
10118
|
+
await checkbox.toggle();
|
|
10119
|
+
}
|
|
10120
|
+
/**
|
|
10121
|
+
* Checks whether every option is disabled — the state a disabled group or a disabled bound form
|
|
10122
|
+
* control produces. Use {@link getOptions} to inspect a single per-option `disabled`.
|
|
10123
|
+
*
|
|
10124
|
+
* @example
|
|
10125
|
+
* ```typescript
|
|
10126
|
+
* expect(await group.isDisabled()).toBe(true);
|
|
10127
|
+
* ```
|
|
10128
|
+
*/
|
|
10129
|
+
async isDisabled() {
|
|
10130
|
+
const checkboxes = await this._checkboxes();
|
|
10131
|
+
if (!checkboxes.length) {
|
|
10132
|
+
return false;
|
|
10133
|
+
}
|
|
10134
|
+
const states = await Promise.all(checkboxes.map((checkbox) => checkbox.isDisabled()));
|
|
10135
|
+
return states.every(Boolean);
|
|
10136
|
+
}
|
|
10137
|
+
/**
|
|
10138
|
+
* Gets the group's accessible name as written by the `ariaLabel` input, or null when the name
|
|
10139
|
+
* comes from an enclosing `tn-form-field` (see {@link getAriaLabelledBy}).
|
|
10140
|
+
*/
|
|
10141
|
+
async getAriaLabel() {
|
|
10142
|
+
return (await this._root()).getAttribute('aria-label');
|
|
10143
|
+
}
|
|
10144
|
+
/** Gets the id the group's accessible name is delegated to, or null. */
|
|
10145
|
+
async getAriaLabelledBy() {
|
|
10146
|
+
return (await this._root()).getAttribute('aria-labelledby');
|
|
10147
|
+
}
|
|
10148
|
+
/**
|
|
10149
|
+
* Gets the test ID attribute value of the group root.
|
|
10150
|
+
*
|
|
10151
|
+
* @example
|
|
10152
|
+
* ```typescript
|
|
10153
|
+
* expect(await group.getTestId()).toBe('checkbox-group-trains');
|
|
10154
|
+
* ```
|
|
10155
|
+
*/
|
|
10156
|
+
async getTestId() {
|
|
10157
|
+
const root = await this._root();
|
|
10158
|
+
return (await root.getAttribute('data-testid')) ?? (await root.getAttribute('data-test'));
|
|
10159
|
+
}
|
|
10160
|
+
}
|
|
10161
|
+
|
|
9621
10162
|
/** DI token under which `tn-radio-group` exposes its {@link TnRadioGroupApi} to child radios. */
|
|
9622
10163
|
const TN_RADIO_GROUP = new InjectionToken('TN_RADIO_GROUP');
|
|
9623
10164
|
|
|
@@ -20141,23 +20682,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
20141
20682
|
}]
|
|
20142
20683
|
}] });
|
|
20143
20684
|
|
|
20144
|
-
/**
|
|
20145
|
-
* The name this spinner falls back to when the caller names neither `ariaLabel`
|
|
20146
|
-
* nor `ariaLabelledby`. Same reasoning as `TN_SPINNER_DEFAULT_LABEL` and
|
|
20147
|
-
* `TN_PROGRESS_BAR_DEFAULT_LABEL`.
|
|
20148
|
-
*
|
|
20149
|
-
* It differs from `TN_SPINNER_DEFAULT_LABEL` — `"Loading..."` against
|
|
20150
|
-
* `"Loading"` — only because this is the string the component already rendered,
|
|
20151
|
-
* inline in its host binding, before #206 gave it the shared resolver. Aligning
|
|
20152
|
-
* the two would change what a screen reader announces for every unnamed branded
|
|
20153
|
-
* spinner already in the wild, which is a louder change than the consistency
|
|
20154
|
-
* fix it would be part of. Exported so specs assert against it by name rather
|
|
20155
|
-
* than by a copied string literal.
|
|
20156
|
-
*/
|
|
20157
|
-
const TN_BRANDED_SPINNER_DEFAULT_LABEL = 'Loading...';
|
|
20158
20685
|
class TnBrandedSpinnerComponent {
|
|
20159
20686
|
ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
20160
20687
|
ariaLabelledby = input(null, ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : []));
|
|
20688
|
+
fallbackName = injectTnFallbackName('brandedSpinner');
|
|
20161
20689
|
/**
|
|
20162
20690
|
* The name to render, or `null` to render no `aria-label` attribute — and the
|
|
20163
20691
|
* dev-mode warning when the caller named neither input.
|
|
@@ -20171,7 +20699,8 @@ class TnBrandedSpinnerComponent {
|
|
|
20171
20699
|
*/
|
|
20172
20700
|
resolvedAriaLabel = tnAccessibleName({
|
|
20173
20701
|
selector: 'tn-branded-spinner',
|
|
20174
|
-
fallback:
|
|
20702
|
+
fallback: this.fallbackName.label,
|
|
20703
|
+
fallbackIsConfigured: this.fallbackName.configured,
|
|
20175
20704
|
activity: 'loading',
|
|
20176
20705
|
ariaLabel: this.ariaLabel,
|
|
20177
20706
|
ariaLabelledby: this.ariaLabelledby
|
|
@@ -20284,30 +20813,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
20284
20813
|
}, template: "<div class=\"tn-branded-spinner-container\">\n <svg \n viewBox=\"0 -7 62 62\" \n preserveAspectRatio=\"xMidYMid meet\" \n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"tn-branded-spinner-logo\">\n \n <!-- EXPLODED LOGO PATHS -->\n <path class=\"exploded center\" fill=\"#AFADAE\" d=\"m41.79 24.13-11.26 6.51-11.28-6.51 11.28-6.51 11.26 6.51Z\" />\n <path class=\"exploded top-left\" fill=\"#35BEEB\" d=\"M27.86 0v13.01l-13.93 8.04-11.28-6.5L27.86 0Z\" />\n <path class=\"exploded bottom-right\" fill=\"#35BEEB\" d=\"M61.03 19.16v13.01L33.19 48.25V35.24l27.84-16.08Z\" />\n <path class=\"exploded bottom-left\" fill=\"#0A95D3\" d=\"M27.86 35.24v13L0 32.17v-13l12.59 7.26s.03.02.05.03l15.23 8.79-.01-.01Z\" />\n <path class=\"exploded top-right\" fill=\"#0A95D3\" d=\"m58.38 14.55-11.27 6.51-13.92-8.05V0l25.19 14.55Z\" />\n </svg>\n</div>", styles: [".tn-branded-spinner{display:inline-block;vertical-align:middle}.tn-branded-spinner-container{display:flex;flex-direction:column;align-items:center;justify-content:center;width:100px;min-height:100px}.tn-branded-spinner-logo{width:100px;height:100px}.tn-branded-spinner-logo path.exploded{fill-opacity:0;stroke-width:1px;stroke:var(--tn-primary, #007bff)}.tn-branded-spinner-logo path#morph-target{fill-opacity:0;fill:var(--tn-primary, #007bff)}\n"] }]
|
|
20285
20814
|
}], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }] } });
|
|
20286
20815
|
|
|
20287
|
-
/**
|
|
20288
|
-
* The accessible name a bar falls back to when the caller names neither
|
|
20289
|
-
* `ariaLabel` nor `ariaLabelledby` (#202).
|
|
20290
|
-
*
|
|
20291
|
-
* Deliberately generic, and deliberately not silent: the host carries
|
|
20292
|
-
* `role="progressbar"` unconditionally, so without a fallback the default
|
|
20293
|
-
* rendering is a progressbar assistive technology announces with no name at all
|
|
20294
|
-
* — "progress bar, 40%", with nothing to say what is progressing. The
|
|
20295
|
-
* alternative fix, withholding the role until there is a name for it, trades
|
|
20296
|
-
* that for no announcement whatever, which is worse: a screen reader would not
|
|
20297
|
-
* learn that anything is in progress, and on a determinate bar it would lose
|
|
20298
|
-
* the value too.
|
|
20299
|
-
*
|
|
20300
|
-
* A generic name is still a poor one, so it is paired with the dev-mode warning
|
|
20301
|
-
* `tnAccessibleName` raises. Exported so specs assert against it by name rather
|
|
20302
|
-
* than by a copied string literal.
|
|
20303
|
-
*/
|
|
20304
|
-
const TN_PROGRESS_BAR_DEFAULT_LABEL = 'Progress';
|
|
20305
20816
|
class TnProgressBarComponent {
|
|
20306
20817
|
mode = input('determinate', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
20307
20818
|
value = input(0, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
20308
20819
|
bufferValue = input(0, ...(ngDevMode ? [{ debugName: "bufferValue" }] : []));
|
|
20309
20820
|
ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
20310
20821
|
ariaLabelledby = input(null, ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : []));
|
|
20822
|
+
fallbackName = injectTnFallbackName('progressBar');
|
|
20311
20823
|
/**
|
|
20312
20824
|
* The name to render, or `null` to render no `aria-label` attribute — and the
|
|
20313
20825
|
* dev-mode warning when the caller named neither input.
|
|
@@ -20323,7 +20835,8 @@ class TnProgressBarComponent {
|
|
|
20323
20835
|
*/
|
|
20324
20836
|
resolvedAriaLabel = tnAccessibleName({
|
|
20325
20837
|
selector: 'tn-progress-bar',
|
|
20326
|
-
fallback:
|
|
20838
|
+
fallback: this.fallbackName.label,
|
|
20839
|
+
fallbackIsConfigured: this.fallbackName.configured,
|
|
20327
20840
|
activity: 'progressing',
|
|
20328
20841
|
ariaLabel: this.ariaLabel,
|
|
20329
20842
|
ariaLabelledby: this.ariaLabelledby
|
|
@@ -20382,19 +20895,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
20382
20895
|
}, template: "<!-- Background/buffer layer -->\n<div class=\"tn-progress-bar-buffer\" aria-hidden=\"true\">\n <!-- Buffer bar hidden in new implementation -->\n <!-- Buffer dots for buffer mode -->\n @if (mode() === 'buffer') {\n <div class=\"tn-progress-bar-buffer-dots\"\n [style.width]=\"bufferStyles().width\"\n [style.right]=\"bufferStyles().right\"></div>\n }\n</div>\n\n<!-- Primary bar -->\n<div class=\"tn-progress-bar-bar tn-progress-bar-primary-bar\" aria-hidden=\"true\">\n <span class=\"tn-progress-bar-bar-inner\"\n [style.transform]=\"primaryTransform()\"></span>\n</div>\n\n<!-- Secondary bar (for indeterminate mode) -->\n@if (mode() === 'indeterminate') {\n <div class=\"tn-progress-bar-bar tn-progress-bar-secondary-bar\" aria-hidden=\"true\">\n <span class=\"tn-progress-bar-bar-inner\"></span>\n </div>\n}\n\n", styles: [":host{display:block;height:4px;overflow:hidden;position:relative;transition:opacity .25s linear;width:100%}.tn-progress-bar-buffer{position:absolute;top:0;bottom:0;width:100%;background-color:var(--tn-bg2, #e5e7eb)}.tn-progress-bar-buffer-bar{display:none}.tn-progress-bar-buffer-dots{position:absolute;top:0;bottom:0;width:100%;background-image:radial-gradient(circle at center,var(--tn-alt-bg2) 2px,transparent 2px);background-size:10px 4px;background-repeat:repeat-x;animation:tn-progress-bar-buffer-dots .25s infinite linear}@keyframes tn-progress-bar-buffer-dots{0%{background-position-x:0}to{background-position-x:-10px}}.tn-progress-bar-bar{position:absolute;top:0;bottom:0;left:0;width:100%;transform-origin:left center}.tn-progress-bar-bar-inner{display:block;position:absolute;top:0;bottom:0;left:0;width:100%;background-color:var(--tn-primary, #3b82f6);transform-origin:left center;height:100%}.tn-progress-bar-primary-bar{z-index:1}.tn-progress-bar-secondary-bar{z-index:0;display:none}.tn-progress-bar-secondary-bar .tn-progress-bar-bar-inner{background-color:var(--tn-primary, #3b82f6)}:host(.tn-progress-bar-indeterminate) .tn-progress-bar-primary-bar .tn-progress-bar-bar-inner{animation:tn-progress-bar-indeterminate 2s infinite linear;background-color:var(--tn-primary, #3b82f6);opacity:1!important;z-index:999!important}@keyframes tn-progress-bar-indeterminate{0%{transform:translate(-100%) scaleX(.3)}to{transform:translate(100%) scaleX(.3)}}:host(.tn-progress-bar-buffer) .tn-progress-bar-buffer-bar{opacity:1}:host(.tn-progress-bar-buffer) .tn-progress-bar-buffer-dots{opacity:1}:host(.tn-progress-bar-determinate) .tn-progress-bar-buffer-dots{display:none}:host(.tn-progress-bar-determinate) .tn-progress-bar-secondary-bar{display:none}:host(.tn-progress-bar-buffer) .tn-progress-bar-secondary-bar{display:none}:host(.tn-progress-bar-indeterminate) .tn-progress-bar-buffer-dots{display:none}:host(.tn-progress-bar-indeterminate) .tn-progress-bar-buffer-bar{display:none}\n"] }]
|
|
20383
20896
|
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], bufferValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "bufferValue", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }] } });
|
|
20384
20897
|
|
|
20385
|
-
/**
|
|
20386
|
-
* The accessible name this bar falls back to when the caller names neither
|
|
20387
|
-
* `ariaLabel` nor `ariaLabelledby` (#209).
|
|
20388
|
-
*
|
|
20389
|
-
* The same string and the same reasoning as `TN_PROGRESS_BAR_DEFAULT_LABEL`
|
|
20390
|
-
* next door, and deliberately a separate constant rather than an import of it:
|
|
20391
|
-
* `tnAccessibleName` takes a fallback PER COMPONENT because the least-bad
|
|
20392
|
-
* generic name differs by what the component is ("Progress" for a bar,
|
|
20393
|
-
* "Loading" for a spinner), and sharing the binding would make a future
|
|
20394
|
-
* divergence in one a silent change to the other. Exported so specs assert
|
|
20395
|
-
* against it by name rather than by a copied string literal.
|
|
20396
|
-
*/
|
|
20397
|
-
const TN_PARTICLE_PROGRESS_BAR_DEFAULT_LABEL = 'Progress';
|
|
20398
20898
|
/**
|
|
20399
20899
|
* The gap in px between the edge of the SVG and each end of the track, on both
|
|
20400
20900
|
* sides — the `x` the two rects are drawn at, and half of what is subtracted
|
|
@@ -20466,6 +20966,7 @@ class TnParticleProgressBarComponent {
|
|
|
20466
20966
|
canvasRef = viewChild.required('canvas');
|
|
20467
20967
|
/** Exposed to the template so the rects and the ARIA value share one inset. */
|
|
20468
20968
|
trackInset = TRACK_INSET;
|
|
20969
|
+
fallbackName = injectTnFallbackName('particleProgressBar');
|
|
20469
20970
|
/**
|
|
20470
20971
|
* The name to render, or `null` to render no `aria-label` at all — and the
|
|
20471
20972
|
* dev-mode warning when the caller named neither input.
|
|
@@ -20483,7 +20984,8 @@ class TnParticleProgressBarComponent {
|
|
|
20483
20984
|
*/
|
|
20484
20985
|
resolvedAriaLabel = tnAccessibleName({
|
|
20485
20986
|
selector: 'tn-particle-progress-bar',
|
|
20486
|
-
fallback:
|
|
20987
|
+
fallback: this.fallbackName.label,
|
|
20988
|
+
fallbackIsConfigured: this.fallbackName.configured,
|
|
20487
20989
|
activity: 'progressing',
|
|
20488
20990
|
ariaLabel: this.ariaLabel,
|
|
20489
20991
|
ariaLabelledby: this.ariaLabelledby
|
|
@@ -24406,27 +24908,6 @@ const TN_DIALOG_DEFAULT_CHROME_LABELS = {
|
|
|
24406
24908
|
*/
|
|
24407
24909
|
const TN_DIALOG_CHROME_LABELS = new InjectionToken('TN_DIALOG_CHROME_LABELS', { providedIn: 'root', factory: () => TN_DIALOG_DEFAULT_CHROME_LABELS });
|
|
24408
24910
|
let nextUniqueId = 0;
|
|
24409
|
-
/**
|
|
24410
|
-
* The accessible name a dialog falls back to when it renders no `title` and the
|
|
24411
|
-
* caller named it through neither this component nor the `DialogConfig` (#219).
|
|
24412
|
-
*
|
|
24413
|
-
* `title` defaults to `''`, so the DEFAULT rendering of this component put an
|
|
24414
|
-
* empty `<h2>` in the header and left the CDK container with no naming
|
|
24415
|
-
* attribute at all — measured as `empty-heading` on the heading and
|
|
24416
|
-
* `aria-dialog-name` on the dialog. A dialog with no name is announced as
|
|
24417
|
-
* "dialog" and nothing else, which is the whole of what a screen-reader user is
|
|
24418
|
-
* told about a surface that just took over the page and trapped their focus.
|
|
24419
|
-
*
|
|
24420
|
-
* "Dialog" is a poor name, and says almost exactly what the role already says.
|
|
24421
|
-
* It is still better than the two alternatives: leaving the surface unnamed, or
|
|
24422
|
-
* withholding `role="dialog"` until there is a name — the latter would move a
|
|
24423
|
-
* listener into a focus trap with no announcement that anything had opened. So
|
|
24424
|
-
* it is paired with the dev-mode warning `tnAccessibleName` raises, which is
|
|
24425
|
-
* what keeps the fallback from becoming a quiet way to ship a nameless dialog.
|
|
24426
|
-
*
|
|
24427
|
-
* Exported so specs assert against it by name rather than by a copied literal.
|
|
24428
|
-
*/
|
|
24429
|
-
const TN_DIALOG_SHELL_DEFAULT_LABEL = 'Dialog';
|
|
24430
24911
|
/**
|
|
24431
24912
|
* The first value that is a name, or `null` if none of them is one.
|
|
24432
24913
|
*
|
|
@@ -24544,6 +25025,7 @@ class TnDialogShellComponent {
|
|
|
24544
25025
|
resolvedAriaLabelledby = computed(() => (this.hasTitle() ? this.titleId : firstNonBlank(this.ariaLabelledby(), this.ref.config?.ariaLabelledBy)), ...(ngDevMode ? [{ debugName: "resolvedAriaLabelledby" }] : []));
|
|
24545
25026
|
/** An explicit label, from this component's input or from the `DialogConfig`. */
|
|
24546
25027
|
explicitAriaLabel = computed(() => firstNonBlank(this.ariaLabel(), this.ref.config?.ariaLabel), ...(ngDevMode ? [{ debugName: "explicitAriaLabel" }] : []));
|
|
25028
|
+
fallbackName = injectTnFallbackName('dialog');
|
|
24547
25029
|
/**
|
|
24548
25030
|
* The name to render as `aria-label`, or `null` to render none — and the
|
|
24549
25031
|
* dev-mode warning when the dialog has no name from any route.
|
|
@@ -24555,7 +25037,8 @@ class TnDialogShellComponent {
|
|
|
24555
25037
|
*/
|
|
24556
25038
|
resolvedAriaLabel = tnAccessibleName({
|
|
24557
25039
|
selector: 'tn-dialog-shell',
|
|
24558
|
-
fallback:
|
|
25040
|
+
fallback: this.fallbackName.label,
|
|
25041
|
+
fallbackIsConfigured: this.fallbackName.configured,
|
|
24559
25042
|
activity: 'open',
|
|
24560
25043
|
hint: 'On this component the usual route is title, which is also the visible heading.',
|
|
24561
25044
|
ariaLabel: this.explicitAriaLabel,
|
|
@@ -24977,25 +25460,6 @@ class TnDialogTesting {
|
|
|
24977
25460
|
}
|
|
24978
25461
|
}
|
|
24979
25462
|
|
|
24980
|
-
/**
|
|
24981
|
-
* The accessible name an open panel falls back to when it has no `title` and the
|
|
24982
|
-
* caller named neither `ariaLabel` nor `ariaLabelledby` (#214).
|
|
24983
|
-
*
|
|
24984
|
-
* `title` defaults to `''`, so the DEFAULT rendering of this component was a
|
|
24985
|
-
* `role="dialog"` with `aria-labelledby` pointing at an empty `<h2>` — measured
|
|
24986
|
-
* as an `aria-dialog-name` violation, alongside `empty-heading`. A dialog with no
|
|
24987
|
-
* name is announced as "dialog" and nothing else, which is the whole of what a
|
|
24988
|
-
* screen-reader user gets told about a surface that just covered the page.
|
|
24989
|
-
*
|
|
24990
|
-
* Withholding `role="dialog"` until there is a name would be the other way to
|
|
24991
|
-
* clear the rule, and it is worse: the panel traps focus either way, so a
|
|
24992
|
-
* listener would be moved into a region with no announcement that anything had
|
|
24993
|
-
* opened. A generic name is still a poor one, so it is paired with the dev-mode
|
|
24994
|
-
* warning `tnAccessibleName` raises.
|
|
24995
|
-
*
|
|
24996
|
-
* Exported so specs assert against it by name rather than by a copied literal.
|
|
24997
|
-
*/
|
|
24998
|
-
const TN_SIDE_PANEL_DEFAULT_LABEL = 'Side panel';
|
|
24999
25463
|
/**
|
|
25000
25464
|
* The name given to the scrolling content region once it becomes focusable
|
|
25001
25465
|
* (#248).
|
|
@@ -25202,6 +25666,7 @@ class TnSidePanelComponent {
|
|
|
25202
25666
|
* when there is one, the caller's IDREF otherwise.
|
|
25203
25667
|
*/
|
|
25204
25668
|
resolvedAriaLabelledby = computed(() => (this.hasTitle() ? this.titleId : this.ariaLabelledby()), ...(ngDevMode ? [{ debugName: "resolvedAriaLabelledby" }] : []));
|
|
25669
|
+
fallbackName = injectTnFallbackName('sidePanel');
|
|
25205
25670
|
/**
|
|
25206
25671
|
* The name to render as `aria-label`, or `null` to render none — and the
|
|
25207
25672
|
* dev-mode warning when the panel has no name from any route.
|
|
@@ -25213,7 +25678,8 @@ class TnSidePanelComponent {
|
|
|
25213
25678
|
*/
|
|
25214
25679
|
resolvedAriaLabel = tnAccessibleName({
|
|
25215
25680
|
selector: 'tn-side-panel',
|
|
25216
|
-
fallback:
|
|
25681
|
+
fallback: this.fallbackName.label,
|
|
25682
|
+
fallbackIsConfigured: this.fallbackName.configured,
|
|
25217
25683
|
activity: 'open',
|
|
25218
25684
|
hint: 'On this component the usual route is title, which is also the visible heading.',
|
|
25219
25685
|
ariaLabel: this.ariaLabel,
|
|
@@ -28150,5 +28616,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
28150
28616
|
* Generated bundle index. Do not edit.
|
|
28151
28617
|
*/
|
|
28152
28618
|
|
|
28153
|
-
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, THEME_MAP, THEME_STORAGE_KEY, TN_AUTOCOMPLETE_DEFAULT_LABELS, TN_AUTOCOMPLETE_LABELS, TN_BRANDED_SPINNER_DEFAULT_LABEL, TN_CALENDAR_INTL, TN_CALENDAR_INTL_DEFAULTS, TN_DIALOG_CHROME_LABELS, TN_DIALOG_DEFAULT_CHROME_LABELS, TN_DIALOG_SHELL_DEFAULT_LABEL, TN_DRAWER_CONTENT_LABEL, TN_DRAWER_DEFAULT_LABEL, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_DISMISSIBLE_ERRORS, TN_FORM_FIELD_ERRORS, TN_FORM_LIST_CONTEXT, TN_PARTICLE_PROGRESS_BAR_DEFAULT_LABEL, TN_PROGRESS_BAR_DEFAULT_LABEL, TN_RADIO_GROUP, TN_SELECT_DEFAULT_LABELS, TN_SELECT_LABELS, TN_SIDE_PANEL_CONTENT_LABEL, TN_SIDE_PANEL_DEFAULT_LABEL, TN_SIDE_PANEL_OVERFLOW_TOLERANCE_PX, TN_SPINNER_DEFAULT_LABEL, TN_STEPPER_STATUS_COMPLETED, TN_STEPPER_STATUS_ERROR, TN_TABLE_DEFAULT_LABELS, TN_TABLE_LABELS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TABLE_SCROLL_REGION_LABEL, TN_TAB_PANEL_CONTENT_LABEL, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TN_TOAST_ANNOUNCE_DELAY_MS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerActionHarness, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarCellHarness, TnCalendarComponent, TnCalendarHarness, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerHarness, TnFilePickerPopupComponent, TnFormErrorsComponent, TnFormErrorsHarness, TnFormFieldComponent, TnFormFieldHarness, TnFormListComponent, TnFormListHarness, TnFormListItemComponent, TnFormListItemHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeDataSource, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioGroupComponent, TnRadioGroupHarness, TnRadioHarness, TnRowActionsDefDirective, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTableTesting, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTooltipHarness, TnTooltipTesting, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, allowsCurrentDirectorySelection, canonicalizeSize, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, formatSizeExact, formatSizeForEditing, getSelectableTypes, injectTnCalendarIntl, injectTnFormFieldAria, injectTnLabels, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, parseSizeParts, registerLucideIcons, resolveErrorMessage, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
|
|
28619
|
+
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, THEME_MAP, THEME_STORAGE_KEY, TN_AUTOCOMPLETE_DEFAULT_LABELS, TN_AUTOCOMPLETE_LABELS, TN_BRANDED_SPINNER_DEFAULT_LABEL, TN_CALENDAR_INTL, TN_CALENDAR_INTL_DEFAULTS, TN_DEFAULT_FALLBACK_LABELS, TN_DIALOG_CHROME_LABELS, TN_DIALOG_DEFAULT_CHROME_LABELS, TN_DIALOG_SHELL_DEFAULT_LABEL, TN_DRAWER_CONTENT_LABEL, TN_DRAWER_DEFAULT_LABEL, TN_FALLBACK_LABELS, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_DISMISSIBLE_ERRORS, TN_FORM_FIELD_ERRORS, TN_FORM_LIST_CONTEXT, TN_PARTICLE_PROGRESS_BAR_DEFAULT_LABEL, TN_PROGRESS_BAR_DEFAULT_LABEL, TN_RADIO_GROUP, TN_SELECT_DEFAULT_LABELS, TN_SELECT_LABELS, TN_SIDE_PANEL_CONTENT_LABEL, TN_SIDE_PANEL_DEFAULT_LABEL, TN_SIDE_PANEL_OVERFLOW_TOLERANCE_PX, TN_SPINNER_DEFAULT_LABEL, TN_STEPPER_STATUS_COMPLETED, TN_STEPPER_STATUS_ERROR, TN_TABLE_DEFAULT_LABELS, TN_TABLE_LABELS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TABLE_SCROLL_REGION_LABEL, TN_TAB_PANEL_CONTENT_LABEL, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TN_TOAST_ANNOUNCE_DELAY_MS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerActionHarness, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarCellHarness, TnCalendarComponent, TnCalendarHarness, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxGroupComponent, TnCheckboxGroupHarness, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerHarness, TnFilePickerPopupComponent, TnFormErrorsComponent, TnFormErrorsHarness, TnFormFieldComponent, TnFormFieldHarness, TnFormListComponent, TnFormListHarness, TnFormListItemComponent, TnFormListItemHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeDataSource, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioGroupComponent, TnRadioGroupHarness, TnRadioHarness, TnRowActionsDefDirective, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTableTesting, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTooltipHarness, TnTooltipTesting, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, allowsCurrentDirectorySelection, canonicalizeSize, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, formatSizeExact, formatSizeForEditing, getSelectableTypes, injectTnCalendarIntl, injectTnFallbackName, injectTnFormFieldAria, injectTnLabels, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, parseSizeParts, registerLucideIcons, resolveErrorMessage, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
|
|
28154
28620
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|