@truenas/ui-components 0.5.3 → 0.6.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 * as _truenas_ui_components from '@truenas/ui-components';
2
2
  import * as _angular_core from '@angular/core';
3
- import { Signal, InjectionToken, Renderer2, OnDestroy, ElementRef, AfterViewInit, TemplateRef, AfterContentInit, Provider, ChangeDetectorRef, OnInit, TrackByFunction, OnChanges, DoCheck, SimpleChanges, PipeTransform, ViewContainerRef, AfterViewChecked, ComponentRef } from '@angular/core';
4
- import { ControlValueAccessor, NgControl, AbstractControl } from '@angular/forms';
3
+ import { Signal, InjectionToken, Renderer2, OnDestroy, ElementRef, AfterViewInit, TemplateRef, AfterContentInit, Provider, DoCheck, ChangeDetectorRef, OnInit, TrackByFunction, OnChanges, SimpleChanges, PipeTransform, ViewContainerRef, AfterViewChecked, ComponentRef } from '@angular/core';
4
+ import { ControlValueAccessor, NgControl, AbstractControl, ValidationErrors } from '@angular/forms';
5
5
  import { ComponentHarness, BaseHarnessFilters, HarnessPredicate, TestKey, HarnessLoader, ModifierKeys as ModifierKeys$1 } from '@angular/cdk/testing';
6
6
  import { SafeHtml, SafeResourceUrl, DomSanitizer } from '@angular/platform-browser';
7
7
  import { ComponentFixture } from '@angular/core/testing';
@@ -30,9 +30,16 @@ type TnTestIdValue = string | number | (string | number | null | undefined)[] |
30
30
  * collapse any run of non-alphanumeric characters to a single hyphen, and trim
31
31
  * leading/trailing hyphens.
32
32
  *
33
- * Mirrors the normalization webui's legacy `ixTest` directive applied via
34
- * lodash `kebabCase`, so migrated values stay stable (`sshPort` → `ssh-port`,
35
- * `addr_trtype` → `addr-trtype`, `'My Label'` → `my-label`).
33
+ * Follows webui's legacy `ixTest` directive (lodash `kebabCase`) for the cases
34
+ * that matter to migrated values — `sshPort` → `ssh-port`, `addr_trtype` →
35
+ * `addr-trtype`, `'My Label'` → `my-label` — but deliberately not for the
36
+ * letter↔digit boundary, which lodash splits and this does not: `nvme0n1` stays
37
+ * `nvme0n1` rather than becoming `nvme-0-n-1`, and `ipv4` stays `ipv4`. Device
38
+ * and protocol names read better whole, and they are exactly the strings that
39
+ * end up in ids. A consumer that must reproduce a legacy lodash-derived id
40
+ * byte-for-byte pre-normalizes the dynamic part itself before handing it over
41
+ * (webui's `normalizeTestIdString` does this); the result then passes through
42
+ * here unchanged.
36
43
  */
37
44
  declare function kebabTestSegment(part: string | number): string;
38
45
  /**
@@ -133,14 +140,29 @@ interface TnOptionTestIdSource {
133
140
  * `[tnTestId]` with `tnTestIdType="option"`. The component's resolved base
134
141
  * (explicit `testId`, else the bound control name) scopes a per-option
135
142
  * discriminator so ids stay unique across instances: base `user` + option
136
- * value `jane-doe` → `option-user-jane-doe`; with no base → `option-jane-doe`.
143
+ * label `Jane Doe` → `option-user-jane-doe`; with no base → `option-jane-doe`.
137
144
  *
138
145
  * The discriminator comes from `extractor` when provided (a component's
139
- * `optionTestIdKey` input), else the option's primitive `value`, else its
140
- * `label`. Shared by `tn-select` and `tn-autocomplete` so the derivation
141
- * rules can't drift between dropdown components; synthetic rows with fixed
142
- * discriminators (e.g. select's `allowEmpty` option) are handled by the
146
+ * `optionTestIdKey` input), else the option's **`label`** — the text actually
147
+ * on screen — falling back to a primitive `value` only for the labelless
148
+ * option. Shared by `tn-select`, `tn-autocomplete` and `tn-chip-input` so the
149
+ * derivation rules can't drift between dropdown components; synthetic rows with
150
+ * fixed discriminators (e.g. select's `allowEmpty` option) are handled by the
143
151
  * caller before delegating here.
152
+ *
153
+ * **Why the label and not the value.** An option's value is frequently opaque
154
+ * to everyone but the code that owns it — an enum ordinal, a record id, a
155
+ * protocol constant — so keying ids off it yields `option-sshconnectmode-0` or
156
+ * `option-user-1734`: not unique in any way a test author can predict, and
157
+ * silently renumbered whenever the enum or the records change. The label is the
158
+ * one part of an option that both the person writing the test and the person
159
+ * reading the page can see, so it is the useful default. What it trades away is
160
+ * uniqueness by construction: a value is unique within a control (it is the
161
+ * model value the control round-trips) where a label is not, so an option set
162
+ * with a repeated display name now produces repeated ids, silently — nothing
163
+ * validates label uniqueness. Where that happens, or an id must be stable
164
+ * across locales, or an id-per-record is genuinely wanted, `extractor`
165
+ * (`[optionTestIdKey]`) still overrides it — that is what the input is for.
144
166
  */
145
167
  declare function optionTestId<O extends TnOptionTestIdSource>(base: TnTestIdValue, option: O, extractor?: (option: O) => string | number | null | undefined): (string | number | null | undefined)[];
146
168
 
@@ -236,6 +258,33 @@ interface TnSelectOptionGroup<T = unknown> {
236
258
  options: TnSelectOption<T>[];
237
259
  disabled?: boolean;
238
260
  }
261
+ /**
262
+ * Copy rendered inside `tn-select` that is the same for every select in an app.
263
+ *
264
+ * Binding these per call site means repeating the identical string on every
265
+ * `<tn-select>` in the codebase — an app with translated copy ends up with two
266
+ * extra attribute rows on each one. Provide {@link TN_SELECT_LABELS} at the app
267
+ * root instead; inputs on `<tn-select>` still win where a particular select
268
+ * needs its own wording.
269
+ */
270
+ interface TnSelectLabels {
271
+ /** Trigger text shown while nothing is selected. */
272
+ placeholder: string;
273
+ /** Message shown inside the dropdown when there are no options to list. */
274
+ noOptions: string;
275
+ /** Label of the "select all" row rendered when `showSelectAll` is set. */
276
+ selectAll: string;
277
+ }
278
+ /** English defaults used when no `TN_SELECT_LABELS` provider is registered. */
279
+ declare const TN_SELECT_DEFAULT_LABELS: TnSelectLabels;
280
+ /**
281
+ * DI token for app-wide default labels. Provide either a static object or a
282
+ * `Signal<TnSelectLabels>` — the latter lets every select react to language
283
+ * changes when the consumer wires it up to an i18n service.
284
+ *
285
+ * Explicit input bindings on `<tn-select>` still win over these defaults.
286
+ */
287
+ declare const TN_SELECT_LABELS: InjectionToken<TnSelectLabels | Signal<TnSelectLabels>>;
239
288
  /**
240
289
  * A keyboard-navigable row in the open dropdown. Either the synthetic
241
290
  * "select all" action (multiple mode with `showSelectAll`) or a real option.
@@ -252,7 +301,13 @@ type TnSelectNavEntry<T> = {
252
301
  declare class TnSelectComponent<T = unknown> implements ControlValueAccessor, OnDestroy {
253
302
  options: _angular_core.InputSignal<TnSelectOption<T>[]>;
254
303
  optionGroups: _angular_core.InputSignal<TnSelectOptionGroup<T>[]>;
255
- placeholder: _angular_core.InputSignal<string>;
304
+ /**
305
+ * Label inputs are nullable on purpose: the component reads the resolved
306
+ * `resolved*` computeds below, which fall back to the DI-provided defaults
307
+ * (a signal — so language changes propagate live). An explicit input binding
308
+ * always wins.
309
+ */
310
+ placeholder: _angular_core.InputSignal<string | undefined>;
256
311
  /**
257
312
  * Explicit accessible label for the select trigger. When set, this is used as
258
313
  * the trigger's `aria-label` instead of the visible `placeholder` — useful in
@@ -268,18 +323,23 @@ declare class TnSelectComponent<T = unknown> implements ControlValueAccessor, On
268
323
  * invalid, required). All-null when standalone or when `ariaLabel` overrides.
269
324
  */
270
325
  protected readonly fieldAria: _truenas_ui_components.TnFormFieldAriaBindings;
326
+ private readonly defaultLabels;
327
+ /** Resolved labels: an explicit input takes precedence over the DI default. */
328
+ protected readonly resolvedPlaceholder: Signal<string>;
329
+ protected readonly resolvedNoOptionsLabel: Signal<string>;
330
+ protected readonly resolvedSelectAllLabel: Signal<string>;
271
331
  /**
272
332
  * `aria-label` for the trigger. An explicit `ariaLabel` always wins; the
273
333
  * `placeholder` fallback only applies while no form-field label is wired via
274
334
  * `aria-labelledby`, so the trigger never advertises two names at once.
275
335
  */
276
- protected triggerAriaLabel: _angular_core.Signal<string | null>;
336
+ protected triggerAriaLabel: Signal<string | null>;
277
337
  /**
278
338
  * Message shown inside the dropdown when no options (and no option groups)
279
- * are available. Defaults to the English `'No options available'`; consumers
280
- * with i18n requirements can pass a translated string.
339
+ * are available. Falls back to {@link TN_SELECT_LABELS}, so an app with i18n
340
+ * wiring provides it once rather than per call site.
281
341
  */
282
- noOptionsLabel: _angular_core.InputSignal<string>;
342
+ noOptionsLabel: _angular_core.InputSignal<string | undefined>;
283
343
  /**
284
344
  * When `true` (single-select mode only), prepends a synthetic "empty"
285
345
  * option to the dropdown so users can unset a chosen value: picking it
@@ -301,15 +361,15 @@ declare class TnSelectComponent<T = unknown> implements ControlValueAccessor, On
301
361
  */
302
362
  showSelectAll: _angular_core.InputSignal<boolean>;
303
363
  /** Label of the select-all row rendered when `showSelectAll` is set. */
304
- selectAllLabel: _angular_core.InputSignal<string>;
364
+ selectAllLabel: _angular_core.InputSignal<string | undefined>;
305
365
  testId: _angular_core.InputSignal<TnTestIdValue>;
306
366
  /** Test-id base, falling back to the bound control name when `testId` is unset. */
307
- protected resolvedTestId: _angular_core.Signal<TnTestIdValue>;
367
+ protected resolvedTestId: Signal<TnTestIdValue>;
308
368
  multiple: _angular_core.InputSignal<boolean>;
309
369
  /**
310
- * Optional extractor for the per-option test-id discriminator. Defaults to
311
- * the option's `value` (when a string/number) or its `label`. Provide this
312
- * when option values are objects, or to pick a more stable/unique key —
370
+ * Optional extractor for the per-option test-id discriminator. Defaults to the
371
+ * option's `label`, the text actually on screen — provide this to key off a
372
+ * locale-independent field instead, or where an id per record is wanted —
313
373
  * mirrors webui's `[ixTest]="[controlName, option.<field>]"` discriminator.
314
374
  *
315
375
  * @example
@@ -355,17 +415,17 @@ declare class TnSelectComponent<T = unknown> implements ControlValueAccessor, On
355
415
  * instances; otherwise falls back to a per-instance counter so two
356
416
  * `<tn-select>`s on the same page never collide on `aria-controls`/group ids.
357
417
  */
358
- protected idNamespace: _angular_core.Signal<string>;
359
- isDisabled: _angular_core.Signal<boolean>;
418
+ protected idNamespace: Signal<string>;
419
+ isDisabled: Signal<boolean>;
360
420
  /**
361
421
  * The synthetic clear-selection option (`allowEmpty`, single mode only).
362
422
  * Its value is `null` cast to `T` so it flows through the same selection
363
423
  * path as real options — `selectedValue`/`writeValue` already model "no
364
424
  * selection" as `null`, so picking it clears the field for free.
365
425
  */
366
- protected emptyOption: _angular_core.Signal<TnSelectOption<T> | null>;
426
+ protected emptyOption: Signal<TnSelectOption<T> | null>;
367
427
  /** Ungrouped options as rendered: the empty option (when enabled) first. */
368
- protected displayOptions: _angular_core.Signal<TnSelectOption<T>[]>;
428
+ protected displayOptions: Signal<TnSelectOption<T>[]>;
369
429
  /** Whether `option` is the synthetic `allowEmpty` clear option. */
370
430
  protected isEmptyOption(option: TnSelectOption<T>): boolean;
371
431
  /**
@@ -373,9 +433,9 @@ declare class TnSelectComponent<T = unknown> implements ControlValueAccessor, On
373
433
  * then groups). Used by keyboard navigation so we can skip disabled
374
434
  * entries and group headers without a separate filter pass.
375
435
  */
376
- navigableOptions: _angular_core.Signal<TnSelectNavEntry<T>[]>;
436
+ navigableOptions: Signal<TnSelectNavEntry<T>[]>;
377
437
  /** Stable DOM id of the currently-highlighted option, for aria-activedescendant. */
378
- focusedOptionId: _angular_core.Signal<string | null>;
438
+ focusedOptionId: Signal<string | null>;
379
439
  /** Stable DOM id for an option; matches what navigableOptions() assigns. */
380
440
  optionId(option: TnSelectOption<T>): string | null;
381
441
  /** Whether `option` is the keyboard-highlighted item. */
@@ -437,9 +497,9 @@ declare class TnSelectComponent<T = unknown> implements ControlValueAccessor, On
437
497
  selectOption(option: TnSelectOption<T>): void;
438
498
  private toggleOption;
439
499
  isOptionSelected(option: TnSelectOption<T>): boolean;
440
- protected displayText: _angular_core.Signal<string>;
500
+ protected displayText: Signal<string>;
441
501
  private findOptionByValue;
442
- protected hasAnyOptions: _angular_core.Signal<boolean>;
502
+ protected hasAnyOptions: Signal<boolean>;
443
503
  /**
444
504
  * Values of every selectable (non-disabled) option, across ungrouped options
445
505
  * and enabled groups. This is the set the select-all row operates on —
@@ -450,15 +510,15 @@ declare class TnSelectComponent<T = unknown> implements ControlValueAccessor, On
450
510
  * ungrouped and inside a group isn't pushed twice — that would make
451
511
  * select-all diverge from `toggleOption()`, which never produces duplicates.
452
512
  */
453
- protected selectableValues: _angular_core.Signal<T[]>;
513
+ protected selectableValues: Signal<T[]>;
454
514
  /** Whether the select-all row is shown (multiple mode, opted in, with options). */
455
- protected showSelectAllRow: _angular_core.Signal<boolean>;
515
+ protected showSelectAllRow: Signal<boolean>;
456
516
  /** Stable DOM id of the select-all row, for aria-activedescendant. */
457
- protected selectAllId: _angular_core.Signal<string>;
517
+ protected selectAllId: Signal<string>;
458
518
  /** True when every selectable option is currently selected. */
459
- protected allSelected: _angular_core.Signal<boolean>;
519
+ protected allSelected: Signal<boolean>;
460
520
  /** True when some — but not all — selectable options are selected. */
461
- protected selectAllIndeterminate: _angular_core.Signal<boolean>;
521
+ protected selectAllIndeterminate: Signal<boolean>;
462
522
  /** Test-id segments for the select-all row; mirrors ix-select's `[name, 'select-all']`. */
463
523
  protected selectAllTestIdParts(): (string | number | null | undefined)[];
464
524
  /**
@@ -523,6 +583,30 @@ declare class TnSelectComponent<T = unknown> implements ControlValueAccessor, On
523
583
  * sources feed both dropdown components.
524
584
  */
525
585
  type TnAutocompleteOption<T = unknown> = TnSelectOption<T>;
586
+ /**
587
+ * Copy rendered inside `tn-autocomplete` that is the same for every instance in
588
+ * an app. Provide {@link TN_AUTOCOMPLETE_LABELS} at the app root rather than
589
+ * repeating the identical strings on each call site; inputs on
590
+ * `<tn-autocomplete>` still win where one instance needs its own wording.
591
+ */
592
+ interface TnAutocompleteLabels {
593
+ /** Placeholder shown in the text field while it is empty. */
594
+ placeholder: string;
595
+ /** Text shown next to the spinner while `loading` is set. */
596
+ loading: string;
597
+ /** Text shown when no option matches the search term. */
598
+ noResults: string;
599
+ }
600
+ /** English defaults used when no `TN_AUTOCOMPLETE_LABELS` provider is registered. */
601
+ declare const TN_AUTOCOMPLETE_DEFAULT_LABELS: TnAutocompleteLabels;
602
+ /**
603
+ * DI token for app-wide default labels. Provide either a static object or a
604
+ * `Signal<TnAutocompleteLabels>` — the latter lets every autocomplete react to
605
+ * language changes when the consumer wires it up to an i18n service.
606
+ *
607
+ * Explicit input bindings on `<tn-autocomplete>` still win over these defaults.
608
+ */
609
+ declare const TN_AUTOCOMPLETE_LABELS: InjectionToken<TnAutocompleteLabels | Signal<TnAutocompleteLabels>>;
526
610
  declare class TnAutocompleteComponent<T = unknown> implements ControlValueAccessor, OnDestroy {
527
611
  private readonly elementRef;
528
612
  private readonly overlay;
@@ -543,8 +627,13 @@ declare class TnAutocompleteComponent<T = unknown> implements ControlValueAccess
543
627
  * Mirrors `tn-select`'s `compareWith`.
544
628
  */
545
629
  compareWith: _angular_core.InputSignal<((a: T | null, b: T | null) => boolean) | undefined>;
546
- /** Placeholder text for the input */
547
- placeholder: _angular_core.InputSignal<string>;
630
+ /**
631
+ * Placeholder text for the input. Label inputs are nullable on purpose: the
632
+ * component reads the `resolved*` computeds below, which fall back to
633
+ * {@link TN_AUTOCOMPLETE_LABELS} (a signal — so language changes propagate
634
+ * live). An explicit input binding always wins.
635
+ */
636
+ placeholder: _angular_core.InputSignal<string | undefined>;
548
637
  /** Whether the input is disabled */
549
638
  disabled: _angular_core.InputSignal<boolean>;
550
639
  /** Require the user to select from the dropdown — reverts on blur if no match */
@@ -563,11 +652,11 @@ declare class TnAutocompleteComponent<T = unknown> implements ControlValueAccess
563
652
  */
564
653
  loading: _angular_core.InputSignal<boolean>;
565
654
  /** Text shown next to the spinner while `loading` is set. */
566
- loadingText: _angular_core.InputSignal<string>;
655
+ loadingText: _angular_core.InputSignal<string | undefined>;
567
656
  /** Custom filter function. Defaults to case-insensitive includes on the option label */
568
657
  filterFn: _angular_core.InputSignal<((option: TnAutocompleteOption<T>, searchTerm: string) => boolean) | undefined>;
569
658
  /** Text shown when no options match the search */
570
- noResultsText: _angular_core.InputSignal<string>;
659
+ noResultsText: _angular_core.InputSignal<string | undefined>;
571
660
  /**
572
661
  * Maximum number of options to render.
573
662
  *
@@ -594,11 +683,11 @@ declare class TnAutocompleteComponent<T = unknown> implements ControlValueAccess
594
683
  /** Test ID attribute */
595
684
  testId: _angular_core.InputSignal<TnTestIdValue>;
596
685
  /** Test-id base, falling back to the bound control name when `testId` is unset. */
597
- protected resolvedTestId: _angular_core.Signal<TnTestIdValue>;
686
+ protected resolvedTestId: Signal<TnTestIdValue>;
598
687
  /**
599
- * Optional extractor for the per-option test-id discriminator. Defaults to
600
- * the option's `value` (when a string/number) or its `label`. Provide this
601
- * when option values are objects, or to pick a more stable/unique key —
688
+ * Optional extractor for the per-option test-id discriminator. Defaults to the
689
+ * option's `label`, the text actually on screen — provide this to key off a
690
+ * locale-independent field instead, or where an id per record is wanted —
602
691
  * mirrors `tn-select`'s input of the same name.
603
692
  *
604
693
  * @example
@@ -612,6 +701,11 @@ declare class TnAutocompleteComponent<T = unknown> implements ControlValueAccess
612
701
  * invalid, required). All-null when standalone or when `ariaLabel` overrides.
613
702
  */
614
703
  protected readonly fieldAria: _truenas_ui_components.TnFormFieldAriaBindings;
704
+ private readonly defaultLabels;
705
+ /** Resolved labels: an explicit input takes precedence over the DI default. */
706
+ protected readonly resolvedPlaceholder: Signal<string>;
707
+ protected readonly resolvedLoadingText: Signal<string>;
708
+ protected readonly resolvedNoResultsText: Signal<string>;
615
709
  /** Emits the full option (label + value) when one is selected */
616
710
  optionSelected: _angular_core.OutputEmitterRef<TnAutocompleteOption<T>>;
617
711
  /**
@@ -638,11 +732,11 @@ declare class TnAutocompleteComponent<T = unknown> implements ControlValueAccess
638
732
  */
639
733
  opened: _angular_core.OutputEmitterRef<void>;
640
734
  /** Reference to the input element */
641
- inputEl: _angular_core.Signal<ElementRef<HTMLInputElement> | undefined>;
735
+ inputEl: Signal<ElementRef<HTMLInputElement> | undefined>;
642
736
  /** Template for the dropdown panel, portaled into a CDK overlay on open. */
643
737
  private dropdownTemplate;
644
738
  /** Normalized panel max-height as a CSS length string. */
645
- protected panelMaxHeightValue: _angular_core.Signal<string>;
739
+ protected panelMaxHeightValue: Signal<string>;
646
740
  /** Current search term typed by the user */
647
741
  protected searchTerm: _angular_core.WritableSignal<string>;
648
742
  /** Whether the dropdown is open */
@@ -654,11 +748,11 @@ declare class TnAutocompleteComponent<T = unknown> implements ControlValueAccess
654
748
  /** CVA disabled state from the form */
655
749
  private formDisabled;
656
750
  /** Combined disabled state */
657
- isDisabled: _angular_core.Signal<boolean>;
751
+ isDisabled: Signal<boolean>;
658
752
  /** Filtered and capped options */
659
- protected filteredOptions: _angular_core.Signal<TnAutocompleteOption<T>[]>;
753
+ protected filteredOptions: Signal<TnAutocompleteOption<T>[]>;
660
754
  /** Whether there are any results to show */
661
- protected hasResults: _angular_core.Signal<boolean>;
755
+ protected hasResults: Signal<boolean>;
662
756
  private onChange;
663
757
  private onTouched;
664
758
  /** Live overlay holding the dropdown panel, or undefined when closed. */
@@ -961,8 +1055,55 @@ interface AutocompleteHarnessFilters extends BaseHarnessFilters {
961
1055
 
962
1056
  type TnDrawerMode = 'side' | 'over';
963
1057
  type TnDrawerPosition = 'start' | 'end';
1058
+ /**
1059
+ * The accessible name a drawer falls back to when the caller names neither
1060
+ * `ariaLabel` nor `ariaLabelledby` (#214).
1061
+ *
1062
+ * `ariaLabel` defaults to `undefined`, so the DEFAULT rendering in `over` mode
1063
+ * was a `role="dialog"` with `aria-modal="true"` and no name — measured as an
1064
+ * `aria-dialog-name` violation. In `side` mode the same omission leaves a
1065
+ * `role="navigation"` landmark unnamed, which axe does not report while there is
1066
+ * only one of them on the page, and which stops telling them apart the moment
1067
+ * there are two.
1068
+ *
1069
+ * One fallback for both modes rather than one per mode: the drawer is the same
1070
+ * surface either way, the name answers the same question ("what is this?"), and
1071
+ * a rule that changes with the mode is one more thing for a caller to be wrong
1072
+ * about. A generic name is still a poor one, so it is paired with the dev-mode
1073
+ * warning `tnAccessibleName` raises.
1074
+ *
1075
+ * Exported so specs assert against it by name rather than by a copied literal.
1076
+ */
1077
+ declare const TN_DRAWER_DEFAULT_LABEL = "Drawer";
1078
+ /**
1079
+ * A drawer, which is two different things by `mode`: in `side` it is
1080
+ * persistent navigation beside the page's content, and in `over` it is a modal
1081
+ * dialog with focus trapped in it.
1082
+ *
1083
+ * FOCUS ON OPEN, IN `over` MODE ONLY
1084
+ * ----------------------------------
1085
+ * An `over` drawer moves focus to the panel container when it opens, whatever
1086
+ * you projected into it, so that a screen reader announces the dialog it has
1087
+ * just entered before any control in it. A `side` drawer does not: navigation
1088
+ * that appears beside the content must not take focus from the page.
1089
+ *
1090
+ * **`[cdkFocusInitial]` is not honoured** (#227). It used to be, through the
1091
+ * CDK auto-capture this replaced, and `cdkTrapFocus` is still on the panel — so
1092
+ * the marker looks like it should work and does not. To focus a control of your
1093
+ * own, focus it yourself once the drawer is open; the component leaves focus
1094
+ * alone as soon as it is inside the panel. `lib/a11y/initial-focus.ts` holds
1095
+ * the reasoning for capturing the container rather than a control.
1096
+ */
964
1097
  declare class TnDrawerComponent implements OnDestroy {
965
1098
  private readonly document;
1099
+ /**
1100
+ * The host, which contains the `side`-mode panel. The `over`-mode one is
1101
+ * portaled out to `document.body`, so "does this drawer hold focus" is a
1102
+ * question about both this and `overlayRef`.
1103
+ */
1104
+ private readonly hostRef;
1105
+ /** For the `afterNextRender` an effect below schedules, outside injection context. */
1106
+ private readonly injector;
966
1107
  /** Whether the drawer sits alongside content ('side') or overlays it ('over') */
967
1108
  mode: _angular_core.InputSignal<TnDrawerMode>;
968
1109
  /** Whether the drawer is open. Two-way bindable via [(opened)] */
@@ -975,29 +1116,100 @@ declare class TnDrawerComponent implements OnDestroy {
975
1116
  position: _angular_core.InputSignal<TnDrawerPosition>;
976
1117
  /** Accessible label for the drawer panel */
977
1118
  ariaLabel: _angular_core.InputSignal<string | undefined>;
1119
+ /** IDREF naming the drawer panel from visible text elsewhere on the page */
1120
+ ariaLabelledby: _angular_core.InputSignal<string | null>;
978
1121
  /**
979
1122
  * Test-id applied to the drawer panel. Rendered under whichever attribute name is
980
1123
  * configured via `TN_TEST_ATTR` (default `data-testid`).
981
1124
  */
982
1125
  testId: _angular_core.InputSignal<TnTestIdValue>;
983
- /** Fires after the open transition completes */
1126
+ /**
1127
+ * Fires once the drawer has finished opening.
1128
+ *
1129
+ * "Finished" means the open transition ended, OR that it was going to take
1130
+ * longer than `TN_TRANSITION_FALLBACK_MS` to say so — which is what a user
1131
+ * with `prefers-reduced-motion: reduce` gets, since this component's own
1132
+ * stylesheet removes the transition for them and one that does not run fires
1133
+ * no `transitionend` (#218). A consumer may assume the drawer has reached its
1134
+ * open state and that `opened()` is true; it may NOT assume the animation is
1135
+ * visually complete, because for that user there was none.
1136
+ */
984
1137
  openedComplete: _angular_core.OutputEmitterRef<void>;
985
- /** Fires after the close transition completes */
1138
+ /**
1139
+ * Fires once the drawer has finished closing. Same guarantee as
1140
+ * `openedComplete`, and the same caveat: it reports the state, not the
1141
+ * animation.
1142
+ *
1143
+ * Focus restoration does NOT hang off this — it happens as soon as the drawer
1144
+ * closes (#214). See the effect in the constructor.
1145
+ */
986
1146
  closed: _angular_core.OutputEmitterRef<void>;
987
1147
  /** Whether the component has rendered (prevents transition flash on load) */
988
1148
  protected initialized: _angular_core.WritableSignal<boolean>;
989
1149
  /** Reference to the overlay element (portaled to body in over mode) */
990
1150
  protected overlayRef: _angular_core.Signal<ElementRef<any> | undefined>;
1151
+ /**
1152
+ * The `over`-mode panel, which is what focus moves to when a modal drawer
1153
+ * opens. Optional rather than required: it lives inside an `@if` on the mode,
1154
+ * so a `side` drawer never renders it.
1155
+ */
1156
+ private overPanelRef;
1157
+ /**
1158
+ * Whichever panel is currently rendered — the `side` one or the `over` one.
1159
+ *
1160
+ * Both template branches carry `#panel` and the `@if` on `mode` renders
1161
+ * exactly one, so this is "the drawer's panel" without the caller having to
1162
+ * ask which mode it is in. A drawer whose `mode` changes at runtime destroys
1163
+ * one element and builds the other, and this query re-answers with it.
1164
+ */
1165
+ private panelRef;
1166
+ /**
1167
+ * Whether the panel carries a real tab stop rather than its resting `-1`
1168
+ * (#270).
1169
+ *
1170
+ * `.tn-drawer__panel` is the element with `overflow-y: auto`, so a drawer
1171
+ * whose projected content is taller than it is scrolls here — and until this
1172
+ * ticket nothing about it was reachable from a keyboard unless the caller
1173
+ * happened to project a tabbable control. The measurement, the observers that
1174
+ * keep it current and the rule that decides when the tab stop may be given
1175
+ * back are `tnScrollableRegion`'s; see `../a11y/scrollable-region.ts`, which
1176
+ * is also where the reasoning for holding it on while the panel has focus is
1177
+ * set out.
1178
+ *
1179
+ * A field initializer rather than the constructor, because it registers an
1180
+ * `effect` and so needs an injection context.
1181
+ */
1182
+ protected panelKeyboardReachable: _angular_core.Signal<boolean>;
991
1183
  /** Focus trap should be active only in 'over' mode when open */
992
1184
  protected trapFocus: _angular_core.Signal<boolean>;
993
1185
  /** Role depends on mode: navigation for side, dialog for over */
994
1186
  protected panelRole: _angular_core.Signal<"dialog" | "navigation">;
1187
+ /**
1188
+ * The name to render as `aria-label`, or `null` to render none — and the
1189
+ * dev-mode warning when the caller named neither input.
1190
+ *
1191
+ * Both halves live in `../a11y/accessible-name`, shared with `tn-side-panel`
1192
+ * and the three progressbars, where the reasoning for each branch is set out:
1193
+ * why an explicit `ariaLabel` always survives, and why the generic fallback is
1194
+ * withheld beside an `ariaLabelledby`.
1195
+ *
1196
+ * A field initializer rather than the constructor, because it registers an
1197
+ * `effect` and so needs an injection context.
1198
+ */
1199
+ protected resolvedAriaLabel: _angular_core.Signal<string | null>;
995
1200
  /** Whether to show the backdrop */
996
1201
  protected showBackdrop: _angular_core.Signal<boolean>;
997
1202
  /** CSS classes for the drawer panel */
998
1203
  protected drawerClasses: _angular_core.Signal<string[]>;
999
1204
  /** Previous focus element for restoration (only captured in over mode) */
1000
1205
  private previousFocus;
1206
+ /**
1207
+ * Decides when an open or a close counts as finished, so that the outputs
1208
+ * above fire exactly once per change whether or not a transition ran. A field
1209
+ * initializer rather than the constructor, because it registers an `effect`
1210
+ * and so needs an injection context.
1211
+ */
1212
+ private lifecycle;
1001
1213
  constructor();
1002
1214
  ngOnDestroy(): void;
1003
1215
  /** Open the drawer */
@@ -1014,21 +1226,116 @@ declare class TnDrawerComponent implements OnDestroy {
1014
1226
  * to dismiss them. The header toggle button is the intended control.
1015
1227
  */
1016
1228
  protected onKeydown(event: KeyboardEvent): void;
1017
- /** Handle transition end — emit events and restore focus after animation completes */
1229
+ /**
1230
+ * Handle transition end — report the open/close early, since the animation is
1231
+ * demonstrably over. Focus restoration is NOT here; it happens as soon as the
1232
+ * drawer closes, because this event does not fire under
1233
+ * `prefers-reduced-motion` — and neither, for the same reason, does the
1234
+ * emission depend on it any more (#218).
1235
+ *
1236
+ * Which output to emit is `lifecycle`'s to decide, from the state the change
1237
+ * it is tracking settled into — NOT from `opened()` read here. The two differ
1238
+ * exactly when this event is late: a drawer reopened while the close was still
1239
+ * animating reads `opened() === true` on the stale close's event.
1240
+ */
1018
1241
  protected onTransitionEnd(event: TransitionEvent): void;
1019
1242
  private restoreFocus;
1020
1243
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnDrawerComponent, never>;
1021
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnDrawerComponent, "tn-drawer", never, { "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "opened": { "alias": "opened"; "required": false; "isSignal": true; }; "disableClose": { "alias": "disableClose"; "required": false; "isSignal": true; }; "width": { "alias": "width"; "required": false; "isSignal": true; }; "position": { "alias": "position"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, { "opened": "openedChange"; "openedComplete": "openedComplete"; "closed": "closed"; }, never, ["*"], true, never>;
1244
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnDrawerComponent, "tn-drawer", never, { "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "opened": { "alias": "opened"; "required": false; "isSignal": true; }; "disableClose": { "alias": "disableClose"; "required": false; "isSignal": true; }; "width": { "alias": "width"; "required": false; "isSignal": true; }; "position": { "alias": "position"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "ariaLabelledby"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, { "opened": "openedChange"; "openedComplete": "openedComplete"; "closed": "closed"; }, never, ["*"], true, never>;
1022
1245
  }
1023
1246
 
1247
+ /**
1248
+ * The flex row that lays a `tn-drawer` out beside a `tn-drawer-content`.
1249
+ *
1250
+ * WHY IT CARRIES NO ROLE, NO LANDMARK AND NO NAME (#214)
1251
+ * -----------------------------------------------------
1252
+ * #214 reported axe evaluating ZERO rules against this component, which is true
1253
+ * and is not a defect in it: the container renders `<ng-content />` and a
1254
+ * stylesheet, so scanned on its own there is nothing there to have a role. The
1255
+ * scan in the report was the childless case, the same way #204's turned out to
1256
+ * be a stepper with no steps.
1257
+ *
1258
+ * The surface a user perceives is `tn-drawer`, and that is where the model is
1259
+ * declared — `role="navigation"` in `side` mode, `role="dialog"` with
1260
+ * `aria-modal` and a focus trap in `over` mode. Giving the container a role of
1261
+ * its own would put a second, unnamed thing in the accessibility tree between a
1262
+ * listener and that surface, describing a layout box.
1263
+ *
1264
+ * `role="main"` on `tn-drawer-content` is the other tempting one, and it belongs
1265
+ * to the application rather than to this library: a page decides where its main
1266
+ * landmark is, and a component library that claims it makes two `main`s the
1267
+ * moment an app has its own.
1268
+ *
1269
+ * Guarded by `drawer-a11y.spec.ts`, which asserts the drawer inside the
1270
+ * container is what axe attributes its results to.
1271
+ */
1024
1272
  declare class TnDrawerContainerComponent {
1025
1273
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnDrawerContainerComponent, never>;
1026
1274
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnDrawerContainerComponent, "tn-drawer-container", never, {}, {}, never, ["*"], true, never>;
1027
1275
  }
1028
1276
 
1277
+ /**
1278
+ * The name this region takes when it becomes focusable (#270).
1279
+ *
1280
+ * A focusable element with no accessible name is announced as a bare "group",
1281
+ * which tells a listener that something has been reached and nothing about what
1282
+ * it is.
1283
+ *
1284
+ * "Content" rather than "Main content", and `role="group"` rather than
1285
+ * `role="main"`: the landmark belongs to the application, not to this library —
1286
+ * `drawer-container.component.ts` sets out why, and a component that claimed it
1287
+ * would give a page two `main`s the moment the page declared its own. A group
1288
+ * is not summarised in a landmarks list, so this names the scroll region
1289
+ * without competing with the page's own structure.
1290
+ *
1291
+ * Overridable through `ariaLabel`, on the same reasoning as
1292
+ * `TN_SIDE_PANEL_CONTENT_LABEL`: a string this library renders into a
1293
+ * consumer's UI has to be translatable, and a consumer who knows what the
1294
+ * region holds can say so. Exported so specs assert against it by name rather
1295
+ * than by a copied literal.
1296
+ */
1297
+ declare const TN_DRAWER_CONTENT_LABEL = "Content";
1298
+ /**
1299
+ * The page content that sits beside a `tn-drawer` inside a
1300
+ * `tn-drawer-container`.
1301
+ *
1302
+ * WHY IT CARRIES A TAB STOP SOMETIMES AND NO LANDMARK EVER (#270)
1303
+ * --------------------------------------------------------------
1304
+ * The host is `overflow: auto`, so everything an application puts beside its
1305
+ * drawer scrolls in this element — and axe's `scrollable-region-focusable`
1306
+ * reports a scroll container that is neither in the tab order nor holds
1307
+ * anything that is. That is not a technicality here: this is the element that
1308
+ * holds a page, so content below its fold is most of the page, and a keyboard
1309
+ * user with no pointer could not reach it.
1310
+ *
1311
+ * The tab stop follows the measurement rather than being permanent, because
1312
+ * this component wraps every page that uses a drawer and a stop that announces
1313
+ * a group and does nothing would land on all of them. `tnScrollableRegion`
1314
+ * holds the measurement, the observers that keep it current, and the rule that
1315
+ * decides when it may be taken away again; see `../a11y/scrollable-region.ts`.
1316
+ *
1317
+ * The role is still not a landmark. `drawer-container.component.ts` explains
1318
+ * why `role="main"` belongs to the application rather than to this library, and
1319
+ * nothing about needing a tab stop changes that — see
1320
+ * `TN_DRAWER_CONTENT_LABEL`.
1321
+ */
1029
1322
  declare class TnDrawerContentComponent {
1323
+ private readonly hostRef;
1324
+ /**
1325
+ * Accessible name for the scrolling region, which is named only while it is
1326
+ * focusable — see `TN_DRAWER_CONTENT_LABEL`. Override it to translate it, or
1327
+ * to say what the region holds ("Pool details").
1328
+ */
1329
+ ariaLabel: _angular_core.InputSignal<string>;
1330
+ /**
1331
+ * Whether the region carries the tab stop, its role and its name — which is
1332
+ * NOT the same question as whether it currently overflows. All three are
1333
+ * gated together, and held on while the region has focus; the reasoning for
1334
+ * both is in `../a11y/scrollable-region.ts`.
1335
+ */
1336
+ protected keyboardReachable: _angular_core.Signal<boolean>;
1030
1337
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnDrawerContentComponent, never>;
1031
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnDrawerContentComponent, "tn-drawer-content", never, {}, {}, never, ["*"], true, never>;
1338
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnDrawerContentComponent, "tn-drawer-content", never, { "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
1032
1339
  }
1033
1340
 
1034
1341
  /**
@@ -1152,11 +1459,15 @@ declare class TnBannerComponent {
1152
1459
  /**
1153
1460
  * Get the appropriate icon name based on banner type
1154
1461
  */
1155
- iconName: _angular_core.Signal<"information" | "alert" | "alert-circle" | "check-circle">;
1462
+ iconName: _angular_core.Signal<"alert" | "information" | "alert-circle" | "check-circle">;
1156
1463
  /**
1157
- * Get ARIA role based on banner type
1158
- * Error/warning use 'alert' for immediate attention
1159
- * Info/success use 'status' for polite announcements
1464
+ * The live-region role, which is also the only thing declaring how urgently
1465
+ * the banner is announced: `alert` implies `aria-live="assertive"` and
1466
+ * `status` implies `polite`. The template carries no `aria-live`, because an
1467
+ * explicit one would override this.
1468
+ *
1469
+ * The mapping is shared with toast rather than restated here — see
1470
+ * `../a11y/live-region.ts` for which severities interrupt and why.
1160
1471
  */
1161
1472
  ariaRole: _angular_core.Signal<"alert" | "status">;
1162
1473
  /**
@@ -1167,9 +1478,101 @@ declare class TnBannerComponent {
1167
1478
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnBannerComponent, "tn-banner", never, { "heading": { "alias": "heading"; "required": false; "isSignal": true; }; "message": { "alias": "message"; "required": false; "isSignal": true; }; "type": { "alias": "type"; "required": false; "isSignal": true; }; "bordered": { "alias": "bordered"; "required": false; "isSignal": true; }; }, {}, ["actionContent"], ["*", "[tnBannerAction]"], true, never>;
1168
1479
  }
1169
1480
 
1481
+ /**
1482
+ * Harness for a single action projected into a `tn-banner`'s action slot.
1483
+ *
1484
+ * Reach these through `TnBannerHarness.getActions()` / `clickAction()` rather
1485
+ * than loading them directly — on their own they would also match a
1486
+ * `[tnBannerAction]` element that some other component projects.
1487
+ *
1488
+ * `[tnBannerAction]` is an attribute directive with no element restriction, so
1489
+ * an action is whatever the caller projected: a `tn-button`, a plain `<a>`, a
1490
+ * native `<button>`. This harness covers all of them rather than assuming
1491
+ * `tn-button`, which is why `TnBannerHarness` does not simply locate
1492
+ * `TnButtonHarness` the way `TnDialogHarness` does — a dialog's footer takes
1493
+ * `tn-button`s, a banner's action slot takes anything.
1494
+ *
1495
+ * @example
1496
+ * ```typescript
1497
+ * const banner = await loader.getHarness(TnBannerHarness);
1498
+ * const actions = await banner.getActions();
1499
+ * expect(await actions[0].getLabel()).toBe('Retry');
1500
+ * await actions[0].click();
1501
+ * ```
1502
+ */
1503
+ declare class TnBannerActionHarness extends ComponentHarness {
1504
+ /**
1505
+ * The selector for the host element of a banner action.
1506
+ */
1507
+ static hostSelector: string;
1508
+ private _buttonLabel;
1509
+ private _innerControl;
1510
+ /**
1511
+ * Gets a `HarnessPredicate` that can be used to search for a banner action
1512
+ * with a specific label.
1513
+ *
1514
+ * Scope it to the action slot when composing a locator by hand — unscoped, it
1515
+ * matches any `[tnBannerAction]` element on the page, which is why
1516
+ * `TnBannerHarness` always passes an `ancestor`.
1517
+ *
1518
+ * @param options Options for filtering which action instances are considered a match.
1519
+ * @returns A `HarnessPredicate` configured with the given options.
1520
+ *
1521
+ * @example
1522
+ * ```typescript
1523
+ * const retry = await loader.getHarness(
1524
+ * TnBannerActionHarness.with({ label: 'Retry', ancestor: '.tn-banner__action' })
1525
+ * );
1526
+ * ```
1527
+ */
1528
+ static with(options?: BannerActionHarnessFilters): HarnessPredicate<TnBannerActionHarness>;
1529
+ /**
1530
+ * Gets the action's label text.
1531
+ *
1532
+ * For a projected `tn-button` this is the button's own label, read from its
1533
+ * label span so a rendered icon's sprite-fallback glyphs never leak in — the
1534
+ * same rule `TnButtonHarness.getLabel()` follows. For anything else it is the
1535
+ * element's trimmed text content.
1536
+ *
1537
+ * @returns Promise resolving to the action's label, trimmed of whitespace.
1538
+ *
1539
+ * @example
1540
+ * ```typescript
1541
+ * const [action] = await banner.getActions();
1542
+ * expect(await action.getLabel()).toBe('Learn more');
1543
+ * ```
1544
+ */
1545
+ getLabel(): Promise<string>;
1546
+ /**
1547
+ * Clicks the action.
1548
+ *
1549
+ * Clicks the inner `button`/`a` when the action is a component that renders
1550
+ * one — a `tn-button` listens on that inner element, and a click dispatched
1551
+ * at the `tn-button` host would not reach it. Otherwise clicks the projected
1552
+ * element itself.
1553
+ *
1554
+ * @returns Promise that resolves when the click action is complete.
1555
+ *
1556
+ * @example
1557
+ * ```typescript
1558
+ * const [action] = await banner.getActions();
1559
+ * await action.click();
1560
+ * ```
1561
+ */
1562
+ click(): Promise<void>;
1563
+ }
1564
+ /**
1565
+ * A set of criteria that can be used to filter a list of `TnBannerActionHarness` instances.
1566
+ */
1567
+ interface BannerActionHarnessFilters extends BaseHarnessFilters {
1568
+ /** Filters by the action's label text. Supports string or regex matching. */
1569
+ label?: string | RegExp;
1570
+ }
1571
+
1170
1572
  /**
1171
1573
  * Harness for interacting with tn-banner in tests.
1172
- * Provides simple text-based querying for existence checks.
1574
+ * Provides text-based querying for existence checks, and access to the actions
1575
+ * the banner projects into its action slot.
1173
1576
  *
1174
1577
  * @example
1175
1578
  * ```typescript
@@ -1185,6 +1588,9 @@ declare class TnBannerComponent {
1185
1588
  * const hasBanner = await loader.hasHarness(
1186
1589
  * TnBannerHarness.with({ textContains: /success/i })
1187
1590
  * );
1591
+ *
1592
+ * // Press one of the banner's actions
1593
+ * await errorBanner.clickAction('Retry');
1188
1594
  * ```
1189
1595
  */
1190
1596
  declare class TnBannerHarness extends ComponentHarness {
@@ -1192,6 +1598,7 @@ declare class TnBannerHarness extends ComponentHarness {
1192
1598
  * The selector for the host element of an `TnBannerComponent` instance.
1193
1599
  */
1194
1600
  static hostSelector: string;
1601
+ private _actions;
1195
1602
  /**
1196
1603
  * Gets a `HarnessPredicate` that can be used to search for a banner
1197
1604
  * with specific text content.
@@ -1226,6 +1633,47 @@ declare class TnBannerHarness extends ComponentHarness {
1226
1633
  * ```
1227
1634
  */
1228
1635
  getText(): Promise<string>;
1636
+ /**
1637
+ * Gets every action the banner projects into its action slot, in DOM order.
1638
+ *
1639
+ * Actions are not necessarily `tn-button`s — `[tnBannerAction]` is an
1640
+ * attribute directive that takes any element — so these come back as
1641
+ * `TnBannerActionHarness`, which reads a label and clicks whatever was
1642
+ * projected. Nothing is filtered out by element type.
1643
+ *
1644
+ * @returns Promise resolving to an array of `TnBannerActionHarness` instances.
1645
+ *
1646
+ * @example
1647
+ * ```typescript
1648
+ * const banner = await loader.getHarness(TnBannerHarness);
1649
+ * const actions = await banner.getActions();
1650
+ * expect(actions).toHaveLength(2);
1651
+ * expect(await actions[0].getLabel()).toBe('Retry');
1652
+ * ```
1653
+ */
1654
+ getActions(): Promise<TnBannerActionHarness[]>;
1655
+ /**
1656
+ * Clicks one of the banner's actions by its label, matching the first in DOM
1657
+ * order. Only matches inside the `.tn-banner__action` slot, not controls in
1658
+ * the banner's default content.
1659
+ *
1660
+ * Named `clickAction` rather than `TnDialogHarness`'s `clickActionButton`
1661
+ * because a banner action need not be a button: a projected `<a
1662
+ * tnBannerAction>` is reached by this method exactly as a `tn-button` is.
1663
+ *
1664
+ * @param label The action label to match. Supports string or regex.
1665
+ * @throws Error naming the label, and the labels actually present, if nothing matches.
1666
+ *
1667
+ * @example
1668
+ * ```typescript
1669
+ * const banner = await loader.getHarness(
1670
+ * TnBannerHarness.with({ textContains: 'network error' })
1671
+ * );
1672
+ * await banner.clickAction('Retry');
1673
+ * await banner.clickAction(/learn more/i);
1674
+ * ```
1675
+ */
1676
+ clickAction(label: string | RegExp): Promise<void>;
1229
1677
  }
1230
1678
  /**
1231
1679
  * A set of criteria that can be used to filter a list of `TnBannerHarness` instances.
@@ -1266,7 +1714,7 @@ declare class TnButtonComponent implements AfterViewInit {
1266
1714
  * form's `(submit)`/`(ngSubmit)` handler; a `(onClick)` binding alone does
1267
1715
  * not. Ignored in anchor mode (`href`/`routerLink`).
1268
1716
  */
1269
- type: _angular_core.InputSignal<"button" | "submit" | "reset">;
1717
+ type: _angular_core.InputSignal<"button" | "reset" | "submit">;
1270
1718
  /**
1271
1719
  * Semantic test-id base for the rendered element. The library prepends the
1272
1720
  * element type (`button`) and renders the result under whichever attribute
@@ -1514,12 +1962,61 @@ declare class TnTooltipDirective implements AfterViewInit, OnDestroy {
1514
1962
  showDelay: _angular_core.InputSignal<number>;
1515
1963
  hideDelay: _angular_core.InputSignal<number>;
1516
1964
  tooltipClass: _angular_core.InputSignal<string>;
1965
+ /**
1966
+ * Allows the tooltip to be pinned ("stuck") open so its content can be interacted with —
1967
+ * needed for tooltips that contain links or other controls.
1968
+ *
1969
+ * UPGRADING: this defaults to `true`, so an existing tooltip whose message happens to contain a
1970
+ * link changes behaviour with no code change on your side — it stops appearing on hover and on
1971
+ * keyboard focus, and the host's click opens it instead. Nothing else about the host changes.
1972
+ * Anything that forwards a caller-supplied message to a `<button>` is a candidate: inside this
1973
+ * library that is `<tn-form-field [tooltip]>`, `<tn-form-section [tooltip]>`, `<tn-card>`'s
1974
+ * title and action tooltips and `<tn-icon-button [tooltip]>`, all of which render a button and
1975
+ * pass the message straight through. Each re-exports this as a `tooltipSticky` input, so
1976
+ * `[tooltipSticky]="false"` on any of them keeps the old hover behaviour, at the cost of the
1977
+ * link staying out of reach.
1978
+ *
1979
+ * A host whose click is already spoken for wants that opt-out permanently, not on upgrade: a
1980
+ * `tnMenuTrigger` would raise the panel over the menu the same click opens, and lose the hover
1981
+ * hint doing it. `<tn-card>`'s kebab-menu trigger passes `false` for exactly that reason, which
1982
+ * is why it is not in the list above.
1983
+ *
1984
+ * This only narrows the rule in `_isPinnable`, it cannot widen it: plain help text is never
1985
+ * pinnable however this is set, and neither is a message on a host that cannot deliver the
1986
+ * click - see `_isHostClickBlocked`. Setting it to false forces a message that does hold a
1987
+ * link back into plain hover behaviour, where the link is unreachable.
1988
+ *
1989
+ * Pinning is not a second stage layered on hover: a tooltip that can be pinned is opened by
1990
+ * clicking the host and by nothing else, because a tooltip that appeared on hover and then had
1991
+ * to be clicked made the user chase a target that was already on screen. See `_isPinnable`
1992
+ * for which tooltips this applies to.
1993
+ *
1994
+ * While pinned the tooltip renders a dismiss button and ignores `mouseleave` and blur; it
1995
+ * closes on a second click of the host, on the dismiss button, on an outside click, or on
1996
+ * Escape.
1997
+ *
1998
+ * The pinning click is additive, not exclusive: the host's own click handler still runs, so a
1999
+ * `<tn-button (click)="save()">` carrying a message with a link both saves and pins. Suppressing
2000
+ * the host's action would be worse — a menu trigger or a toggle that silently stopped working
2001
+ * because someone put a link in its tooltip — but a host that navigates away should either keep
2002
+ * its tooltip plain or set this to false.
2003
+ */
2004
+ stickyEnabled: _angular_core.InputSignal<boolean>;
2005
+ /** Accessible name for the dismiss button rendered in sticky mode. */
2006
+ closeAriaLabel: _angular_core.InputSignal<string>;
2007
+ /** Accessible name for the panel itself once pinned, where it is announced as a dialog. */
2008
+ panelAriaLabel: _angular_core.InputSignal<string>;
1517
2009
  private _overlayRef;
1518
2010
  private _tooltipInstance;
1519
2011
  private _showTimeout;
1520
2012
  private _hideTimeout;
1521
2013
  private _isTooltipVisible;
2014
+ private _isSticky;
1522
2015
  private _positionSub;
2016
+ private _repositionSub;
2017
+ private _escapeSub;
2018
+ private _outsideClickSub;
2019
+ private _dismissSub;
1523
2020
  private _focusSub;
1524
2021
  private _tooltipId;
1525
2022
  private _overlay;
@@ -1527,18 +2024,58 @@ declare class TnTooltipDirective implements AfterViewInit, OnDestroy {
1527
2024
  private _viewContainerRef;
1528
2025
  private _overlayPositionBuilder;
1529
2026
  private _ariaDescriber;
2027
+ private _scrollDispatcher;
2028
+ private _viewportRuler;
1530
2029
  private _focusMonitor;
1531
2030
  private _ngZone;
1532
2031
  private _viewInitialized;
1533
2032
  private _describedTarget;
1534
2033
  private _describedMessage;
1535
2034
  private _innerObserver;
2035
+ private _popupStateTarget;
2036
+ private _popupStateWritten;
2037
+ private _popupStateSelfWrites;
1536
2038
  /**
1537
- * Re-sync the description (see ngAfterViewInit) when the inputs change. The initial
2039
+ * Which disclosure attributes the host has been seen writing for itself, per element.
2040
+ *
2041
+ * Per element for the same reason `_popupStateSelfWrites` is, and then some: ownership is a
2042
+ * fact about the element that carries the attribute, and the target moves (`<tn-button>`
2043
+ * swapping between `<a>` and `<button>` through an `@if`). Held as bare names it outlived the
2044
+ * element it was learned from and denied the disclosure state to a replacement carrying no
2045
+ * attributes of its own — permanently, since the yielded state leaves `_popupStateTarget`
2046
+ * null and so hid the target change from the reset in `_writeHostPopupState`. Keyed by
2047
+ * element, the replacement starts owning nothing because it does, and the fact survives for
2048
+ * as long as the element it is about.
2049
+ */
2050
+ private _popupStateHostOwned;
2051
+ private _cachedArrowInset;
2052
+ /**
2053
+ * Whether this tooltip is opened by a click and pinned, rather than shown on hover.
2054
+ *
2055
+ * Only messages carrying content the reader can reach earn the click interaction, because they
2056
+ * are the ones a hover tooltip cannot serve — it disappears on the way to the link. Plain help
2057
+ * text, which is the overwhelming majority, keeps the hover behaviour and never pins: pinning
2058
+ * it would cost a click and buy the reader nothing.
2059
+ *
2060
+ * `hasInteractiveContent` is deliberately strict about what counts, since the message is
2061
+ * sanitized before it renders — see `REACHABLE_CONTENT_SELECTOR`.
2062
+ */
2063
+ private readonly _isPinnable;
2064
+ /**
2065
+ * Keeps a panel that is already on screen in step with its inputs.
2066
+ *
2067
+ * `_attachTooltip` seeds these once, which was enough while every panel was a hover panel that
2068
+ * lived for a second. A pinned panel stays up until the user dismisses it, so a message that
2069
+ * changes underneath it would leave the rendered text — and the link the user is about to
2070
+ * click — disagreeing with the description `_syncAriaDescription` has already moved on to.
2071
+ */
2072
+ private readonly _syncPanelInputs;
2073
+ /**
2074
+ * Re-sync the ARIA attributes (see ngAfterViewInit) when the inputs change. The initial
1538
2075
  * write cannot happen here: on the first run the host's child components (e.g.
1539
2076
  * tn-button's inner `<button>`) have not rendered yet.
1540
2077
  */
1541
- private readonly _syncDescriptionOnInputChange;
2078
+ private readonly _syncAriaOnInputChange;
1542
2079
  /**
1543
2080
  * Expose the message to assistive tech via CDK's AriaDescriber: it keeps the text in
1544
2081
  * a persistent visually-hidden element (so `aria-describedby` never dangles — the
@@ -1549,31 +2086,295 @@ declare class TnTooltipDirective implements AfterViewInit, OnDestroy {
1549
2086
  * because screen readers read descriptions off the focused control, not wrappers.
1550
2087
  */
1551
2088
  ngAfterViewInit(): void;
2089
+ private _syncAria;
2090
+ /**
2091
+ * Watches the host for the things `_syncAria` reads out of the DOM rather than out of a signal,
2092
+ * for as long as any of them can still change the answer.
2093
+ *
2094
+ * Three things move underneath it. The inner control can render after view init (`@if` branches
2095
+ * inside the wrapper swapping, deferred content). `disabled`/`aria-disabled` can be toggled on
2096
+ * it at any time, which decides whether the pinning click can arrive at all, and so whether the
2097
+ * host advertises itself as a disclosure control; `tabindex` and `href` read the same way,
2098
+ * through `_isHostKeyboardOperable` and through `_ariaTarget`'s `INTERACTIVE_SELECTOR` match —
2099
+ * both selectors say `a[href]`, so an anchor dropping its `href` stops being activatable
2100
+ * exactly as one taking `tabindex="-1"` does, and the disclosure state has to come off it. The
2101
+ * disclosure attributes are watched for a different reason: to notice the host writing one of
2102
+ * them itself. See `_absorbPopupStateRecords`, which also keeps our own writes of them from
2103
+ * retriggering this. `aria-describedby` stays outside the filter, so `AriaDescriber` cannot
2104
+ * loop it.
2105
+ *
2106
+ * None of that can matter on a host that is itself the control (`<button tnTooltip>`,
2107
+ * `tn-icon-button`'s inner button — the overwhelming majority) while its tooltip is a plain
2108
+ * hover one: `_ariaTarget` is the host whatever the subtree does, and the rest is read only on
2109
+ * the way to pinning something. A table rendering hundreds of tooltip'd action buttons would
2110
+ * otherwise carry hundreds of live subtree observers with nothing to report, so those hosts go
2111
+ * unobserved until their message turns pinnable — or until `stick()` pins one anyway, which it
2112
+ * will do on a message the host click never would have.
2113
+ */
2114
+ private _observeHost;
2115
+ private _disconnectHostObserver;
2116
+ /**
2117
+ * The element the tooltip's ARIA attributes belong on: the host when it is itself a control,
2118
+ * otherwise its single interactive descendant (e.g. `<tn-button>`'s inner `<button>`), because
2119
+ * screen readers read descriptions and states off the focused control, not off wrappers.
2120
+ *
2121
+ * A container holding several controls keeps them on the host — annotating an arbitrary first
2122
+ * control would attach the text to the wrong element.
2123
+ */
2124
+ private _ariaTarget;
2125
+ /**
2126
+ * Whether the host is in a state where the click that pins the tooltip cannot be relied on.
2127
+ *
2128
+ * A disabled control is: a native disabled `<button>` fires no click at all, and
2129
+ * `<tn-button [disabled]>` swallows the retargeted one in a capture-phase listener before this
2130
+ * directive's host binding runs. `aria-disabled` is the exception that keeps this a rule about
2131
+ * intent rather than about event plumbing — it is advisory, so the element still dispatches
2132
+ * clicks normally. `_onClick` therefore declines to pin for any host this reports, rather than
2133
+ * relying on the click not showing up. Suppressing hover for a pinnable message would then leave the
2134
+ * tooltip with no way in whatsoever — and a disabled control with a tooltip explaining why,
2135
+ * docs link included, is a normal thing to build. Those fall back to plain hover behaviour,
2136
+ * which is what they did before pinning existed: the link stays out of reach, but the
2137
+ * explanation does not.
2138
+ */
2139
+ private _isHostClickBlocked;
2140
+ /**
2141
+ * Whether the element carrying the tooltip's ARIA state can be operated from the keyboard.
2142
+ *
2143
+ * The click is the only way into a pinned panel, so a host that cannot produce one from the
2144
+ * keyboard would put the tooltip out of reach of keyboard users entirely — and would write
2145
+ * `aria-expanded`, advertising a disclosure they cannot operate. Two host shapes fail that way:
2146
+ *
2147
+ * - `_ariaTarget` falls back to the bare host when it is not a control and holds no single
2148
+ * interactive descendant — `<span [tnTooltip]="'… <a href>…'">` is exactly that. It can be
2149
+ * clicked with a pointer, so nothing in `_isHostClickBlocked` catches it, but it cannot be
2150
+ * focused or activated at all, and `aria-expanded` is invalid on its implicit `generic` role.
2151
+ * A host wearing `role="button"` is the same case: the role renames it for assistive tech
2152
+ * without making the browser synthesise a click for it.
2153
+ * - A text control (`<input>`, `<select>`, `<textarea>`) is focusable but not *activatable*:
2154
+ * Enter submits the form and Space types a space, so no click ever arrives. On top of that,
2155
+ * every pointer click into the field — placing the caret — would toggle the panel.
2156
+ *
2157
+ * The question is therefore "does a click on this element mean *activate me*", which is what
2158
+ * `KEYBOARD_ACTIVATABLE_SELECTOR` answers; `INTERACTIVE_SELECTOR` answers the broader "is this
2159
+ * the element ARIA belongs on" and is too wide to stand in for it. Both shapes fall back to
2160
+ * plain hover, which is what they did before pinning existed.
2161
+ *
2162
+ * `tabindex="-1"` does not count: it makes an element a focus target without putting it in the
2163
+ * tab order, and `_restoreFocusTarget` leaves one behind on hosts it had to focus by hand.
2164
+ */
2165
+ private _isHostKeyboardOperable;
2166
+ /**
2167
+ * Whether this tooltip is opened by clicking its host, rather than on hover.
2168
+ *
2169
+ * `_isPinnable` is the message's half of that decision; the host has the other half, and both
2170
+ * have to agree. A host that cannot deliver the pinning click, or cannot be operated from the
2171
+ * keyboard at all, goes back to being a hover tooltip.
2172
+ */
2173
+ private _pinsOnClick;
2174
+ /**
2175
+ * Marks a pinnable host as the disclosure control for its tooltip.
2176
+ *
2177
+ * Nothing about a plain button says "clicking me reveals something", so the host has to carry
2178
+ * the state that does: `aria-expanded` for whether the panel is currently up, `aria-haspopup`
2179
+ * for what kind of thing it opens (a pinned panel is a `dialog` — see `TnTooltipComponent`'s
2180
+ * `sticky`), and `aria-controls` pointing at the panel while it exists, so assistive tech can
2181
+ * jump to it. A hover tooltip reveals nothing on activation and carries none of this.
2182
+ */
2183
+ private _syncHostPopupState;
2184
+ /**
2185
+ * Writes the disclosure attributes, remembering the exact value written for each.
2186
+ *
2187
+ * A host that carries any of them keeps all three, and this directive never removes or
2188
+ * overwrites a value it did not itself put there. Hosts own these legitimately and mean
2189
+ * something else by them: a `tnMenuTrigger` is `aria-haspopup="menu"`, a `<tn-select>` points
2190
+ * `aria-controls` at its own listbox, and `<tn-icon-button [ariaExpanded]>` binds
2191
+ * `aria-expanded` to the same inner `<button>` a `tnTooltip` on it would land on. There is only
2192
+ * one of each attribute to go around, and the host's click is what they describe — a tooltip is
2193
+ * the lesser claim.
2194
+ *
2195
+ * Ownership is not re-derived from the current value each time, because a value comparison
2196
+ * cannot tell "the host wrote nothing" from "the host wrote the same string we did" — and
2197
+ * `aria-expanded="false"` is exactly that string. `<tn-icon-button [ariaExpanded]="expanded()">`
2198
+ * with `expanded()` starting `undefined` walks straight into it: the attribute is absent at the
2199
+ * first sync so the tooltip claims it, the consumer later sets `false`, Angular writes the same
2200
+ * `"false"` the tooltip wrote, and on pin the host's collapsed popup would be announced as
2201
+ * expanded. So a write the tooltip did not make is recorded as a fact when it happens — see
2202
+ * `_absorbPopupStateRecords` — and the host keeps the attribute from then on.
2203
+ *
2204
+ * And it is decided for the group, not per attribute: the three describe one popup between
2205
+ * them, so yielding them one at a time would leave the host describing two. A menu trigger
2206
+ * carrying its own `aria-expanded="true"` would keep that value and still take
2207
+ * `aria-haspopup="dialog"` and an `aria-controls` pointing at the tooltip panel, announcing
2208
+ * "expanded dialog controlling tn-tooltip-xxx" for a panel that may well be closed. So if any
2209
+ * one of them is spoken for, the tooltip writes none of them and the host keeps the coherent
2210
+ * state it owns. Nothing about reaching the panel depends on them: it still opens on the host
2211
+ * click and closes on Escape, an outside click or the dismiss button.
2212
+ */
2213
+ private _writeHostPopupState;
2214
+ /**
2215
+ * Whether the attribute is free to write: never written by the host, and either absent or still
2216
+ * holding the value written for it.
2217
+ */
2218
+ private _ownsHostAttribute;
2219
+ /**
2220
+ * Separates the observer records caused by this directive's own writes from the rest, and
2221
+ * returns whether what is left is worth a re-sync.
2222
+ *
2223
+ * Every mutation of a disclosure attribute arrives here, including the ones `_writeHostPopupState`
2224
+ * just made — which is why `attributeFilter` can list them without looping. Each write registers
2225
+ * itself in `_popupStateSelfWrites` first, and one write produces exactly one record, so the
2226
+ * counter cancels them out one for one. Anything left over came from the host, and that is the
2227
+ * fact worth keeping: the value it wrote may well be the one already there.
2228
+ */
2229
+ private _absorbPopupStateRecords;
2230
+ private _setPopupStateAttribute;
2231
+ /** Removing an attribute that is not there mutates nothing, so it must not be counted either. */
2232
+ private _removePopupStateAttribute;
2233
+ /**
2234
+ * Registers a write of our own so `_absorbPopupStateRecords` can cancel out the record it
2235
+ * produces. Only writes made while the observer is connected produce one — the first
2236
+ * `_syncAria` runs before `ngAfterViewInit` has created it, and counting that one would leave a
2237
+ * credit behind for the host's first write to spend.
2238
+ *
2239
+ * Kept per element rather than per attribute name, because a write and the record it produces
2240
+ * can come apart when the element does. A wrapper swapping its inner control (`<tn-button>`
2241
+ * moving between `<a>` and `<button>` through an `@if`) has `_clearHostPopupState` write to the
2242
+ * outgoing element, which is detached by then and so is no longer observed: no record is ever
2243
+ * queued for it. A single shared ledger would carry those credits over to the incoming element
2244
+ * and absorb the host's own writes there as if they were ours. Per element, they are stranded
2245
+ * on a node nothing consults again, and the WeakMap lets it go.
2246
+ */
2247
+ private _countPopupStateWrite;
2248
+ private _clearHostPopupState;
1552
2249
  private _syncAriaDescription;
1553
- /**
1554
- * The overlay tooltip renders its message as HTML (`[innerHTML]`), but AriaDescriber
1555
- * writes the description as plain text — strip any markup so screen readers never
1556
- * announce literal tags. DOMParser parses inert markup (no script execution).
1557
- */
1558
- private _plainTextMessage;
1559
2250
  private _removeAriaDescription;
1560
2251
  ngOnDestroy(): void;
1561
2252
  _onMouseEnter(): void;
1562
2253
  _onMouseLeave(): void;
2254
+ _onClick(event: MouseEvent): void;
1563
2255
  _onKeydown(event: KeyboardEvent): void;
1564
2256
  /** Shows the tooltip */
1565
2257
  show(delay?: number): void;
1566
- /** Hides the tooltip */
2258
+ /** Hides the tooltip, unpinning it if it was sticky */
1567
2259
  hide(delay?: number): void;
1568
- /** Toggle the tooltip visibility */
2260
+ /**
2261
+ * Toggles the tooltip, opening it the way its host's own click would.
2262
+ *
2263
+ * Routed through `_pinsOnClick` for the same reason `_onClick` is: `show()` on a pinnable
2264
+ * message puts up a decorative, `aria-hidden` panel with `pointer-events: none`, so the link
2265
+ * inside it cannot be clicked and `mouseleave` takes it away again — the unreachable state pinning exists
2266
+ * to replace, which a public method should not be able to produce either.
2267
+ */
1569
2268
  toggle(): void;
2269
+ /** Whether the tooltip is currently pinned open */
2270
+ isSticky(): boolean;
2271
+ /**
2272
+ * Pins the tooltip open. Shows it first if it isn't visible yet, so it works both as a
2273
+ * follow-up to hover and on its own (e.g. a keyboard-activated host).
2274
+ *
2275
+ * This is the imperative escape hatch: it pins any tooltip with a message, ignoring both
2276
+ * `tnTooltipSticky` and the interactive-content rule that decide whether the *host click* pins
2277
+ * one. A tooltip pinned this way behaves like any other pinned tooltip — dismiss button,
2278
+ * Escape, outside click, and a host click all close it again.
2279
+ *
2280
+ * @param options.focusTooltip Move focus into the tooltip, so its content is reachable
2281
+ * without a pointer.
2282
+ */
2283
+ stick(options?: {
2284
+ focusTooltip?: boolean;
2285
+ }): void;
2286
+ /**
2287
+ * Unpins and hides the tooltip.
2288
+ *
2289
+ * @param restoreFocus Move focus back to the host. Used when the tooltip is dismissed from
2290
+ * the keyboard, where focus would otherwise be lost with the removed element.
2291
+ */
2292
+ unstick(restoreFocus?: boolean): void;
2293
+ /**
2294
+ * Drops the pinned flag and re-advertises the host without it.
2295
+ *
2296
+ * Shared by `unstick()` and `_destroyTooltip` rather than done in either, so the flag and the
2297
+ * disclosure state it drives cannot come apart: whichever gets there first does both halves,
2298
+ * and the other finds nothing left to do. Closing routes that do not go through `unstick()` —
2299
+ * Escape on the host, a disabled input, `ngOnDestroy` — still arrive here through the teardown.
2300
+ */
2301
+ private _releaseSticky;
2302
+ /**
2303
+ * Where focus goes when a pinned tooltip is dismissed from the keyboard.
2304
+ *
2305
+ * A non-focusable host never pins on its own click (see `_isHostKeyboardOperable`), but
2306
+ * `stick()` pins whatever it is called on — `<span tnTooltip="… <a href>…">` included — so
2307
+ * focusing the host blindly is a no-op there, and tearing the panel down straight after drops
2308
+ * focus to `<body>`. Prefer the element that would carry the tooltip's
2309
+ * ARIA state, and if even that cannot hold focus, make the host able to: `tabindex="-1"` keeps
2310
+ * it out of the tab order while letting it be a focus target, and is left in place because
2311
+ * removing it again would drop the focus it was added to catch.
2312
+ */
2313
+ private _restoreFocusTarget;
1570
2314
  private _createOverlay;
2315
+ /**
2316
+ * Points the speech-bubble arrow at the host rather than at the panel's own centre.
2317
+ *
2318
+ * The two only coincide when the panel is perfectly centred on its origin. They come apart
2319
+ * whenever the panel is nudged sideways to stay inside the viewport, or when it is resized
2320
+ * after being placed (entering sticky mode), which used to leave the arrow pointing at empty
2321
+ * space next to the control it belongs to.
2322
+ */
2323
+ private _updateArrowOffset;
2324
+ /**
2325
+ * How far the arrow has to stay from the panel's edge: its own half-base plus the corner
2326
+ * radius. Both are read off the rendered panel, so a change in the stylesheet cannot leave a
2327
+ * stale number behind here.
2328
+ *
2329
+ * Cached for as long as the panel is attached. Only the stylesheet can move these, while the
2330
+ * caller runs on every reposition — which, with the reposition scroll strategy, is every 20ms
2331
+ * for as long as the user keeps scrolling; a `getComputedStyle` per frame to re-read two
2332
+ * constants is not worth it.
2333
+ */
2334
+ private _arrowInset;
1571
2335
  private _resolvePosition;
1572
2336
  private _attachTooltip;
2337
+ /**
2338
+ * Keeps the arrow pointing at the host across re-placements that `positionChanges` does not
2339
+ * report.
2340
+ *
2341
+ * CDK emits `positionChanges` from `_applyPosition` behind
2342
+ * `position !== this._lastPosition || scrollVisibility changed` (cdk 21.1.0), so a panel
2343
+ * re-placed at the *same* position with different coordinates — viewport clamping, which is the
2344
+ * case the arrow offset exists for — emits nothing at all. Listening to the events that drive
2345
+ * those re-placements covers it; the side-placement specs in `tooltip.directive.spec.ts` fail
2346
+ * without this. On a genuine flip both paths run, which is two rect reads either way since the
2347
+ * inset is cached.
2348
+ *
2349
+ * Subscribed here rather than in `_createOverlay`, and deliberately after
2350
+ * `this._overlayRef.attach()`, because the arrow has to be measured against the pane's *new*
2351
+ * position. `RepositionScrollStrategy.enable()` reaches `_scrollDispatcher.scrolled()` through
2352
+ * the same shared subject and the same audit window that this does, so the two run in
2353
+ * subscription order — and `enable()` is called from inside `attach()`. Subscribing before it
2354
+ * put `_updateArrowOffset` first on every scroll tick, reading the pane rect from before the
2355
+ * re-placement: on a pinned side-placed panel, a vertical scroll moved `hostRect.top` while
2356
+ * `panelRect.top` still held the previous value, and since a same-position re-placement emits
2357
+ * no `positionChanges` nothing came along to correct it.
2358
+ *
2359
+ * Re-attaching re-runs the CDK's subscriptions too, so this has to be renewed per attach rather
2360
+ * than held for the lifetime of the overlay - hence the teardown in `_destroyTooltip`.
2361
+ */
2362
+ private _subscribeToReposition;
2363
+ /**
2364
+ * Listens for Escape only while the tooltip is pinned. The CDK keyboard dispatcher hands the
2365
+ * event to the top-most overlay that has subscribers and stops there, so a permanent
2366
+ * subscription would let a plain hover tooltip swallow the Escape meant for the dialog it
2367
+ * sits in.
2368
+ */
2369
+ private _subscribeToEscape;
2370
+ /** Dismisses a pinned tooltip when the user clicks anything else on the page. */
2371
+ private _subscribeToOutsideClicks;
2372
+ private _destroyTooltip;
2373
+ private _isFocusInsideTooltip;
1573
2374
  private _getPositions;
1574
2375
  private _clearTimeouts;
1575
2376
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnTooltipDirective, never>;
1576
- static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnTooltipDirective, "[tnTooltip]", never, { "message": { "alias": "tnTooltip"; "required": false; "isSignal": true; }; "position": { "alias": "tnTooltipPosition"; "required": false; "isSignal": true; }; "disabled": { "alias": "tnTooltipDisabled"; "required": false; "isSignal": true; }; "showDelay": { "alias": "tnTooltipShowDelay"; "required": false; "isSignal": true; }; "hideDelay": { "alias": "tnTooltipHideDelay"; "required": false; "isSignal": true; }; "tooltipClass": { "alias": "tnTooltipClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
2377
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnTooltipDirective, "[tnTooltip]", never, { "message": { "alias": "tnTooltip"; "required": false; "isSignal": true; }; "position": { "alias": "tnTooltipPosition"; "required": false; "isSignal": true; }; "disabled": { "alias": "tnTooltipDisabled"; "required": false; "isSignal": true; }; "showDelay": { "alias": "tnTooltipShowDelay"; "required": false; "isSignal": true; }; "hideDelay": { "alias": "tnTooltipHideDelay"; "required": false; "isSignal": true; }; "tooltipClass": { "alias": "tnTooltipClass"; "required": false; "isSignal": true; }; "stickyEnabled": { "alias": "tnTooltipSticky"; "required": false; "isSignal": true; }; "closeAriaLabel": { "alias": "tnTooltipCloseAriaLabel"; "required": false; "isSignal": true; }; "panelAriaLabel": { "alias": "tnTooltipAriaLabel"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
1577
2378
  }
1578
2379
 
1579
2380
  declare class TnIconButtonComponent implements AfterViewInit {
@@ -1596,6 +2397,16 @@ declare class TnIconButtonComponent implements AfterViewInit {
1596
2397
  tooltip: _angular_core.InputSignal<string | undefined>;
1597
2398
  /** Position of the styled tooltip relative to the button. */
1598
2399
  tooltipPosition: _angular_core.InputSignal<TooltipPosition>;
2400
+ /**
2401
+ * Whether a tooltip message holding a link may be pinned open by clicking the button (see
2402
+ * `tnTooltipSticky`). On by default, like the directive.
2403
+ *
2404
+ * It does not make plain tooltips pinnable — an icon button's tooltip is nearly always a label
2405
+ * for its action, and those keep hovering and never touch the click. Set it to false only to
2406
+ * force a message that does hold a link back to hover behaviour, accepting that the link is
2407
+ * then unreachable.
2408
+ */
2409
+ tooltipSticky: _angular_core.InputSignal<boolean>;
1599
2410
  library: _angular_core.InputSignal<IconLibraryType | undefined>;
1600
2411
  /** Extra class(es) applied to the inner icon, e.g. for animations or state colors. */
1601
2412
  iconClass: _angular_core.InputSignal<string>;
@@ -1612,7 +2423,7 @@ declare class TnIconButtonComponent implements AfterViewInit {
1612
2423
  focus(options?: FocusOptions): void;
1613
2424
  ngAfterViewInit(): void;
1614
2425
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnIconButtonComponent, never>;
1615
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnIconButtonComponent, "tn-icon-button", never, { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "dense": { "alias": "dense"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaExpanded": { "alias": "ariaExpanded"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "tooltip": { "alias": "tooltip"; "required": false; "isSignal": true; }; "tooltipPosition": { "alias": "tooltipPosition"; "required": false; "isSignal": true; }; "library": { "alias": "library"; "required": false; "isSignal": true; }; "iconClass": { "alias": "iconClass"; "required": false; "isSignal": true; }; }, { "onClick": "onClick"; }, never, ["*"], true, never>;
2426
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnIconButtonComponent, "tn-icon-button", never, { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "dense": { "alias": "dense"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaExpanded": { "alias": "ariaExpanded"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "tooltip": { "alias": "tooltip"; "required": false; "isSignal": true; }; "tooltipPosition": { "alias": "tooltipPosition"; "required": false; "isSignal": true; }; "tooltipSticky": { "alias": "tooltipSticky"; "required": false; "isSignal": true; }; "library": { "alias": "library"; "required": false; "isSignal": true; }; "iconClass": { "alias": "iconClass"; "required": false; "isSignal": true; }; }, { "onClick": "onClick"; }, never, ["*"], true, never>;
1616
2427
  }
1617
2428
 
1618
2429
  /**
@@ -1764,20 +2575,6 @@ interface IconButtonHarnessFilters extends BaseHarnessFilters {
1764
2575
  size?: string;
1765
2576
  }
1766
2577
 
1767
- declare enum InputType {
1768
- Email = "email",
1769
- Number = "number",
1770
- Password = "password",
1771
- PlainText = "text",
1772
- /**
1773
- * Data-size field: the form model holds a raw byte count (a number), while the
1774
- * field displays/accepts a human-readable string (e.g. `2 GiB`, `500M`, `2 TB`).
1775
- * See `tn-input`'s `sizeStandard` / `sizeDefaultUnit` inputs to tune formatting
1776
- * and bare-number parsing.
1777
- */
1778
- Size = "size"
1779
- }
1780
-
1781
2578
  /**
1782
2579
  * Unit standard used to format and parse data sizes.
1783
2580
  *
@@ -1813,20 +2610,112 @@ declare function formatSize(bytes: number | string | null | undefined, standard?
1813
2610
  * @param standard Unit standard (defaults to IEC base-2).
1814
2611
  */
1815
2612
  declare function parseSize(raw: string | number | null | undefined, defaultUnit?: string, standard?: SizeStandard): number | null;
2613
+ /**
2614
+ * The pieces {@link parseSize} recognized, for a caller that needs the unit the
2615
+ * text was written in and not just the byte count it denotes.
2616
+ *
2617
+ * @internal
2618
+ */
2619
+ interface ParsedSize {
2620
+ /** The byte count the text denotes. */
2621
+ bytes: number;
2622
+ /** The number as it was written, without the unit — `'1500'`, `'1.755'`. */
2623
+ digits: string;
2624
+ /** Power of the base the unit carries (B = 0, K = 1, M = 2, ...). */
2625
+ exponent: number;
2626
+ }
2627
+ /**
2628
+ * {@link parseSize}, keeping the parts rather than collapsing them to a byte
2629
+ * count. The unit matters when the text has to be re-rendered: the number the
2630
+ * user wrote and the unit they wrote it in always denote their byte count
2631
+ * exactly, which a rounded rendering may not.
2632
+ *
2633
+ * @internal
2634
+ */
2635
+ declare function parseSizeParts(raw: string | number | null | undefined, defaultUnit?: string, standard?: SizeStandard): ParsedSize | null;
2636
+ /**
2637
+ * Renders a byte count in the largest unit that expresses it EXACTLY within
2638
+ * `round` decimals, or `null` when no suitable unit does.
2639
+ *
2640
+ * The point is a field the user types into. `formatSize` always picks the
2641
+ * natural unit and rounds to fit — 1 572 864 000 becomes `1.46 GiB`, which is a
2642
+ * fine way to READ a size but a lie to hand back to someone who typed
2643
+ * `1500 MiB`: it no longer denotes the value the field holds. One unit down
2644
+ * usually does denote it exactly, so this looks there.
2645
+ *
2646
+ * It looks exactly one step down and no further, which is what keeps the result
2647
+ * readable: a value that is exact in neither its own unit nor the next is
2648
+ * always exact in bytes, and `1567663063 B` helps nobody. Such a value has no
2649
+ * exact spelling worth showing, so this returns `null` and the caller falls
2650
+ * back to the rounded rendering.
2651
+ *
2652
+ * @param bytes The byte count to render. Non-integer and unsafe magnitudes
2653
+ * return `null` — a byte count is a whole number, and past
2654
+ * `MAX_SAFE_INTEGER` "exactly" stops meaning anything.
2655
+ * @param standard Unit standard (defaults to IEC base-2).
2656
+ * @param round Decimal places the rendering may use (defaults to 2).
2657
+ * @internal
2658
+ */
2659
+ declare function formatSizeExact(bytes: number, standard?: SizeStandard, round?: number): string | null;
2660
+ /**
2661
+ * Formats a byte count for a field the user edits: {@link formatSizeExact} when
2662
+ * some unit states the value exactly, and {@link formatSize}'s rounded
2663
+ * rendering when none does.
2664
+ *
2665
+ * @param bytes The byte count to format.
2666
+ * @param standard Unit standard (defaults to IEC base-2).
2667
+ * @param round Decimal places to round to (defaults to 2).
2668
+ * @internal
2669
+ */
2670
+ declare function formatSizeForEditing(bytes: number | string | null | undefined, standard?: SizeStandard, round?: number): string;
2671
+ /**
2672
+ * Tidies a size the user typed into its canonical spelling, without ever
2673
+ * restating it as a value it does not denote.
2674
+ *
2675
+ * `2048 KiB` becomes `2 MiB` and `200tib` becomes `200 TiB`, because both
2676
+ * renderings are exact. `1500` becomes `1500 MiB` rather than `1.46 GiB`, and
2677
+ * `1.755 GiB` is left at `1.755 GiB`: where no unit expresses the value within
2678
+ * `round` decimals, the number and unit the user wrote do, so those are kept
2679
+ * and only the spelling is normalized.
2680
+ *
2681
+ * Returns `null` for text {@link parseSize} cannot read, which the caller leaves
2682
+ * on screen for its validators to flag.
2683
+ *
2684
+ * @param raw The text to canonicalize.
2685
+ * @param defaultUnit Unit assumed when the input carries no unit (defaults to `MiB`).
2686
+ * @param standard Unit standard (defaults to IEC base-2).
2687
+ * @param round Decimal places a rendering may use (defaults to 2).
2688
+ * @internal
2689
+ */
2690
+ declare function canonicalizeSize(raw: string | number | null | undefined, defaultUnit?: string, standard?: SizeStandard, round?: number): string | null;
1816
2691
 
1817
- declare class TnInputComponent implements AfterViewInit, OnDestroy, ControlValueAccessor {
1818
- inputEl: _angular_core.Signal<ElementRef<HTMLInputElement | HTMLTextAreaElement>>;
1819
- inputType: _angular_core.InputSignal<InputType>;
1820
- placeholder: _angular_core.InputSignal<string>;
1821
- testId: _angular_core.InputSignal<TnTestIdValue>;
1822
- /** Test-id base, falling back to the bound control name when `testId` is unset. */
1823
- protected resolvedTestId: _angular_core.Signal<TnTestIdValue>;
1824
- disabled: _angular_core.InputSignal<boolean>;
1825
- multiline: _angular_core.InputSignal<boolean>;
1826
- rows: _angular_core.InputSignal<number>;
1827
- /**
1828
- * Explicit accessible name for the control. Rendered as `aria-label` on the
1829
- * input/textarea. Inside a `tn-form-field` the field's label is associated
2692
+ declare enum InputType {
2693
+ Email = "email",
2694
+ Number = "number",
2695
+ Password = "password",
2696
+ PlainText = "text",
2697
+ /**
2698
+ * Data-size field: the form model holds a raw byte count (a number), while the
2699
+ * field displays/accepts a human-readable string (e.g. `2 GiB`, `500M`, `2 TB`).
2700
+ * See `tn-input`'s `sizeStandard` / `sizeDefaultUnit` inputs to tune formatting
2701
+ * and bare-number parsing.
2702
+ */
2703
+ Size = "size"
2704
+ }
2705
+
2706
+ declare class TnInputComponent implements AfterViewInit, OnDestroy, ControlValueAccessor {
2707
+ inputEl: _angular_core.Signal<ElementRef<HTMLInputElement | HTMLTextAreaElement>>;
2708
+ inputType: _angular_core.InputSignal<InputType>;
2709
+ placeholder: _angular_core.InputSignal<string>;
2710
+ testId: _angular_core.InputSignal<TnTestIdValue>;
2711
+ /** Test-id base, falling back to the bound control name when `testId` is unset. */
2712
+ protected resolvedTestId: _angular_core.Signal<TnTestIdValue>;
2713
+ disabled: _angular_core.InputSignal<boolean>;
2714
+ multiline: _angular_core.InputSignal<boolean>;
2715
+ rows: _angular_core.InputSignal<number>;
2716
+ /**
2717
+ * Explicit accessible name for the control. Rendered as `aria-label` on the
2718
+ * input/textarea. Inside a `tn-form-field` the field's label is associated
1830
2719
  * automatically (via `aria-labelledby`), so leave this unset there unless the
1831
2720
  * announced name must differ from the visible label — when set, it wins. Set
1832
2721
  * it for standalone usage so the control isn't unnamed in the a11y tree.
@@ -1894,6 +2783,12 @@ declare class TnInputComponent implements AfterViewInit, OnDestroy, ControlValue
1894
2783
  /**
1895
2784
  * Decimal places used when formatting a `Size` field's value for display
1896
2785
  * (e.g. `1.5 GiB`). Ignored unless `inputType` is `Size`.
2786
+ *
2787
+ * Display only: the form model always holds the exact byte count the typed
2788
+ * text denotes, and is never re-read from the display. The display avoids
2789
+ * rounding where it can — `1500M` shows as `1500 MiB`, not as a `1.46 GiB`
2790
+ * that means 1 567 663 063 — and where a value fits no unit exactly (a size
2791
+ * read back from the server, say) it is rounded for reading only.
1897
2792
  */
1898
2793
  sizeRound: _angular_core.InputSignal<number>;
1899
2794
  /**
@@ -2598,6 +3493,16 @@ declare class TnChipComponent implements AfterViewInit, OnDestroy {
2598
3493
  classes: _angular_core.Signal<string[]>;
2599
3494
  handleClick(event: MouseEvent): void;
2600
3495
  handleClose(event: MouseEvent): void;
3496
+ /**
3497
+ * Handles the chip's Delete/Backspace dismiss shortcut. Bound to both the
3498
+ * body and the close button so the shortcut works wherever focus sits inside
3499
+ * the chip; the wrapper between them carries no role and is not focusable,
3500
+ * so it is not a legitimate place to hang a key handler.
3501
+ *
3502
+ * Enter and Space are deliberately absent: the body is a native `<button>`,
3503
+ * which already turns both into a `click`. Handling them here as well would
3504
+ * emit `onClick` twice per keypress.
3505
+ */
2601
3506
  handleKeyDown(event: KeyboardEvent): void;
2602
3507
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnChipComponent, never>;
2603
3508
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnChipComponent, "tn-chip", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "closable": { "alias": "closable"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, { "onClose": "onClose"; "onClick": "onClick"; }, never, never, true, never>;
@@ -2627,6 +3532,7 @@ declare class TnChipHarness extends ComponentHarness {
2627
3532
  */
2628
3533
  static hostSelector: string;
2629
3534
  private _chip;
3535
+ private _body;
2630
3536
  private _label;
2631
3537
  private _icon;
2632
3538
  private _closeButton;
@@ -2863,6 +3769,20 @@ declare class TnChipInputComponent<T = string> implements ControlValueAccessor,
2863
3769
  testId: _angular_core.InputSignal<TnTestIdValue>;
2864
3770
  /** Test-id base, falling back to the bound control name when `testId` is unset. */
2865
3771
  protected resolvedTestId: _angular_core.Signal<TnTestIdValue>;
3772
+ /**
3773
+ * Optional extractor for the per-option test-id discriminator, applied to both
3774
+ * a suggestion row and the chip it becomes. Defaults to the option's `label`,
3775
+ * the text actually on screen — provide this to key off a locale-independent
3776
+ * field instead, or where two options share a display name and the derived ids
3777
+ * would otherwise collide. Free-text chips have no option to extract from and
3778
+ * stay named by their own value.
3779
+ *
3780
+ * @example
3781
+ * ```html
3782
+ * <tn-chip-input testId="users" [optionTestIdKey]="(o) => o.value.id" ... />
3783
+ * ```
3784
+ */
3785
+ optionTestIdKey: _angular_core.InputSignal<((option: TnChipInputOption<T>) => string | number | null | undefined) | undefined>;
2866
3786
  /** Emits the committed value whenever a chip is added. */
2867
3787
  chipAdded: _angular_core.OutputEmitterRef<T>;
2868
3788
  /** Emits the removed value whenever a chip is removed. */
@@ -2924,19 +3844,27 @@ declare class TnChipInputComponent<T = string> implements ControlValueAccessor,
2924
3844
  /**
2925
3845
  * Scopes a per-chip test id beneath the component's base.
2926
3846
  *
2927
- * The discriminator is the value itself when primitive, else the matching
2928
- * option's label — `String(value)` on an object is `[object Object]`, which
2929
- * would stamp an identical id on every chip. Duplicates are worse than
2930
- * absence for automation, so an object value with no option to name it yet
2931
- * (options still loading) stays attribute-free rather than colliding. A primitive
2932
- * that normalizes away is dropped for the same reason — see
2933
- * {@link discriminatedTestId}.
3847
+ * A chip backed by an option goes through the same {@link optionTestId}
3848
+ * derivation as the suggestion row that created it, so the two carry the same
3849
+ * discriminator by construction rather than one naming the label and the other
3850
+ * the value — including whatever fallback that shared rule settles on, and any
3851
+ * `optionTestIdKey` override.
3852
+ *
3853
+ * A value with no matching option is named by itself when it is a primitive:
3854
+ * either a free-text chip, which is its own text, or an option-backed value
3855
+ * whose options have not arrived yet — an async `[options]` load moves such a
3856
+ * chip's id from the value to the resolved label once it does. An object value
3857
+ * with no match cannot stand in for itself, because `String(value)` on an
3858
+ * object is `[object Object]` and would stamp an identical id on every chip.
3859
+ * Duplicates are worse than absence for automation, so that chip stays
3860
+ * attribute-free rather than colliding. A primitive that normalizes away is
3861
+ * dropped for the same reason — see {@link discriminatedTestId}.
2934
3862
  */
2935
3863
  protected chipTestId(value: T): TnTestIdValue;
2936
3864
  /**
2937
3865
  * Scopes a per-suggestion test id beneath the component's base, via the shared
2938
3866
  * dropdown-option derivation. Unlike `tn-select` / `tn-autocomplete`, which
2939
- * emit an unscoped `option-<value>` when they have no base, an unidentified
3867
+ * emit an unscoped `option-<label>` when they have no base, an unidentified
2940
3868
  * chip-input stays attribute-free — its rows carry no page-unique id, so
2941
3869
  * emitting one would invite collisions between inputs.
2942
3870
  */
@@ -2958,6 +3886,24 @@ declare class TnChipInputComponent<T = string> implements ControlValueAccessor,
2958
3886
  private commitText;
2959
3887
  /** Commits a resolved value, honouring duplicate and cap rules. */
2960
3888
  private commitValue;
3889
+ /**
3890
+ * The option a committed value came from, or `undefined` for a free-text chip.
3891
+ *
3892
+ * Both the chip's label and its test id need this lookup, and both are called
3893
+ * from the template — once per chip per change-detection cycle — so a linear
3894
+ * scan of the options would be quadratic in (chips × options) on every cycle.
3895
+ * With the default identity comparator, {@link optionIndex} answers in constant
3896
+ * time; a custom `compareWith` can't be indexed (only it knows what equality
3897
+ * means for the value), so that path keeps the scan.
3898
+ */
3899
+ private optionFor;
3900
+ /**
3901
+ * Value → option, rebuilt only when the option list changes. First entry wins,
3902
+ * matching the `find` it replaces where a value is repeated across options.
3903
+ * Keys compare by `Map` identity, which agrees with the `===` this stands in
3904
+ * for on every value a form control can hold.
3905
+ */
3906
+ private optionIndex;
2961
3907
  private valuesIncludes;
2962
3908
  private valueMatches;
2963
3909
  private clearInput;
@@ -2974,7 +3920,7 @@ declare class TnChipInputComponent<T = string> implements ControlValueAccessor,
2974
3920
  private attachOverlay;
2975
3921
  private detachOverlay;
2976
3922
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnChipInputComponent<any>, never>;
2977
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnChipInputComponent<any>, "tn-chip-input", never, { "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "separatorKeys": { "alias": "separatorKeys"; "required": false; "isSignal": true; }; "addOnBlur": { "alias": "addOnBlur"; "required": false; "isSignal": true; }; "allowCustomValue": { "alias": "allowCustomValue"; "required": false; "isSignal": true; }; "allowDuplicates": { "alias": "allowDuplicates"; "required": false; "isSignal": true; }; "maxChips": { "alias": "maxChips"; "required": false; "isSignal": true; }; "suggestions": { "alias": "suggestions"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "compareWith": { "alias": "compareWith"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, { "chipAdded": "chipAdded"; "chipRemoved": "chipRemoved"; "searchChange": "searchChange"; }, never, never, true, never>;
3923
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnChipInputComponent<any>, "tn-chip-input", never, { "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "separatorKeys": { "alias": "separatorKeys"; "required": false; "isSignal": true; }; "addOnBlur": { "alias": "addOnBlur"; "required": false; "isSignal": true; }; "allowCustomValue": { "alias": "allowCustomValue"; "required": false; "isSignal": true; }; "allowDuplicates": { "alias": "allowDuplicates"; "required": false; "isSignal": true; }; "maxChips": { "alias": "maxChips"; "required": false; "isSignal": true; }; "suggestions": { "alias": "suggestions"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "compareWith": { "alias": "compareWith"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "optionTestIdKey": { "alias": "optionTestIdKey"; "required": false; "isSignal": true; }; }, { "chipAdded": "chipAdded"; "chipRemoved": "chipRemoved"; "searchChange": "searchChange"; }, never, never, true, never>;
2978
3924
  }
2979
3925
 
2980
3926
  /**
@@ -3266,6 +4212,28 @@ declare class TnCardComponent {
3266
4212
  */
3267
4213
  titleTooltipAriaLabel: _angular_core.InputSignal<string | undefined>;
3268
4214
  protected readonly resolvedTitleTooltipAriaLabel: _angular_core.Signal<string>;
4215
+ /**
4216
+ * Whether a tooltip message holding a link may be pinned open by clicking its host (see
4217
+ * `tnTooltipSticky`). On by default, like the directive, and it reaches the title help button
4218
+ * and the footer actions.
4219
+ *
4220
+ * The help button has no other use for the click, so there pinning is all the click does. A
4221
+ * footer action does have one: pinning is additive there, exactly as `tnTooltipSticky` describes
4222
+ * it — one click on a `TnCardAction` whose `tooltip` holds a link both runs `handler()` and pins
4223
+ * the panel. That is fine for a handler that leaves the card in place, and worth a second look
4224
+ * for one that navigates away or closes it, since the panel it pinned goes with the card. Set
4225
+ * this to false for such a card.
4226
+ *
4227
+ * The kebab-menu trigger is deliberately out of scope. Its click already opens the menu, so a
4228
+ * pinnable tooltip there would put a panel over the menu from that same click and take the hint
4229
+ * off hover to do it; `headerMenuTriggerTooltip` therefore always hovers, and this flag does not
4230
+ * reach it.
4231
+ *
4232
+ * It does not make plain tooltips pinnable — card help and action hints are nearly always plain
4233
+ * text, and those keep hovering. Set it to false only to force a message that does hold a link
4234
+ * back to hover behaviour, accepting that the link is then unreachable.
4235
+ */
4236
+ tooltipSticky: _angular_core.InputSignal<boolean>;
3269
4237
  elevation: _angular_core.InputSignal<"none" | "low" | "medium" | "high">;
3270
4238
  padding: _angular_core.InputSignal<"small" | "large" | "medium">;
3271
4239
  padContent: _angular_core.InputSignal<boolean>;
@@ -3301,6 +4269,10 @@ declare class TnCardComponent {
3301
4269
  /**
3302
4270
  * Hover tooltip for the kebab-menu trigger. Defaults to `headerMenuTriggerAriaLabel`, so setting
3303
4271
  * a single translated string covers both the accessible name and the visible hint.
4272
+ *
4273
+ * Hover is all it ever is: unlike the card's other tooltips this one is never pinnable, because
4274
+ * the trigger's click belongs to the menu — see `tooltipSticky`. A message holding a link works
4275
+ * here, but the link stays out of reach.
3304
4276
  */
3305
4277
  headerMenuTriggerTooltip: _angular_core.InputSignal<string | undefined>;
3306
4278
  protected readonly resolvedHeaderMenuAriaLabel: _angular_core.Signal<string>;
@@ -3329,7 +4301,7 @@ declare class TnCardComponent {
3329
4301
  onHeaderMenuItemClick(_item: TnMenuItem): void;
3330
4302
  getStatusClass(type?: string): string;
3331
4303
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnCardComponent, never>;
3332
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnCardComponent, "tn-card", never, { "title": { "alias": "title"; "required": false; "isSignal": true; }; "titleLink": { "alias": "titleLink"; "required": false; "isSignal": true; }; "titleRouterLink": { "alias": "titleRouterLink"; "required": false; "isSignal": true; }; "titleQueryParams": { "alias": "titleQueryParams"; "required": false; "isSignal": true; }; "titleTooltip": { "alias": "titleTooltip"; "required": false; "isSignal": true; }; "titleTooltipAriaLabel": { "alias": "titleTooltipAriaLabel"; "required": false; "isSignal": true; }; "elevation": { "alias": "elevation"; "required": false; "isSignal": true; }; "padding": { "alias": "padding"; "required": false; "isSignal": true; }; "padContent": { "alias": "padContent"; "required": false; "isSignal": true; }; "fillHeight": { "alias": "fillHeight"; "required": false; "isSignal": true; }; "bordered": { "alias": "bordered"; "required": false; "isSignal": true; }; "background": { "alias": "background"; "required": false; "isSignal": true; }; "headerStatus": { "alias": "headerStatus"; "required": false; "isSignal": true; }; "headerControl": { "alias": "headerControl"; "required": false; "isSignal": true; }; "headerMenu": { "alias": "headerMenu"; "required": false; "isSignal": true; }; "headerMenuTriggerTestId": { "alias": "headerMenuTriggerTestId"; "required": false; "isSignal": true; }; "headerMenuTriggerAriaLabel": { "alias": "headerMenuTriggerAriaLabel"; "required": false; "isSignal": true; }; "headerMenuTriggerTooltip": { "alias": "headerMenuTriggerTooltip"; "required": false; "isSignal": true; }; "primaryAction": { "alias": "primaryAction"; "required": false; "isSignal": true; }; "secondaryAction": { "alias": "secondaryAction"; "required": false; "isSignal": true; }; "footerLink": { "alias": "footerLink"; "required": false; "isSignal": true; }; }, {}, ["projectedHeader", "headerActions", "footerActions"], ["[tnCardHeader]", "*"], true, never>;
4304
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnCardComponent, "tn-card", never, { "title": { "alias": "title"; "required": false; "isSignal": true; }; "titleLink": { "alias": "titleLink"; "required": false; "isSignal": true; }; "titleRouterLink": { "alias": "titleRouterLink"; "required": false; "isSignal": true; }; "titleQueryParams": { "alias": "titleQueryParams"; "required": false; "isSignal": true; }; "titleTooltip": { "alias": "titleTooltip"; "required": false; "isSignal": true; }; "titleTooltipAriaLabel": { "alias": "titleTooltipAriaLabel"; "required": false; "isSignal": true; }; "tooltipSticky": { "alias": "tooltipSticky"; "required": false; "isSignal": true; }; "elevation": { "alias": "elevation"; "required": false; "isSignal": true; }; "padding": { "alias": "padding"; "required": false; "isSignal": true; }; "padContent": { "alias": "padContent"; "required": false; "isSignal": true; }; "fillHeight": { "alias": "fillHeight"; "required": false; "isSignal": true; }; "bordered": { "alias": "bordered"; "required": false; "isSignal": true; }; "background": { "alias": "background"; "required": false; "isSignal": true; }; "headerStatus": { "alias": "headerStatus"; "required": false; "isSignal": true; }; "headerControl": { "alias": "headerControl"; "required": false; "isSignal": true; }; "headerMenu": { "alias": "headerMenu"; "required": false; "isSignal": true; }; "headerMenuTriggerTestId": { "alias": "headerMenuTriggerTestId"; "required": false; "isSignal": true; }; "headerMenuTriggerAriaLabel": { "alias": "headerMenuTriggerAriaLabel"; "required": false; "isSignal": true; }; "headerMenuTriggerTooltip": { "alias": "headerMenuTriggerTooltip"; "required": false; "isSignal": true; }; "primaryAction": { "alias": "primaryAction"; "required": false; "isSignal": true; }; "secondaryAction": { "alias": "secondaryAction"; "required": false; "isSignal": true; }; "footerLink": { "alias": "footerLink"; "required": false; "isSignal": true; }; }, {}, ["projectedHeader", "headerActions", "footerActions"], ["[tnCardHeader]", "*"], true, never>;
3333
4305
  }
3334
4306
 
3335
4307
  /**
@@ -4265,7 +5237,6 @@ declare class TnSlideToggleComponent implements AfterViewInit, OnDestroy, Contro
4265
5237
  registerOnTouched(fn: () => void): void;
4266
5238
  setDisabledState(isDisabled: boolean): void;
4267
5239
  onToggleChange(event: Event): void;
4268
- onLabelClick(): void;
4269
5240
  classes: _angular_core.Signal<string[]>;
4270
5241
  effectiveAriaLabel: _angular_core.Signal<string | undefined>;
4271
5242
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnSlideToggleComponent, never>;
@@ -4361,6 +5332,20 @@ declare class TnTabComponent implements AfterContentInit {
4361
5332
  selected: _angular_core.OutputEmitterRef<void>;
4362
5333
  iconContent: _angular_core.Signal<TemplateRef<unknown> | undefined>;
4363
5334
  index: _angular_core.WritableSignal<number>;
5335
+ /**
5336
+ * Id namespace, set by the parent `tn-tabs` so that both ends of the tab↔panel wiring
5337
+ * agree. The default is unique per instance rather than a constant, so a `tn-tab`
5338
+ * rendered outside a `tn-tabs` still renders an `id` that collides with nothing.
5339
+ */
5340
+ groupId: _angular_core.WritableSignal<string>;
5341
+ /**
5342
+ * Whether the parent has a panel at this tab's index, which is what decides whether
5343
+ * `aria-controls` is rendered at all. `tn-tabs` walks its tabs and its panels
5344
+ * independently, so a group given more tabs than panels — or a `tn-tab` used outside a
5345
+ * `tn-tabs`, which is why this starts false — would otherwise point `aria-controls` at
5346
+ * an id no element in the document carries.
5347
+ */
5348
+ hasPanel: _angular_core.WritableSignal<boolean>;
4364
5349
  isActive: _angular_core.WritableSignal<boolean>;
4365
5350
  tabsComponent?: {
4366
5351
  onKeydown: (event: KeyboardEvent, index: number) => void;
@@ -4373,20 +5358,92 @@ declare class TnTabComponent implements AfterContentInit {
4373
5358
  onKeydown(event: KeyboardEvent): void;
4374
5359
  classes: _angular_core.Signal<string>;
4375
5360
  tabIndex: _angular_core.Signal<-1 | 0>;
5361
+ /** This tab's own id, which its panel points back at with `aria-labelledby`. */
5362
+ tabId: _angular_core.Signal<string>;
5363
+ /** The id of the panel this tab controls, for `aria-controls`. */
5364
+ panelId: _angular_core.Signal<string>;
4376
5365
  hasIcon: _angular_core.Signal<boolean>;
4377
5366
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnTabComponent, never>;
4378
5367
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnTabComponent, "tn-tab", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "iconTemplate": { "alias": "iconTemplate"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, { "selected": "selected"; }, ["iconContent"], ["*"], true, never>;
4379
5368
  }
4380
5369
 
5370
+ /**
5371
+ * The name the scrolling content region takes when it becomes focusable and the
5372
+ * panel has no `label` to name it from (#270).
5373
+ *
5374
+ * A focusable element with no accessible name is announced as a bare "group".
5375
+ * A panel rendered outside a `tn-tabs`, or one whose caller left `label` empty,
5376
+ * has no better name available — so this is the last resort rather than the
5377
+ * usual case, and `contentLabel` prefers the label every time there is one.
5378
+ *
5379
+ * Exported so specs assert against it by name rather than by a copied literal.
5380
+ */
5381
+ declare const TN_TAB_PANEL_CONTENT_LABEL = "Tab panel content";
4381
5382
  declare class TnTabPanelComponent {
4382
5383
  label: _angular_core.InputSignal<string>;
4383
5384
  lazyLoad: _angular_core.InputSignal<boolean>;
4384
5385
  testId: _angular_core.InputSignal<TnTestIdValue>;
4385
5386
  content: _angular_core.Signal<TemplateRef<unknown>>;
5387
+ /**
5388
+ * The scrolling content region, which is `.tn-tab-panel__content` — the
5389
+ * element the stylesheet gives `overflow: auto`, not the `role="tabpanel"`
5390
+ * wrapper around it. Optional because it lives inside `@if (shouldRender())`,
5391
+ * so a lazy panel that has never been active does not render it.
5392
+ *
5393
+ * The template ref is `#contentRegion` and not `#content`, which is taken:
5394
+ * the `content` query above asks for a `TemplateRef` under that name, and
5395
+ * naming a real element `#content` would hand it an `ElementRef` instead.
5396
+ */
5397
+ private contentRef;
5398
+ /**
5399
+ * Whether the content region carries the tab stop, its role and its name —
5400
+ * and, conversely, whether the `role="tabpanel"` wrapper gives its own up.
5401
+ *
5402
+ * `.tn-tab-panel__content` is what scrolls, so it is what a keyboard user
5403
+ * has to be able to stand on to read past the fold (#270). The measurement,
5404
+ * the observers behind it and the rule that holds the answer true while the
5405
+ * region has focus are `tnScrollableRegion`'s; the template says why the two
5406
+ * elements trade one tab stop rather than carrying two.
5407
+ */
5408
+ protected contentKeyboardReachable: _angular_core.Signal<boolean>;
4386
5409
  index: _angular_core.WritableSignal<number>;
5410
+ /**
5411
+ * Id namespace, set by the parent `tn-tabs` so that both ends of the tab↔panel wiring
5412
+ * agree. Unique per instance by default, for the same reason as on `tn-tab`: a panel
5413
+ * rendered outside a `tn-tabs` still renders an `id`, and one colliding with another
5414
+ * group's panel would be worse than one nothing points at.
5415
+ */
5416
+ groupId: _angular_core.WritableSignal<string>;
5417
+ /**
5418
+ * Whether the parent has a tab at this panel's index, which is what decides whether
5419
+ * `aria-labelledby` is rendered. The mirror of `hasPanel` on `tn-tab`, and for the same
5420
+ * reason: more panels than tabs, or a panel outside a `tn-tabs`, would otherwise leave
5421
+ * this pointing at an id nothing carries — which is what `aria-labelledby="tab-0"` did
5422
+ * unconditionally before #232.
5423
+ */
5424
+ hasTab: _angular_core.WritableSignal<boolean>;
4387
5425
  isActive: _angular_core.WritableSignal<boolean>;
4388
5426
  hasBeenActive: _angular_core.WritableSignal<boolean>;
4389
5427
  elementRef: ElementRef<any>;
5428
+ /** This panel's own id, which its tab points at with `aria-controls`. */
5429
+ panelId: _angular_core.Signal<string>;
5430
+ /** The id of the tab that labels this panel, for `aria-labelledby`. */
5431
+ tabId: _angular_core.Signal<string>;
5432
+ /**
5433
+ * What the scrolling content region is named while it carries the tab stop.
5434
+ *
5435
+ * The panel's own `label` where there is one, because that is what its tab
5436
+ * says and a listener arriving from the tab hears the same words for the
5437
+ * region it opened. `aria-labelledby` pointing at the tab would be the other
5438
+ * way to say it and is not used here: the tab lives in a sibling component,
5439
+ * so the IDREF resolves only inside a `tn-tabs`, and a panel rendered on its
5440
+ * own would be left unnamed by exactly the route that was supposed to name
5441
+ * it — the `--tn-error-text` hazard in another shape.
5442
+ *
5443
+ * Trimmed, because a whitespace-only label names the region with nothing,
5444
+ * which is the state `TN_TAB_PANEL_CONTENT_LABEL` exists to prevent.
5445
+ */
5446
+ protected contentLabel: _angular_core.Signal<string>;
4390
5447
  classes: _angular_core.Signal<string>;
4391
5448
  shouldRender: _angular_core.Signal<boolean>;
4392
5449
  onActivate(): void;
@@ -4407,10 +5464,17 @@ declare class TnTabsComponent implements AfterContentInit, AfterViewInit, OnDest
4407
5464
  orientation: _angular_core.InputSignal<"horizontal" | "vertical">;
4408
5465
  highlightPosition: _angular_core.InputSignal<"bottom" | "top" | "left" | "right">;
4409
5466
  /**
4410
- * Test-id applied to the tablist root element. Rendered under whichever attribute name
4411
- * is configured via `TN_TEST_ATTR` (default `data-testid`).
5467
+ * Test-id applied to the root element — the wrapper around the tablist and the panels,
5468
+ * not the tablist itself, which is the header inside it. Rendered under whichever
5469
+ * attribute name is configured via `TN_TEST_ATTR` (default `data-testid`).
4412
5470
  */
4413
5471
  testId: _angular_core.InputSignal<TnTestIdValue>;
5472
+ /**
5473
+ * Namespace for the ids this group hands to its tabs and panels, so that two `tn-tabs`
5474
+ * on one page cannot both mint `tab-0` and cross-wire each other's `aria-controls` and
5475
+ * `aria-labelledby`. See `tab-ids.ts`.
5476
+ */
5477
+ private readonly groupId;
4414
5478
  selectedIndexChange: _angular_core.OutputEmitterRef<number>;
4415
5479
  tabChange: _angular_core.OutputEmitterRef<TabChangeEvent>;
4416
5480
  private internalSelectedIndex;
@@ -5045,6 +6109,18 @@ declare class TnKeyboardShortcutComponent {
5045
6109
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnKeyboardShortcutComponent, "tn-keyboard-shortcut", never, { "shortcut": { "alias": "shortcut"; "required": false; "isSignal": true; }; "platform": { "alias": "platform"; "required": false; "isSignal": true; }; "separator": { "alias": "separator"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
5046
6110
  }
5047
6111
 
6112
+ /** What a host binds its template and its dismiss handler to. @internal */
6113
+ interface DismissibleErrorState {
6114
+ /** The host's own list when it has one, otherwise the app-wide default. */
6115
+ resolvedDismissibleErrors: Signal<readonly string[]>;
6116
+ /** Whether the shown message renders a close button beside it. */
6117
+ showDismiss: Signal<boolean>;
6118
+ /** Accessible name for that button. */
6119
+ resolvedDismissAriaLabel: Signal<string>;
6120
+ /** Hover hint for that button. */
6121
+ resolvedDismissTooltip: Signal<string>;
6122
+ }
6123
+
5048
6124
  /**
5049
6125
  * Contract between `tn-form-field` and its projected form control, published
5050
6126
  * over DI so the control can wire its ARIA attributes to the field's chrome
@@ -5143,6 +6219,21 @@ declare class TnFormFieldComponent implements AfterContentInit, TnFormFieldConte
5143
6219
  tooltip: _angular_core.InputSignal<string>;
5144
6220
  /** Placement of the tooltip relative to its help icon. */
5145
6221
  tooltipPosition: _angular_core.InputSignal<TooltipPosition>;
6222
+ /**
6223
+ * Whether a tooltip message holding a link may be pinned open by clicking the help button (see
6224
+ * `tnTooltipSticky`). On by default, like the directive.
6225
+ *
6226
+ * It does not make plain tooltips pinnable — field help is nearly always plain text, and that
6227
+ * keeps hovering. Set it to false only to force a message that does hold a link back to hover
6228
+ * behaviour, accepting that the link is then unreachable.
6229
+ */
6230
+ tooltipSticky: _angular_core.InputSignal<boolean>;
6231
+ /**
6232
+ * Accessible name for the help button, which is icon-only and so has nothing else to be named
6233
+ * by. The message is what the button is for, but it may hold markup — a link is the whole point
6234
+ * of `tooltipSticky` — and `aria-label` takes plain text, so the tags come off first.
6235
+ */
6236
+ protected readonly tooltipAriaLabel: _angular_core.Signal<string>;
5146
6237
  /**
5147
6238
  * Per-field overrides for validation messages, keyed by error key. Values may
5148
6239
  * be a string or a function that receives the error's detail value. Takes
@@ -5150,7 +6241,56 @@ declare class TnFormFieldComponent implements AfterContentInit, TnFormFieldConte
5150
6241
  * built-in defaults.
5151
6242
  */
5152
6243
  errorMessages: _angular_core.InputSignal<Partial<Record<string, _truenas_ui_components.TnFormFieldErrorMessage>>>;
6244
+ /**
6245
+ * Error keys whose message renders with a dismiss button beside it — in
6246
+ * practice a failure the user cannot fix by editing the value, so the message
6247
+ * would otherwise stick until the control changes: a server-side rejection an
6248
+ * error handler attached to the control, or an async validator that judged the
6249
+ * value the user already picked.
6250
+ *
6251
+ * Only the error actually being shown gets the button. A control carrying both
6252
+ * a dismissible key and `required` shows the `required` message, undismissable,
6253
+ * because that is the message on screen.
6254
+ *
6255
+ * Dismissing deletes these keys from the control's errors — listing them here
6256
+ * is what grants that, since a message the user can close but that does not go
6257
+ * away would be worse than no button. Every listed key the control carries goes
6258
+ * at once, not just the one behind the message: an app that spreads one failure
6259
+ * across sibling keys (a flag, its message, a legacy alias) would otherwise see
6260
+ * the message reappear from a sibling. Unlisted errors are left alone, and
6261
+ * {@link dismiss} reports which message went.
6262
+ *
6263
+ * Left unset, the app-wide {@link TN_FORM_FIELD_DISMISSIBLE_ERRORS} default
6264
+ * applies; pass `[]` to opt this field out of it.
6265
+ */
6266
+ dismissibleErrors: _angular_core.InputSignal<readonly string[] | undefined>;
6267
+ /**
6268
+ * Accessible name for the dismiss button, which is icon-only and so has
6269
+ * nothing else to be named by. The library cannot translate its own
6270
+ * `'Dismiss this error'` default, so a consumer with an i18n layer passes an
6271
+ * already-translated string here.
6272
+ */
6273
+ dismissAriaLabel: _angular_core.InputSignal<string | undefined>;
6274
+ /**
6275
+ * Hover tooltip for the dismiss button. Defaults to the resolved
6276
+ * `dismissAriaLabel`, so one translated string covers both the accessible name
6277
+ * and the visible hint.
6278
+ */
6279
+ dismissTooltip: _angular_core.InputSignal<string | undefined>;
6280
+ /**
6281
+ * Emits the error key the user dismissed, after it has been removed — the key
6282
+ * of the message that was on screen, so a consumer listing several dismissible
6283
+ * keys knows which one went.
6284
+ */
6285
+ dismiss: _angular_core.OutputEmitterRef<string>;
5153
6286
  control: _angular_core.Signal<NgControl | undefined>;
6287
+ private host;
6288
+ /**
6289
+ * `read: ElementRef` because the ref sits on `tn-icon-button`, and a component
6290
+ * ref resolves to the instance by default — the element is what the focus
6291
+ * check needs.
6292
+ */
6293
+ private dismissButton;
5154
6294
  private destroyRef;
5155
6295
  /**
5156
6296
  * App-wide message resolver, captured once at construction. Unlike the
@@ -5187,6 +6327,28 @@ declare class TnFormFieldComponent implements AfterContentInit, TnFormFieldConte
5187
6327
  protected showInlineExtras: _angular_core.Signal<boolean>;
5188
6328
  protected hasError: _angular_core.Signal<boolean>;
5189
6329
  protected errorMessage: _angular_core.Signal<string>;
6330
+ /**
6331
+ * The error key the shown message came from. Same pick `resolveErrorMessage`
6332
+ * makes, so the dismiss button can never belong to an error other than the one
6333
+ * being read.
6334
+ */
6335
+ protected activeError: _angular_core.Signal<string | null>;
6336
+ /**
6337
+ * Which list applies, whether the button shows, and what it is called — shared
6338
+ * with `tn-form-errors` so a field message and a group message beside it can
6339
+ * never decide dismissibility by different rules.
6340
+ */
6341
+ protected dismissible: DismissibleErrorState;
6342
+ /**
6343
+ * Drops the dismissed error, then puts focus back on the control rather than
6344
+ * letting it fall to `<body>` with the button — dismissing a server-side error
6345
+ * means "let me try again", and the control is where trying again happens.
6346
+ *
6347
+ * Focus only moves if it was on the button to begin with: a dismiss triggered
6348
+ * from anywhere else (a Safari mouse click, which leaves the button unfocused)
6349
+ * has no focus to lose and should not steal any.
6350
+ */
6351
+ protected dismissError(): void;
5190
6352
  ngAfterContentInit(): void;
5191
6353
  private syncControlState;
5192
6354
  /**
@@ -5194,14 +6356,12 @@ declare class TnFormFieldComponent implements AfterContentInit, TnFormFieldConte
5194
6356
  * `errorMessages` input (and the injected resolver), so it is reactive: the
5195
6357
  * displayed message updates when either the control errors or the overrides
5196
6358
  * change — e.g. a runtime locale switch.
6359
+ *
6360
+ * The ladder itself lives in `./form-field.errors`, shared with
6361
+ * `tn-form-errors` so a group-level message reads exactly like the
6362
+ * field-level one it sits beside.
5197
6363
  */
5198
6364
  private resolveErrorMessage;
5199
- /**
5200
- * Runs a caller-supplied message provider, swallowing any throw so a buggy
5201
- * override or resolver cannot break change detection. Logs in dev mode and
5202
- * returns null so resolution falls through to the next layer.
5203
- */
5204
- private runGuarded;
5205
6365
  showError: _angular_core.Signal<boolean>;
5206
6366
  showHint: _angular_core.Signal<boolean>;
5207
6367
  protected showSubscript: _angular_core.Signal<boolean>;
@@ -5218,7 +6378,7 @@ declare class TnFormFieldComponent implements AfterContentInit, TnFormFieldConte
5218
6378
  /** Forced or validator-inferred required state, for `aria-required`. */
5219
6379
  requiredState: _angular_core.Signal<boolean>;
5220
6380
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnFormFieldComponent, never>;
5221
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnFormFieldComponent, "tn-form-field", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "hint": { "alias": "hint"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "subscriptSizing": { "alias": "subscriptSizing"; "required": false; "isSignal": true; }; "tooltip": { "alias": "tooltip"; "required": false; "isSignal": true; }; "tooltipPosition": { "alias": "tooltipPosition"; "required": false; "isSignal": true; }; "errorMessages": { "alias": "errorMessages"; "required": false; "isSignal": true; }; }, {}, ["control"], ["*"], true, never>;
6381
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnFormFieldComponent, "tn-form-field", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "hint": { "alias": "hint"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "subscriptSizing": { "alias": "subscriptSizing"; "required": false; "isSignal": true; }; "tooltip": { "alias": "tooltip"; "required": false; "isSignal": true; }; "tooltipPosition": { "alias": "tooltipPosition"; "required": false; "isSignal": true; }; "tooltipSticky": { "alias": "tooltipSticky"; "required": false; "isSignal": true; }; "errorMessages": { "alias": "errorMessages"; "required": false; "isSignal": true; }; "dismissibleErrors": { "alias": "dismissibleErrors"; "required": false; "isSignal": true; }; "dismissAriaLabel": { "alias": "dismissAriaLabel"; "required": false; "isSignal": true; }; "dismissTooltip": { "alias": "dismissTooltip"; "required": false; "isSignal": true; }; }, { "dismiss": "dismiss"; }, ["control"], ["*"], true, never>;
5222
6382
  }
5223
6383
 
5224
6384
  /**
@@ -5275,6 +6435,59 @@ type TnFormFieldErrorResolver = (errorKey: string, errorValue: unknown, control:
5275
6435
  * ```
5276
6436
  */
5277
6437
  declare const TN_FORM_FIELD_ERRORS: InjectionToken<TnFormFieldErrorResolver>;
6438
+ /**
6439
+ * App-wide default for which error keys carry a dismiss button, for apps whose
6440
+ * server-side failures always land under the same keys. A field's
6441
+ * `dismissibleErrors` input overrides it — including with `[]`, to opt one field
6442
+ * out of the default.
6443
+ *
6444
+ * Listing a key here grants permission to delete it: dismissing removes it from
6445
+ * the control's errors, since a message the user can close but that will not go
6446
+ * away is worse than no button at all.
6447
+ *
6448
+ * @example
6449
+ * ```ts
6450
+ * providers: [
6451
+ * {
6452
+ * provide: TN_FORM_FIELD_DISMISSIBLE_ERRORS,
6453
+ * useValue: ['manualValidateError', 'manualValidateErrorMsg'],
6454
+ * },
6455
+ * ];
6456
+ * ```
6457
+ */
6458
+ declare const TN_FORM_FIELD_DISMISSIBLE_ERRORS: InjectionToken<readonly string[]>;
6459
+ /** What {@link resolveErrorMessage} needs to answer for one control. */
6460
+ interface ResolveErrorMessageOptions {
6461
+ /** The control's current errors — the caller has already checked it has some. */
6462
+ errors: ValidationErrors;
6463
+ /** Per-instance overrides, keyed by error key. Consulted first. */
6464
+ errorMessages?: TnFormFieldErrorMessages;
6465
+ /** The app-wide resolver from {@link TN_FORM_FIELD_ERRORS}, if one is provided. */
6466
+ resolver?: TnFormFieldErrorResolver | null;
6467
+ /** The failing control, passed through to the resolver. */
6468
+ control?: AbstractControl | null;
6469
+ /**
6470
+ * Who is asking, used only to attribute the dev-mode `console.error` this
6471
+ * logs when a caller-supplied message provider throws. Optional: the library's
6472
+ * own components pass their selector so the log names the element on screen,
6473
+ * and an outside caller has nothing useful to put here.
6474
+ */
6475
+ selector?: string;
6476
+ }
6477
+ /**
6478
+ * Resolves a user-facing message for a control's active error, in the order
6479
+ * `tn-form-field` has always used:
6480
+ *
6481
+ * 1. a per-instance `errorMessages` override (string or factory),
6482
+ * 2. the app-wide {@link TN_FORM_FIELD_ERRORS} resolver,
6483
+ * 3. {@link defaultErrorMessage} for Angular's standard validators,
6484
+ * 4. the error value itself, when a custom validator returned its own string,
6485
+ * 5. the raw error key.
6486
+ *
6487
+ * Shared with `tn-form-errors` so a group-level message reads exactly like the
6488
+ * field-level one it sits beside.
6489
+ */
6490
+ declare function resolveErrorMessage(options: ResolveErrorMessageOptions): string;
5278
6491
 
5279
6492
  /**
5280
6493
  * Harness for interacting with `tn-form-field` in tests.
@@ -5308,6 +6521,7 @@ declare class TnFormFieldHarness extends ComponentHarness {
5308
6521
  private _error;
5309
6522
  private _hint;
5310
6523
  private _tooltip;
6524
+ private _dismiss;
5311
6525
  /**
5312
6526
  * Gets a `HarnessPredicate` that can be used to search for a form field
5313
6527
  * with specific attributes.
@@ -5364,6 +6578,37 @@ declare class TnFormFieldHarness extends ComponentHarness {
5364
6578
  * ```
5365
6579
  */
5366
6580
  hasError(): Promise<boolean>;
6581
+ /**
6582
+ * Checks whether the shown error carries a dismiss button — true only when the
6583
+ * active error key is one of the field's `dismissibleErrors`.
6584
+ *
6585
+ * @returns Promise resolving to true if the dismiss button is present.
6586
+ *
6587
+ * @example
6588
+ * ```typescript
6589
+ * const field = await loader.getHarness(TnFormFieldHarness.with({ label: 'Image' }));
6590
+ * expect(await field.isErrorDismissible()).toBe(true);
6591
+ * ```
6592
+ */
6593
+ isErrorDismissible(): Promise<boolean>;
6594
+ /**
6595
+ * Clicks the dismiss button, as a user clearing a server-side error would.
6596
+ *
6597
+ * The field clears the error itself — every key in its `dismissibleErrors`
6598
+ * that the control carries — puts focus back on the control, and then emits
6599
+ * `dismiss` with the key that was on screen, so the message is gone by the
6600
+ * time this resolves.
6601
+ *
6602
+ * @throws If the shown error is not dismissible.
6603
+ *
6604
+ * @example
6605
+ * ```typescript
6606
+ * const field = await loader.getHarness(TnFormFieldHarness.with({ label: 'Image' }));
6607
+ * await field.dismissError();
6608
+ * expect(await field.hasError()).toBe(false);
6609
+ * ```
6610
+ */
6611
+ dismissError(): Promise<void>;
5367
6612
  /**
5368
6613
  * Gets the hint text, if visible.
5369
6614
  *
@@ -5391,6 +6636,9 @@ declare class TnFormFieldHarness extends ComponentHarness {
5391
6636
  /**
5392
6637
  * Gets the tooltip message (read from the trigger's accessible label).
5393
6638
  *
6639
+ * A message holding markup — a link, say — comes back as the text a screen reader hears, since
6640
+ * that is what the label carries; assert against the message's text, not its tags.
6641
+ *
5394
6642
  * @returns Promise resolving to the tooltip text, or null if no tooltip.
5395
6643
  *
5396
6644
  * @example
@@ -5472,6 +6720,455 @@ interface FormFieldHarnessFilters extends BaseHarnessFilters {
5472
6720
  testId?: string;
5473
6721
  }
5474
6722
 
6723
+ /**
6724
+ * Renders the validation message for a control that is NOT projected into a
6725
+ * `tn-form-field` — in practice a `FormGroup` or `FormArray`, whose errors
6726
+ * belong to the group as a whole and so have no single field to sit under.
6727
+ *
6728
+ * `tn-form-field` covers the ordinary case and should still be preferred: it
6729
+ * owns the label, the `aria-describedby` wiring and the subscript slot. Reach
6730
+ * for this component only where there is no field to own the message —
6731
+ * a cross-field validator on a group, a `minArrayLength` on a form array, a
6732
+ * server-side error attached to a group by an error handler.
6733
+ *
6734
+ * The message comes from the same ladder `tn-form-field` uses (per-instance
6735
+ * `errorMessages`, then the app-wide {@link TN_FORM_FIELD_ERRORS} resolver,
6736
+ * then the built-in defaults), so a group message reads exactly like the field
6737
+ * messages around it. Like `tn-form-field`, it shows ONE message — the active
6738
+ * error, chosen by the same priority — rather than every error at once.
6739
+ *
6740
+ * @example
6741
+ * ```html
6742
+ * <tn-form-errors [control]="form.controls.schedule" />
6743
+ * ```
6744
+ */
6745
+ declare class TnFormErrorsComponent {
6746
+ /** The control whose errors are rendered. Usually a group or an array. */
6747
+ control: _angular_core.InputSignal<AbstractControl<any, any, any>>;
6748
+ /**
6749
+ * Per-instance overrides, keyed by error key. Take precedence over the
6750
+ * app-wide resolver, exactly as on `tn-form-field`.
6751
+ */
6752
+ errorMessages: _angular_core.InputSignal<Partial<Record<string, _truenas_ui_components.TnFormFieldErrorMessage>>>;
6753
+ /**
6754
+ * Show the message before the user has touched or dirtied the control.
6755
+ *
6756
+ * Off by default, so a freshly opened form does not greet the user with
6757
+ * errors. Turn it on where the invalid value did not come from the user —
6758
+ * an edit form populated from an API, or a group an error handler has just
6759
+ * attached a server-side failure to.
6760
+ */
6761
+ showWhenUntouched: _angular_core.InputSignal<boolean>;
6762
+ /**
6763
+ * Test-id base for the message element (`error-` prefixed). There is no
6764
+ * fallback: an `AbstractControl` does not know its own name, so a message
6765
+ * that needs to be addressable in a test has to be named here.
6766
+ */
6767
+ testId: _angular_core.InputSignal<TnTestIdValue>;
6768
+ /**
6769
+ * Error keys whose message renders with a dismiss button beside it — in
6770
+ * practice a failure the user cannot fix by editing a field, so the message
6771
+ * would otherwise stick: a server-side rejection an error handler attached to
6772
+ * the group.
6773
+ *
6774
+ * Only the error actually being shown gets the button, since that is the
6775
+ * message the button belongs to.
6776
+ *
6777
+ * Dismissing deletes these keys from the group's errors — listing them here is
6778
+ * what grants that. Every listed key the group carries goes at once, not just
6779
+ * the one behind the message, so a failure spread across sibling keys cannot
6780
+ * reappear from a sibling. Unlike `tn-form-field` there is no control to hand
6781
+ * focus back to once the button goes away, so a consumer who cares where focus
6782
+ * lands should move it in the {@link dismiss} handler.
6783
+ *
6784
+ * Left unset, the app-wide {@link TN_FORM_FIELD_DISMISSIBLE_ERRORS} default
6785
+ * applies; pass `[]` to opt this message out of it.
6786
+ */
6787
+ dismissibleErrors: _angular_core.InputSignal<readonly string[] | undefined>;
6788
+ /**
6789
+ * Accessible name for the dismiss button, which is icon-only and so has
6790
+ * nothing else to be named by. The library cannot translate its own
6791
+ * `'Dismiss this error'` default, so a consumer with an i18n layer passes an
6792
+ * already-translated string here.
6793
+ */
6794
+ dismissAriaLabel: _angular_core.InputSignal<string | undefined>;
6795
+ /**
6796
+ * Hover tooltip for the dismiss button. Defaults to the resolved
6797
+ * `dismissAriaLabel`, so one translated string covers both the accessible name
6798
+ * and the visible hint.
6799
+ */
6800
+ dismissTooltip: _angular_core.InputSignal<string | undefined>;
6801
+ /** Emits the error key whose message the user dismissed, after it is removed. */
6802
+ dismiss: _angular_core.OutputEmitterRef<string>;
6803
+ /**
6804
+ * Id of the message element, so a caller can point a control's
6805
+ * `aria-describedby` at a group message it is covered by.
6806
+ */
6807
+ readonly errorId: string;
6808
+ private errorResolver;
6809
+ private state;
6810
+ protected errorMessage: _angular_core.Signal<string>;
6811
+ /**
6812
+ * Whether to render. A control can be invalid with no message to show — a
6813
+ * group whose only error resolves to blank — so the message is part of the
6814
+ * condition rather than something the template renders empty.
6815
+ */
6816
+ protected show: _angular_core.Signal<boolean>;
6817
+ /**
6818
+ * The id when there is a message to point at, and null when there is not — so
6819
+ * a caller can bind this straight to `aria-describedby` without ever naming
6820
+ * an element that is not on screen.
6821
+ */
6822
+ readonly describedBy: _angular_core.Signal<string | null>;
6823
+ /**
6824
+ * The error key the shown message came from. Same pick `resolveErrorMessage`
6825
+ * makes, so the dismiss button can never belong to an error other than the one
6826
+ * being read.
6827
+ */
6828
+ protected activeError: _angular_core.Signal<string | null>;
6829
+ /**
6830
+ * Which list applies, whether the button shows, and what it is called — shared
6831
+ * with `tn-form-field` so a group message and the field messages beside it can
6832
+ * never decide dismissibility by different rules.
6833
+ */
6834
+ protected dismissible: DismissibleErrorState;
6835
+ protected dismissError(): void;
6836
+ constructor();
6837
+ private sync;
6838
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnFormErrorsComponent, never>;
6839
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnFormErrorsComponent, "tn-form-errors", never, { "control": { "alias": "control"; "required": true; "isSignal": true; }; "errorMessages": { "alias": "errorMessages"; "required": false; "isSignal": true; }; "showWhenUntouched": { "alias": "showWhenUntouched"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "dismissibleErrors": { "alias": "dismissibleErrors"; "required": false; "isSignal": true; }; "dismissAriaLabel": { "alias": "dismissAriaLabel"; "required": false; "isSignal": true; }; "dismissTooltip": { "alias": "dismissTooltip"; "required": false; "isSignal": true; }; }, { "dismiss": "dismiss"; }, never, never, true, never>;
6840
+ }
6841
+
6842
+ /**
6843
+ * A set of criteria that can be used to filter a list of
6844
+ * `TnFormErrorsHarness` instances.
6845
+ */
6846
+ interface TnFormErrorsHarnessFilters extends BaseHarnessFilters {
6847
+ /** Filters by the rendered message. Supports string or regex matching. */
6848
+ textContains?: string | RegExp;
6849
+ }
6850
+ /**
6851
+ * Harness for `tn-form-errors`.
6852
+ *
6853
+ * The host renders nothing while the control is valid, untouched, or has no
6854
+ * message to show, so `hasMessage()` — not the presence of the harness — is
6855
+ * what tells you whether an error is visible.
6856
+ *
6857
+ * @example
6858
+ * ```typescript
6859
+ * const errors = await loader.getHarness(TnFormErrorsHarness);
6860
+ * expect(await errors.getMessage()).toBe('Select at least one day');
6861
+ * ```
6862
+ */
6863
+ declare class TnFormErrorsHarness extends ComponentHarness {
6864
+ static hostSelector: string;
6865
+ /**
6866
+ * Gets a `HarnessPredicate` for finding a `tn-form-errors` by its message.
6867
+ *
6868
+ * @param options Options for filtering which instances are considered a match.
6869
+ */
6870
+ static with(options?: TnFormErrorsHarnessFilters): HarnessPredicate<TnFormErrorsHarness>;
6871
+ private message;
6872
+ private dismissButton;
6873
+ /** Whether a message is currently rendered. */
6874
+ hasMessage(): Promise<boolean>;
6875
+ /** The rendered message, or `''` when none is shown. */
6876
+ getMessage(): Promise<string>;
6877
+ /**
6878
+ * Whether the shown message carries a dismiss button — true only when the
6879
+ * active error key is one of the host's `dismissibleErrors`.
6880
+ */
6881
+ isDismissible(): Promise<boolean>;
6882
+ /**
6883
+ * Clicks the dismiss button, as a user clearing a server-side error would.
6884
+ *
6885
+ * The component clears the error itself — every key in its `dismissibleErrors`
6886
+ * that the control carries — and then emits `dismiss` with the key that was on
6887
+ * screen, so the message is gone by the time this resolves. The handler is for
6888
+ * what the app does next, such as moving focus.
6889
+ *
6890
+ * @throws If the shown message is not dismissible.
6891
+ */
6892
+ dismiss(): Promise<void>;
6893
+ }
6894
+
6895
+ /**
6896
+ * What a `tn-form-list-item` needs to know about the list it was projected
6897
+ * into. Published over DI rather than passed as an input, so that locking a
6898
+ * list is one binding on the list and not one on every entry the consumer
6899
+ * writes inside its `@for`.
6900
+ *
6901
+ * A projected entry's element injector chains through the `tn-form-list` it is
6902
+ * declared inside, the same way a control projected into `tn-form-field` reaches
6903
+ * {@link TN_FORM_FIELD_CONTEXT}. An entry used on its own injects nothing and
6904
+ * falls back to its own defaults.
6905
+ *
6906
+ * Its own file, and not `form-list.component.ts`, because the list already
6907
+ * imports the item to count its entries — a token declared beside either
6908
+ * component would make that import cycle.
6909
+ */
6910
+ interface TnFormListContext {
6911
+ /** Whether the enclosing list is locked, so an entry can disable its remove button. */
6912
+ disabled: Signal<boolean>;
6913
+ }
6914
+ /** DI token under which `tn-form-list` exposes its {@link TnFormListContext}. */
6915
+ declare const TN_FORM_LIST_CONTEXT: InjectionToken<TnFormListContext>;
6916
+
6917
+ /**
6918
+ * The editor for a repeating group of fields — a `FormArray` the user grows
6919
+ * and shrinks, rendered as a labelled group of {@link TnFormListItemComponent}
6920
+ * cards with an Add control in the header.
6921
+ *
6922
+ * Not to be confused with `tn-list`, which DISPLAYS a list of items. This one
6923
+ * edits one, and owns none of the array: the consumer holds the `FormArray`,
6924
+ * renders an item per element, and does the pushing and splicing in response
6925
+ * to `(add)` and each item's `(delete)`. That keeps the item's shape — which
6926
+ * only the consumer knows — out of the library.
6927
+ *
6928
+ * @example
6929
+ * ```html
6930
+ * <tn-form-list label="ACL entries" [control]="form.controls.entries" (add)="addEntry()">
6931
+ * @for (entry of form.controls.entries.controls; track entry; let i = $index) {
6932
+ * <tn-form-list-item label="ACL entry" (delete)="removeEntry(i)">
6933
+ * <!-- the entry's own fields -->
6934
+ * </tn-form-list-item>
6935
+ * }
6936
+ * </tn-form-list>
6937
+ * ```
6938
+ */
6939
+ declare class TnFormListComponent implements TnFormListContext {
6940
+ /**
6941
+ * The `FormArray` being edited. Optional, and used only to render an error
6942
+ * that belongs to the array as a whole — a minimum or maximum length. The
6943
+ * component neither reads the elements nor writes to it.
6944
+ */
6945
+ control: _angular_core.InputSignal<AbstractControl<any, any, any> | undefined>;
6946
+ /**
6947
+ * What the list is called, in the plural ('ACL entries'). Names the group,
6948
+ * so a screen reader announces which list a field is inside. Supports the
6949
+ * same lightweight markup as `tn-form-field` labels.
6950
+ */
6951
+ label: _angular_core.InputSignal<string>;
6952
+ /**
6953
+ * Optional help tooltip, shown via an icon in the header — next to the label
6954
+ * where there is one, and on its own where there is not.
6955
+ */
6956
+ tooltip: _angular_core.InputSignal<string>;
6957
+ /** Placement of the tooltip relative to its help icon. */
6958
+ tooltipPosition: _angular_core.InputSignal<TooltipPosition>;
6959
+ /** Marks the list as required — at least one entry. Renders the asterisk. */
6960
+ required: _angular_core.InputSignal<boolean>;
6961
+ /** Whether the Add control renders. Turn it off at a maximum length. */
6962
+ canAdd: _angular_core.InputSignal<boolean>;
6963
+ /**
6964
+ * Locks the list, for one the user may not edit yet: the group reports itself
6965
+ * `aria-disabled`, the entries are dimmed and stop taking pointer events, and
6966
+ * Add and every remove button are disabled.
6967
+ *
6968
+ * It does NOT disable the fields inside the entries — those are projected
6969
+ * content the consumer owns, so locking them is `entries.disable()` on the
6970
+ * `FormArray`, which is also what keeps their values out of `form.value`. This
6971
+ * input deliberately does not reach them by going `inert` instead: the entries
6972
+ * stay on screen, and `inert` would drop what a sighted user can still read out
6973
+ * of the accessibility tree entirely.
6974
+ */
6975
+ disabled: _angular_core.InputSignal<boolean>;
6976
+ /** Text of the Add control. English by default — pass a translated string. */
6977
+ addLabel: _angular_core.InputSignal<string>;
6978
+ /** Shown in place of the entries while there are none. */
6979
+ emptyMessage: _angular_core.InputSignal<string>;
6980
+ /**
6981
+ * Overrides the derived empty state. Set it to `false` while the entries are
6982
+ * still being fetched, so a list that is merely not loaded yet does not
6983
+ * announce itself as empty and then fill in.
6984
+ */
6985
+ empty: _angular_core.InputSignal<boolean | undefined>;
6986
+ /** Per-error overrides for the array-level message, as on `tn-form-field`. */
6987
+ errorMessages: _angular_core.InputSignal<Partial<Record<string, _truenas_ui_components.TnFormFieldErrorMessage>>>;
6988
+ /**
6989
+ * Show the array-level message before the user has touched the array — for a
6990
+ * list populated from an API, or one an error handler has just attached a
6991
+ * server-side failure to. Passed straight to `tn-form-errors`.
6992
+ */
6993
+ showErrorWhenUntouched: _angular_core.InputSignal<boolean>;
6994
+ /**
6995
+ * Error keys whose array-level message renders with a close button, and which
6996
+ * dismissing deletes. Unset takes the app-wide
6997
+ * `TN_FORM_FIELD_DISMISSIBLE_ERRORS` default; `[]` opts this list out. Passed
6998
+ * straight to `tn-form-errors`, which is where the reasoning lives.
6999
+ */
7000
+ dismissibleErrors: _angular_core.InputSignal<readonly string[] | undefined>;
7001
+ /** Accessible name for that close button. Pass it already translated. */
7002
+ dismissAriaLabel: _angular_core.InputSignal<string | undefined>;
7003
+ /** Hover hint for that close button. Defaults to `dismissAriaLabel`. */
7004
+ dismissTooltip: _angular_core.InputSignal<string | undefined>;
7005
+ /**
7006
+ * Test-id base for the group (`form-list-` prefixed). Also names the
7007
+ * array-level message, which gets it `error-` prefixed.
7008
+ */
7009
+ testId: _angular_core.InputSignal<TnTestIdValue>;
7010
+ /** Emitted when Add is pressed. Appending the element is the consumer's. */
7011
+ add: _angular_core.OutputEmitterRef<void>;
7012
+ /**
7013
+ * Emitted with the error key when the user closes the array-level message,
7014
+ * after it has been removed. `tn-form-errors` has no control to hand focus
7015
+ * back to, so a consumer who cares where focus lands moves it here.
7016
+ */
7017
+ dismiss: _angular_core.OutputEmitterRef<string>;
7018
+ /**
7019
+ * The projected entries. Counted rather than derived from the `FormArray`,
7020
+ * so the empty state follows what is actually on screen — a consumer may
7021
+ * filter or page the elements it renders, and `control` is optional anyway.
7022
+ */
7023
+ private items;
7024
+ protected isEmpty: _angular_core.Signal<boolean>;
7025
+ /**
7026
+ * The array-level message, queried rather than reached through a template
7027
+ * reference: it renders inside an `@if`, and a reference declared in a block
7028
+ * is scoped to that block.
7029
+ */
7030
+ private errors;
7031
+ /**
7032
+ * Points the group at its own message. `role="alert"` covers the moment the
7033
+ * error appears; it says nothing to a screen reader that tabs into the list
7034
+ * afterwards, which would otherwise hear the label and never learn the array
7035
+ * is in error. Null while there is no message, so this never names an element
7036
+ * that is not on screen.
7037
+ */
7038
+ protected describedBy: _angular_core.Signal<string | null>;
7039
+ /**
7040
+ * Accessible name for the Add control, which reads as a bare 'Add' beside
7041
+ * every other list on a long form. Names the list it adds to.
7042
+ */
7043
+ protected addAriaLabel: _angular_core.Signal<string>;
7044
+ protected tooltipAriaLabel: _angular_core.Signal<string>;
7045
+ /**
7046
+ * Stable id naming the group from the label text alone. Without it the
7047
+ * group's accessible name would also absorb the tooltip button and the Add
7048
+ * control, announcing both on every field inside the list.
7049
+ */
7050
+ protected readonly labelId: string;
7051
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnFormListComponent, never>;
7052
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnFormListComponent, "tn-form-list", never, { "control": { "alias": "control"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "tooltip": { "alias": "tooltip"; "required": false; "isSignal": true; }; "tooltipPosition": { "alias": "tooltipPosition"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "canAdd": { "alias": "canAdd"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "addLabel": { "alias": "addLabel"; "required": false; "isSignal": true; }; "emptyMessage": { "alias": "emptyMessage"; "required": false; "isSignal": true; }; "empty": { "alias": "empty"; "required": false; "isSignal": true; }; "errorMessages": { "alias": "errorMessages"; "required": false; "isSignal": true; }; "showErrorWhenUntouched": { "alias": "showErrorWhenUntouched"; "required": false; "isSignal": true; }; "dismissibleErrors": { "alias": "dismissibleErrors"; "required": false; "isSignal": true; }; "dismissAriaLabel": { "alias": "dismissAriaLabel"; "required": false; "isSignal": true; }; "dismissTooltip": { "alias": "dismissTooltip"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, { "add": "add"; "dismiss": "dismiss"; }, ["items"], ["*"], true, never>;
7053
+ }
7054
+
7055
+ /**
7056
+ * One entry of a {@link TnFormListComponent} — a bordered card holding the
7057
+ * controls for a single element of the form array, with the control that
7058
+ * removes it.
7059
+ *
7060
+ * The item owns only the frame and the remove button; the controls inside are
7061
+ * the consumer's, bound to that element's `FormGroup`.
7062
+ */
7063
+ declare class TnFormListItemComponent {
7064
+ /**
7065
+ * Whether this entry can be removed. Turn it off for an entry the form
7066
+ * requires — the button disappears rather than being disabled, since a
7067
+ * permanently disabled control tells the user nothing about why.
7068
+ */
7069
+ canDelete: _angular_core.InputSignal<boolean>;
7070
+ /**
7071
+ * What one entry is called, in the singular ('ACL entry', 'Portal'). Used to
7072
+ * name the remove button, which is icon-only and has nothing else to be
7073
+ * named by. Pass it already translated.
7074
+ */
7075
+ label: _angular_core.InputSignal<string>;
7076
+ /**
7077
+ * Accessible name for the remove button. Defaults to `Remove <label>`, or
7078
+ * plain `Remove` when there is no label. Set it to translate the wording —
7079
+ * the library ships English only.
7080
+ */
7081
+ removeAriaLabel: _angular_core.InputSignal<string>;
7082
+ /**
7083
+ * Disables the remove button — the entry stays readable, the control just
7084
+ * stops working, the way a native disabled control does.
7085
+ *
7086
+ * Left unset it follows the enclosing `tn-form-list`'s own `disabled`, so
7087
+ * locking a list is one binding on the list rather than one per entry. Set it
7088
+ * explicitly to lock a single entry inside an otherwise editable list.
7089
+ */
7090
+ disabled: _angular_core.InputSignal<boolean | undefined>;
7091
+ /** Test-id base for the remove button (`icon-button-` prefixed). */
7092
+ testId: _angular_core.InputSignal<TnTestIdValue>;
7093
+ /** Emitted when the remove button is pressed. Removing is the consumer's. */
7094
+ delete: _angular_core.OutputEmitterRef<void>;
7095
+ /** Absent when the entry is used outside a `tn-form-list`. */
7096
+ private list;
7097
+ protected resolvedDisabled: _angular_core.Signal<boolean>;
7098
+ protected resolvedRemoveAriaLabel: _angular_core.Signal<string>;
7099
+ protected resolvedTestId: _angular_core.Signal<string | number | (string | number | null | undefined)[]>;
7100
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnFormListItemComponent, never>;
7101
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnFormListItemComponent, "tn-form-list-item", never, { "canDelete": { "alias": "canDelete"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "removeAriaLabel": { "alias": "removeAriaLabel"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, { "delete": "delete"; }, never, ["*"], true, never>;
7102
+ }
7103
+
7104
+ /** Criteria for filtering `TnFormListHarness` instances. */
7105
+ interface TnFormListHarnessFilters extends BaseHarnessFilters {
7106
+ /** Filters by the list's label. Supports string or regex matching. */
7107
+ label?: string | RegExp;
7108
+ }
7109
+ /** Criteria for filtering `TnFormListItemHarness` instances. */
7110
+ type TnFormListItemHarnessFilters = BaseHarnessFilters;
7111
+ /**
7112
+ * Harness for one entry of a `tn-form-list`.
7113
+ *
7114
+ * @example
7115
+ * ```typescript
7116
+ * const [first] = await list.getItems();
7117
+ * await first.remove();
7118
+ * ```
7119
+ */
7120
+ declare class TnFormListItemHarness extends ComponentHarness {
7121
+ static hostSelector: string;
7122
+ static with(options?: TnFormListItemHarnessFilters): HarnessPredicate<TnFormListItemHarness>;
7123
+ private removeButton;
7124
+ /** Whether this entry offers a remove control. */
7125
+ canRemove(): Promise<boolean>;
7126
+ /**
7127
+ * Whether the remove control is disabled — true for an entry inside a
7128
+ * `disabled` list, or one given its own `disabled`. `false` when the entry
7129
+ * offers no remove control at all; ask `canRemove()` to tell the two apart.
7130
+ */
7131
+ isRemoveDisabled(): Promise<boolean>;
7132
+ /** Presses the remove control. Throws when the entry has none. */
7133
+ remove(): Promise<void>;
7134
+ }
7135
+ /**
7136
+ * Harness for `tn-form-list`, the editor for a repeating group of fields.
7137
+ *
7138
+ * @example
7139
+ * ```typescript
7140
+ * const list = await loader.getHarness(TnFormListHarness.with({ label: 'ACL entries' }));
7141
+ * expect(await list.getItemCount()).toBe(0);
7142
+ * expect(await list.isEmpty()).toBe(true);
7143
+ *
7144
+ * await list.add();
7145
+ * expect(await list.getItemCount()).toBe(1);
7146
+ * ```
7147
+ */
7148
+ declare class TnFormListHarness extends ComponentHarness {
7149
+ static hostSelector: string;
7150
+ static with(options?: TnFormListHarnessFilters): HarnessPredicate<TnFormListHarness>;
7151
+ private labelEl;
7152
+ private addButton;
7153
+ private emptyEl;
7154
+ /** The list's label, or `''` when it has none. */
7155
+ getLabel(): Promise<string>;
7156
+ /** Whether the Add control renders. */
7157
+ canAdd(): Promise<boolean>;
7158
+ /** Whether the Add control is disabled. */
7159
+ isAddDisabled(): Promise<boolean>;
7160
+ /** Presses Add. Throws when the list offers no Add control. */
7161
+ add(): Promise<void>;
7162
+ /** The entries currently rendered. */
7163
+ getItems(): Promise<TnFormListItemHarness[]>;
7164
+ /** How many entries are rendered. */
7165
+ getItemCount(): Promise<number>;
7166
+ /** Whether the empty message is showing. */
7167
+ isEmpty(): Promise<boolean>;
7168
+ /** The empty message, or `''` when the list has entries. */
7169
+ getEmptyMessage(): Promise<string>;
7170
+ }
7171
+
5475
7172
  /**
5476
7173
  * Semantic grouping for a related set of form fields. Renders a native
5477
7174
  * `<fieldset>` with an optional `<legend>` heading and help tooltip, and
@@ -5488,6 +7185,21 @@ declare class TnFormSectionComponent {
5488
7185
  tooltip: _angular_core.InputSignal<string>;
5489
7186
  /** Placement of the tooltip relative to its help icon. */
5490
7187
  tooltipPosition: _angular_core.InputSignal<TooltipPosition>;
7188
+ /**
7189
+ * Whether a tooltip message holding a link may be pinned open by clicking the help button (see
7190
+ * `tnTooltipSticky`). On by default, like the directive.
7191
+ *
7192
+ * It does not make plain tooltips pinnable — section help is nearly always plain text, and that
7193
+ * keeps hovering. Set it to false only to force a message that does hold a link back to hover
7194
+ * behaviour, accepting that the link is then unreachable.
7195
+ */
7196
+ tooltipSticky: _angular_core.InputSignal<boolean>;
7197
+ /**
7198
+ * Accessible name for the help button, which is icon-only and so has nothing else to be named
7199
+ * by. The message is what the button is for, but it may hold markup — a link is the whole point
7200
+ * of `tooltipSticky` — and `aria-label` takes plain text, so the tags come off first.
7201
+ */
7202
+ protected readonly tooltipAriaLabel: _angular_core.Signal<string>;
5491
7203
  /** Test id applied to the host for harness/e2e selection. */
5492
7204
  testId: _angular_core.InputSignal<TnTestIdValue>;
5493
7205
  /**
@@ -5498,7 +7210,7 @@ declare class TnFormSectionComponent {
5498
7210
  */
5499
7211
  protected readonly headingId: string;
5500
7212
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnFormSectionComponent, never>;
5501
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnFormSectionComponent, "tn-form-section", never, { "heading": { "alias": "heading"; "required": false; "isSignal": true; }; "tooltip": { "alias": "tooltip"; "required": false; "isSignal": true; }; "tooltipPosition": { "alias": "tooltipPosition"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
7213
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnFormSectionComponent, "tn-form-section", never, { "heading": { "alias": "heading"; "required": false; "isSignal": true; }; "tooltip": { "alias": "tooltip"; "required": false; "isSignal": true; }; "tooltipPosition": { "alias": "tooltipPosition"; "required": false; "isSignal": true; }; "tooltipSticky": { "alias": "tooltipSticky"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
5502
7214
  }
5503
7215
 
5504
7216
  /**
@@ -6233,8 +7945,78 @@ declare class TnListItemComponent {
6233
7945
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnListItemComponent, "tn-list-item", never, { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "clickable": { "alias": "clickable"; "required": false; "isSignal": true; }; "dense": { "alias": "dense"; "required": false; "isSignal": true; }; "wrap": { "alias": "wrap"; "required": false; "isSignal": true; }; }, { "itemClick": "itemClick"; }, ["leadingIcons", "leadingAvatars", "secondaryLines", "secondaryTexts", "trailing"], ["[tnListIcon], [tnListAvatar]", "[tnListItemTitle], [tnListItemPrimary]", "*", "[tnListItemLine], [tnListItemSecondary]", "[tnListItemTrailing]"], true, never>;
6234
7946
  }
6235
7947
 
6236
- declare class TnListSubheaderComponent {
7948
+ /**
7949
+ * A section heading inside a list.
7950
+ *
7951
+ * A container role can forbid its children's roles, so a heading between two
7952
+ * rows invalidates the container — axe's `aria-required-children`, and the
7953
+ * defect fixed in #237 and #259. The heading is not dropped for that; it is
7954
+ * moved, and where the HOST goes depends on what owns it. See `ariaOwnerRole`
7955
+ * for what "owns" means here.
7956
+ *
7957
+ * | Owner | Host | The element around the text |
7958
+ * |---|---|---|
7959
+ * | `role="list"` | `listitem` | `heading`, level 3 |
7960
+ * | `role="listbox"` | `group`, named by the text below | an ordinary span |
7961
+ * | anything else | `heading`, level 3 | an ordinary span |
7962
+ *
7963
+ * Inside a LIST the host becomes the `listitem` the list requires and the
7964
+ * heading goes one level in — `<li><h3>Pools</h3></li>` in plain HTML, which
7965
+ * keeps the section heading in the accessibility tree at the level it always
7966
+ * had. The cost is that the list counts one more item per section, which is the
7967
+ * same count a browser reports for that HTML.
7968
+ *
7969
+ * Inside a LISTBOX neither of those is available. `listitem` is not an allowed
7970
+ * child of a listbox either, so it trades one `aria-required-children` violation
7971
+ * for another; and the heading cannot move one level in the way it does for a
7972
+ * list, because axe reads THROUGH a `group` when it collects what a listbox
7973
+ * owns — measured, a `group` wrapping a `role="heading"` reports the same
7974
+ * violation, now naming the heading. So the section survives as a `group` with
7975
+ * the subheader's own text as its accessible name, via `aria-labelledby` to the
7976
+ * unmarked span: the text is still in the accessibility tree and still
7977
+ * announced, as a named section rather than as a heading.
7978
+ *
7979
+ * That group holds the text and NOT the rows that follow it: a subheader is
7980
+ * projected content and is a sibling of the rows it introduces, so it can name
7981
+ * a section without enclosing one. Genuinely nesting the options is markup for
7982
+ * the consumer to write.
7983
+ *
7984
+ * Outside either — or nested inside a row of one, where a heading is already
7985
+ * legal — the host carries the heading itself and the inner element is an
7986
+ * ordinary span.
7987
+ */
7988
+ declare class TnListSubheaderComponent implements DoCheck {
6237
7989
  inset: _angular_core.InputSignal<boolean>;
7990
+ private readonly owner;
7991
+ /**
7992
+ * Id of the inner span, so that the `group` form can be named by the text.
7993
+ *
7994
+ * Allocated per instance rather than per render, and emitted only when the
7995
+ * group needs it — see the template.
7996
+ */
7997
+ protected readonly textId: string;
7998
+ /**
7999
+ * Which of the three containers in the class docblock this is sitting in.
8000
+ *
8001
+ * Only the two that prescribe their children are named; every other owner,
8002
+ * `null` included, leaves the heading on the host where it has always been.
8003
+ */
8004
+ private readonly ownerKind;
8005
+ /** The role the host carries. See the table in the class docblock. */
8006
+ protected readonly hostRole: _angular_core.Signal<"heading" | "listitem" | "group">;
8007
+ /** Whether the heading is on the HOST, which is where `aria-level` follows it. */
8008
+ protected readonly headingOnHost: _angular_core.Signal<boolean>;
8009
+ /**
8010
+ * Whether the heading has moved one level in, onto the span.
8011
+ *
8012
+ * `list` alone, and never both this and {@link headingOnHost}: one section is
8013
+ * one heading, and a host that is both a `listitem` and a heading is what
8014
+ * #237 removed.
8015
+ */
8016
+ protected readonly headingInside: _angular_core.Signal<boolean>;
8017
+ /** Whether the host is a `group` that the span below has to name. */
8018
+ protected readonly namesGroup: _angular_core.Signal<boolean>;
8019
+ ngDoCheck(): void;
6238
8020
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnListSubheaderComponent, never>;
6239
8021
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnListSubheaderComponent, "tn-list-subheader", never, { "inset": { "alias": "inset"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
6240
8022
  }
@@ -6303,14 +8085,53 @@ declare class TnListItemTrailingDirective {
6303
8085
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnListItemTrailingDirective, never>;
6304
8086
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnListItemTrailingDirective, "[tnListItemTrailing]", never, {}, {}, never, never, true, never>;
6305
8087
  }
6306
- declare class TnDividerDirective {
8088
+ /**
8089
+ * Makes an element that is already something else read as a divider.
8090
+ *
8091
+ * Same ARIA as `TnDividerComponent` and for the same reason (#237):
8092
+ * `role="separator"`, unless a `role="list"` is what owns it, where a separator
8093
+ * is not an allowed child and invalidates the list.
8094
+ *
8095
+ * **It no longer matches `tn-divider` as well, which is a breaking change**:
8096
+ * code that writes `<tn-divider>` while importing only this directive now has
8097
+ * no match for that element and stops compiling. Import `TnDividerComponent`
8098
+ * for the element form — it is what declares the element, and what every use of
8099
+ * `<tn-divider>` in this repository already imports.
8100
+ *
8101
+ * On `tn-divider` this only ever restated what the component declares, and once
8102
+ * the role varies by context, a second source for it is a second answer waiting
8103
+ * to disagree — two instances of the tracking that decides it, on one element,
8104
+ * writing one attribute.
8105
+ */
8106
+ declare class TnDividerDirective implements DoCheck {
8107
+ private readonly owner;
8108
+ protected readonly role: _angular_core.Signal<"presentation" | "separator">;
8109
+ ngDoCheck(): void;
6307
8110
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnDividerDirective, never>;
6308
- static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnDividerDirective, "tn-divider, [tnDivider]", never, {}, {}, never, never, true, never>;
8111
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnDividerDirective, "[tnDivider]", never, {}, {}, never, never, true, never>;
6309
8112
  }
6310
8113
 
6311
- declare class TnDividerComponent {
8114
+ /**
8115
+ * A rule between things.
8116
+ *
8117
+ * `role="separator"`, except where the element that owns it is a container
8118
+ * whose children are prescribed — a `role="list"` owns only `listitem`, so a
8119
+ * separator between two rows invalidates the list it sits in (#237). Owned by a
8120
+ * ROW of that list — a divider inside a `tn-list-item` — it is a separator like
8121
+ * anywhere else. See `ariaOwnerRole` for what "owns" means and why the DOM
8122
+ * decides it rather than DI.
8123
+ */
8124
+ declare class TnDividerComponent implements DoCheck {
6312
8125
  vertical: _angular_core.InputSignal<boolean>;
6313
8126
  inset: _angular_core.InputSignal<boolean>;
8127
+ private readonly owner;
8128
+ /**
8129
+ * `presentation` rather than no role at all: both are invisible to assistive
8130
+ * technology and satisfy the container, and this one says in the DOM that the
8131
+ * rule is decoration on purpose.
8132
+ */
8133
+ protected readonly role: _angular_core.Signal<"presentation" | "separator">;
8134
+ ngDoCheck(): void;
6314
8135
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnDividerComponent, never>;
6315
8136
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnDividerComponent, "tn-divider", never, { "vertical": { "alias": "vertical"; "required": false; "isSignal": true; }; "inset": { "alias": "inset"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
6316
8137
  }
@@ -6330,14 +8151,95 @@ declare class TnListOptionComponent implements AfterContentInit {
6330
8151
  internalDisabled: _angular_core.WritableSignal<boolean | null>;
6331
8152
  internalColor: _angular_core.WritableSignal<"primary" | "warn" | "accent" | null>;
6332
8153
  effectiveSelected: _angular_core.Signal<boolean>;
8154
+ /**
8155
+ * Whether the parent `tn-selection-list` is disabled as a whole, pushed down
8156
+ * by the listbox (#221). `false` when there is no parent.
8157
+ *
8158
+ * Kept apart from `internalDisabled` rather than written into it, because the
8159
+ * two answer different questions and a single slot cannot hold both. The list
8160
+ * disabling itself must not erase an option the consumer disabled on its own,
8161
+ * and re-enabling the list is exactly where a shared slot loses that: the
8162
+ * parent has no record of what the option's own state was before it wrote
8163
+ * over it, so `[disabled]="true"` on the option never comes back. That is not
8164
+ * hypothetical — it is what `setDisabledState()` did to a per-option
8165
+ * `[disabled]` on every `control.enable()` before this signal existed.
8166
+ *
8167
+ * A plain boolean rather than the nullable this sits beside: absent and
8168
+ * not-disabled are the same thing for a parent's opinion, whereas
8169
+ * `internalDisabled` needs `null` to mean "the input still speaks".
8170
+ */
8171
+ listDisabled: _angular_core.WritableSignal<boolean>;
8172
+ /**
8173
+ * Disabled if EITHER source says so — the list as a whole, or this option.
8174
+ *
8175
+ * Not a precedence: a disabled list cannot be overridden by an enabled
8176
+ * option, and a list that is enabled has nothing to say about an option that
8177
+ * disabled itself. Only the second half is a fallback chain, and it is the
8178
+ * one that already existed: `internalDisabled` outranks the input when it has
8179
+ * been written. Nothing in the library writes it any more — `setDisabledState()`
8180
+ * was its only writer and now stops at `listDisabled` — so it is kept for the
8181
+ * public surface it has always been part of, alongside `internalSelected` and
8182
+ * `internalColor`, rather than because a path in here still uses it.
8183
+ */
6333
8184
  effectiveDisabled: _angular_core.Signal<boolean>;
6334
8185
  effectiveColor: _angular_core.Signal<"primary" | "warn" | "accent">;
8186
+ /**
8187
+ * The tabindex a parent `tn-selection-list` has assigned, or `null` when this
8188
+ * option is standalone.
8189
+ *
8190
+ * Set by the listbox's roving tabindex (#216): 0 on the one option that is
8191
+ * the list's single tab stop, -1 on the rest. `null` is not "no tab stop" but
8192
+ * "no parent" — it is what keeps a `tn-list-option` used on its own behaving
8193
+ * as #213 left it, and it is why this is a nullable number rather than a
8194
+ * number defaulting to -1.
8195
+ */
8196
+ rovingTabindex: _angular_core.WritableSignal<number | null>;
8197
+ /**
8198
+ * -1 rather than a removed attribute for a disabled option under a roving
8199
+ * tabindex, which is the one place these two paths disagree. An element with
8200
+ * no `tabindex` cannot be focused programmatically either, so dropping the
8201
+ * attribute would leave the listbox's arrow keys with nothing to move focus
8202
+ * to — and the listbox deliberately visits disabled options, so that they can
8203
+ * be perceived rather than silently skipped. Standalone, where every stop
8204
+ * costs a Tab press, the disabled option is still left out entirely.
8205
+ */
8206
+ effectiveTabindex: _angular_core.Signal<number | null>;
6335
8207
  protected hasLeadingContent: _angular_core.WritableSignal<boolean>;
6336
8208
  protected hasSecondaryTextContent: _angular_core.WritableSignal<boolean>;
6337
8209
  protected hasPrimaryTextDirective: _angular_core.WritableSignal<boolean>;
6338
8210
  ngAfterContentInit(): void;
6339
8211
  private checkContentProjection;
8212
+ /**
8213
+ * Move real DOM focus to this option.
8214
+ *
8215
+ * Called by the parent listbox as the arrow keys move (#216). Focus lands on
8216
+ * the host, which is both the element carrying the roving tabindex and the
8217
+ * element `:host(:focus-visible)` draws the #215 focus ring on — the two
8218
+ * reasons the listbox moves focus rather than pointing at the option with
8219
+ * `aria-activedescendant`.
8220
+ */
8221
+ focus(): void;
6340
8222
  onClick(_event: Event): void;
8223
+ /**
8224
+ * The key is swallowed BEFORE the disabled guard, not after it.
8225
+ *
8226
+ * Declining to toggle is not the same as declining the key. Space scrolls the
8227
+ * page on any element that is neither a form control nor a scroller, and the
8228
+ * option host is neither — so bailing out first hands the browser a Space
8229
+ * with its default action intact, on an element the user deliberately focused
8230
+ * and got nothing from. Nothing upstream saves it either: the listbox's own
8231
+ * handler falls through for Space on purpose, so that the key reaches here.
8232
+ *
8233
+ * Only reachable under a parent `tn-selection-list` (#216), whose roving
8234
+ * tabindex is what makes a disabled option focusable — standalone it carries
8235
+ * no `tabindex`, so it cannot hold focus and this never fires on it.
8236
+ *
8237
+ * Which is safe only because the key has to have been pressed on the host
8238
+ * itself. A host listener hears whatever bubbles out of projected content, so
8239
+ * a consumer's `<input>` inside an option would otherwise have its Space
8240
+ * swallowed by the `preventDefault()` above — and, on an enabled option,
8241
+ * toggle the option as well as typing.
8242
+ */
6341
8243
  onKeydown(event: Event): void;
6342
8244
  toggle(): void;
6343
8245
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnListOptionComponent, never>;
@@ -6353,21 +8255,238 @@ declare class TnSelectionListComponent implements ControlValueAccessor {
6353
8255
  disabled: _angular_core.InputSignal<boolean>;
6354
8256
  multiple: _angular_core.InputSignal<boolean>;
6355
8257
  color: _angular_core.InputSignal<"primary" | "warn" | "accent">;
8258
+ /**
8259
+ * Explicit accessible name for the listbox. Inside a `tn-form-field` with a
8260
+ * label this is unnecessary — the field names the list automatically — but a
8261
+ * list with neither is announced as an unlabelled listbox.
8262
+ *
8263
+ * ALIASED to the attribute name, unlike the plain `ariaLabel` most controls in
8264
+ * this library take, and the divergence is forced rather than chosen: this
8265
+ * component's `role="listbox"` is on the HOST, so `<tn-selection-list
8266
+ * aria-label="Mailboxes">` is valid markup that named the list before this
8267
+ * input existed. The host binding below rewrites that attribute on every
8268
+ * change-detection pass, so an unaliased input would silently STRIP a name
8269
+ * that used to work. The alias makes the same markup set the input instead,
8270
+ * and the binding writes it straight back.
8271
+ */
8272
+ ariaLabel: _angular_core.InputSignal<string | undefined>;
8273
+ /** Id of visible text naming the listbox. Wins over `ariaLabel` where it resolves. */
8274
+ ariaLabelledby: _angular_core.InputSignal<string | undefined>;
8275
+ /**
8276
+ * ARIA wiring from an enclosing `tn-form-field`, read for `labelledby` alone:
8277
+ * #235 is about the listbox having no accessible name, and the field's
8278
+ * `describedby`/`invalid`/`required` are a separate question this list has
8279
+ * never answered either way.
8280
+ *
8281
+ * Called with NO argument, so it reports the field's label id unconditioned.
8282
+ * Handing it the `ariaLabel` input would have it suppress the field itself —
8283
+ * on truthiness, so a whitespace-only label would cancel the field while being
8284
+ * dropped as no name, leaving nothing — and it would only do so while this
8285
+ * field is initialised after the one it reads, since a signal captured before
8286
+ * its own initialiser runs arrives as `undefined` and is swallowed by an
8287
+ * optional call. The suppression is applied below instead, where the rest of
8288
+ * the precedence is.
8289
+ */
8290
+ private readonly fieldAria;
8291
+ /**
8292
+ * The `aria-label` this component supplies, or `null` when it supplies none.
8293
+ *
8294
+ * Blank is not a name: `aria-label=""` names the listbox as emptily as no
8295
+ * attribute at all, while satisfying axe's `aria-input-field-name` rule — a
8296
+ * green check on a control a screen reader announces as "listbox".
8297
+ */
8298
+ private readonly resolvedAriaLabel;
8299
+ /**
8300
+ * The explicit `ariaLabelledby` input, blank normalised away, or `null`.
8301
+ *
8302
+ * Kept apart from the field's label because the two rank differently against
8303
+ * a name the consumer wrote on the host — see {@link hostAriaLabelledby}.
8304
+ */
8305
+ private readonly explicitAriaLabelledby;
8306
+ private readonly hostElement;
8307
+ /**
8308
+ * The value this component last wrote for each naming attribute, so that it
8309
+ * can tell its own from the consumer's. See {@link claim}.
8310
+ */
8311
+ private readonly written;
6356
8312
  selectionChange: _angular_core.OutputEmitterRef<TnSelectionChange>;
6357
8313
  options: _angular_core.Signal<readonly TnListOptionComponent[]>;
6358
8314
  private formDisabled;
6359
8315
  isDisabled: _angular_core.Signal<boolean>;
8316
+ /**
8317
+ * The option the user has moved to, and the slot it occupied at the time —
8318
+ * or `null` while they have not moved yet.
8319
+ *
8320
+ * Kept separate from `activeIndex` rather than seeded with a starting value,
8321
+ * because "where the user last was" and "where a user who has not arrived yet
8322
+ * would land" are different questions and only the second one should follow
8323
+ * the selection around. See `activeIndex`.
8324
+ *
8325
+ * The OPTION and not merely its index, because the options are content
8326
+ * children and the caller can add or remove them ABOVE the one the user is
8327
+ * standing on. An index survives that edit while quietly changing meaning —
8328
+ * it names whichever option shifted into the slot — so the tab stop and the
8329
+ * arrow keys' starting point both come away from the option holding focus,
8330
+ * and one ArrowDown lands two options from where the user is. A reference
8331
+ * cannot drift that way. The index rides along only as the fallback for the
8332
+ * one case a reference cannot answer: the option itself being removed.
8333
+ */
8334
+ private visited;
8335
+ /**
8336
+ * Which option carries the listbox's single tab stop.
8337
+ *
8338
+ * Before the user has touched the list this tracks the first selected option,
8339
+ * which is what APG asks for — tabbing into a list that already has a
8340
+ * selection should land where the user left off rather than at the top. Once
8341
+ * they have moved, `visited` pins it and the selection no longer drags the
8342
+ * tab stop around underneath them.
8343
+ *
8344
+ * Resolved against the current options on every read rather than stored, so
8345
+ * that a list edited underneath the user still points at the option they were
8346
+ * on. Only once that option has left the list is there nothing to resolve,
8347
+ * and the remembered slot is the best answer left — clamped, because an index
8348
+ * held across a removal otherwise reads past the end of the array.
8349
+ */
8350
+ private activeIndex;
8351
+ /**
8352
+ * The option host that currently holds DOM focus, or `null`.
8353
+ *
8354
+ * Held as an element rather than an index, because the whole point of it is
8355
+ * to outlive the option: an index still resolves after a removal, to whatever
8356
+ * option moved into that slot.
8357
+ */
8358
+ private focusedOptionElement;
6360
8359
  private onChange;
6361
8360
  private onTouched;
6362
8361
  constructor();
8362
+ /**
8363
+ * ArrowUp / ArrowDown / Home / End — unmodified, and pressed on an option
8364
+ * host — and deliberately nothing else.
8365
+ *
8366
+ * Space and Enter are absent because `tn-list-option` has handled them since
8367
+ * before this component had any keyboard handling at all, and its keydown
8368
+ * bubbles up to here. Toggling from both places would toggle twice — select
8369
+ * then immediately deselect — which reads as the key doing nothing rather
8370
+ * than as a bug, so it is worth stating why it is missing rather than leaving
8371
+ * a later reader to add it.
8372
+ *
8373
+ * Every other key is left alone, Tab included: `preventDefault()` on an
8374
+ * unrecognised key is how a widget traps a keyboard user inside it.
8375
+ *
8376
+ * Navigation is not gated on `isDisabled()`, because moving focus selects
8377
+ * nothing: a disabled list a user can still read through is the same
8378
+ * reasoning that has the arrow keys visit disabled options at all.
8379
+ *
8380
+ * That is a statement about NAVIGATION only, and deliberately not the wider
8381
+ * claim that a disabled list can be toggled. It cannot: `isDisabled()` is
8382
+ * pushed onto every option's `listDisabled` by the effect in the constructor
8383
+ * (#221), so the option's own guard refuses the toggle whichever route asked
8384
+ * for it — mouse, Space, Enter or a reactive form. What a disabled list still
8385
+ * allows is moving through it, which selects nothing.
8386
+ *
8387
+ * Only keys pressed ON an option host are the listbox's. The handler is on
8388
+ * the host and hears everything that bubbles through it, so without that test
8389
+ * a consumer who projects a focusable control into an option — a text input,
8390
+ * a slider — would have Home and End taken off its caret and the arrows taken
8391
+ * off its value, and get a `preventDefault()` for it. Nothing in this library
8392
+ * projects such a control today, which is why this is a target check rather
8393
+ * than a redesign.
8394
+ */
8395
+ onKeydown(event: KeyboardEvent): void;
8396
+ /**
8397
+ * Keep the tab stop under whichever option actually holds focus.
8398
+ *
8399
+ * Covers the routes into the list that are not the arrow keys — a click, and
8400
+ * a Tab that lands here — so that leaving the list and coming back returns to
8401
+ * the option the user was last on, rather than to the one the arrow keys
8402
+ * happened to leave the index at.
8403
+ *
8404
+ * `contains` rather than an identity check on the target, because focus can
8405
+ * land on something projected into the option rather than on the option host.
8406
+ */
8407
+ onFocusIn(event: FocusEvent): void;
8408
+ /**
8409
+ * Forget the focused option once focus leaves it under its own steam.
8410
+ *
8411
+ * Guarded on the option still being in the document, because a `focusout`
8412
+ * fired BY a removal — Firefox fires one, Chrome does not — is exactly the
8413
+ * case the restore in the constructor exists for, and clearing on it would
8414
+ * defeat that restore in one browser and not the other.
8415
+ */
8416
+ onFocusOut(event: FocusEvent): void;
8417
+ /**
8418
+ * The `aria-label` the host should carry.
8419
+ *
8420
+ * A METHOD rather than a signal, and that is the load-bearing part of this
8421
+ * whole arrangement: a host binding re-evaluates on every change-detection
8422
+ * pass, while a `computed` re-runs only when a signal it read has changed.
8423
+ * These two answers depend on the host's CURRENT attributes, which are not
8424
+ * signals — a consumer's `[attr.aria-label]` bound in the parent template
8425
+ * never reaches an input and never notifies anything. Asked once per pass,
8426
+ * the answer stays true; asked from a `computed`, it goes stale the moment
8427
+ * the consumer's binding changes, and the list is left announcing the wrong
8428
+ * name or no name at all.
8429
+ */
8430
+ protected hostAriaLabel(): string | null;
8431
+ /**
8432
+ * The `aria-labelledby` the host should carry — and the one place the naming
8433
+ * precedence is decided.
8434
+ *
8435
+ * Most specific first: the `ariaLabelledby` input, then any name the consumer
8436
+ * put on the host by another route, then the enclosing `tn-form-field`'s
8437
+ * label. The field comes last because it is chrome the consumer did not write
8438
+ * on this element, and it is WITHHELD rather than rendered alongside: ARIA
8439
+ * prefers `aria-labelledby` wherever it resolves, so a field reference emitted
8440
+ * beside a consumer's `aria-label` does not merely coexist with it — it
8441
+ * outranks it, and the list announces the field's label instead.
8442
+ *
8443
+ * With none of the three, a list stays unnamed — deliberately: a generic
8444
+ * fallback ("List") would satisfy axe while announcing nothing the user can
8445
+ * act on, and only the consumer knows what this list holds.
8446
+ */
8447
+ protected hostAriaLabelledby(): string | null;
8448
+ /** Whether the host already carries a non-blank name this component did not write. */
8449
+ private namedByConsumer;
8450
+ /**
8451
+ * What to bind `attribute` to: this component's own `value` where it has one,
8452
+ * and otherwise whatever is already on the host — so that a name the consumer
8453
+ * set is preserved rather than overwritten by the `null` that means "nothing
8454
+ * to say".
8455
+ *
8456
+ * That distinction is the reason this exists. `[attr.x]="null"` REMOVES the
8457
+ * attribute, and the role is on the HOST here, so `<tn-selection-list
8458
+ * aria-label="…">` and `[attr.aria-labelledby]="…"` are valid markup that
8459
+ * named this list before it had any naming input at all. Measured on Angular
8460
+ * 21, a plain host binding of these attributes ran after the parent's and left
8461
+ * the element with NEITHER — named to unnamed, silently, with the parent's
8462
+ * value reappearing only on a later pass and only for the one whose bound
8463
+ * value had changed.
8464
+ *
8465
+ * Ownership is tracked by value rather than by a flag, so it also lapses
8466
+ * correctly: an attribute this component wrote and something else has since
8467
+ * changed is no longer this component's to rewrite or take away.
8468
+ */
8469
+ private claim;
6363
8470
  writeValue(value: unknown[]): void;
6364
8471
  registerOnChange(fn: (value: unknown[]) => void): void;
6365
8472
  registerOnTouched(fn: () => void): void;
8473
+ /**
8474
+ * Records the form's state and stops there — the effect in the constructor is
8475
+ * what reaches the options, via `isDisabled()`.
8476
+ *
8477
+ * It used to also write each `option.internalDisabled` directly, which is the
8478
+ * only reason this route enforced anything while `[disabled]` enforced
8479
+ * nothing. Two problems, both fixed by routing it through the same signal the
8480
+ * input uses: the two paths could disagree, and `internalDisabled` is the
8481
+ * slot an option's own `[disabled]` input falls back to — so `control.enable()`
8482
+ * wrote `false` over an option the consumer had disabled independently, and
8483
+ * never gave it back.
8484
+ */
6366
8485
  setDisabledState(isDisabled: boolean): void;
6367
8486
  onOptionSelectionChange(): void;
6368
8487
  get selectedOptions(): TnListOptionComponent[];
6369
8488
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnSelectionListComponent, never>;
6370
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnSelectionListComponent, "tn-selection-list", never, { "dense": { "alias": "dense"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "multiple": { "alias": "multiple"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; }, { "selectionChange": "selectionChange"; }, ["options"], ["*"], true, [{ directive: typeof TnTestIdDirective; inputs: { "tnTestId": "testId"; }; outputs: {}; }]>;
8489
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnSelectionListComponent, "tn-selection-list", never, { "dense": { "alias": "dense"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "multiple": { "alias": "multiple"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "aria-label"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; "isSignal": true; }; }, { "selectionChange": "selectionChange"; }, ["options"], ["*"], true, [{ directive: typeof TnTestIdDirective; inputs: { "tnTestId": "testId"; }; outputs: {}; }]>;
6371
8490
  }
6372
8491
 
6373
8492
  declare class TnHeaderCellDefDirective {
@@ -6392,6 +8511,26 @@ declare class TnTableColumnDirective {
6392
8511
  * overrides the card field label. Falls back to the column `name` when unset.
6393
8512
  */
6394
8513
  label: _angular_core.InputSignal<string | undefined>;
8514
+ /**
8515
+ * Renders the header label for screen readers only, leaving the header cell
8516
+ * visually blank. For a column whose purpose is obvious from its contents and
8517
+ * whose heading would only add noise — a row-actions or icon column.
8518
+ *
8519
+ * A blank header is not the same thing as an unlabelled one: `<th>` with no
8520
+ * text at all fails axe's `empty-table-header`, and a screen-reader user
8521
+ * moving across the header row hears nothing where a column exists (#246). So
8522
+ * the label is still rendered, inside the table's own `.cdk-visually-hidden`
8523
+ * — the same treatment the built-in `__expand` and row-actions headers get.
8524
+ *
8525
+ * The text is `label`, falling back to the column `name`. A `tnHeaderCellDef`
8526
+ * template is NOT rendered when this is set: the point is that nothing shows,
8527
+ * and the class that hides it is scoped to the table's own view, so a
8528
+ * consumer's projected markup could not use it anyway.
8529
+ *
8530
+ * Header-only. Card mode has no header row; use `cardHidden` to keep a column
8531
+ * off the card.
8532
+ */
8533
+ hideLabel: _angular_core.InputSignal<boolean>;
6395
8534
  /**
6396
8535
  * Relative importance of this column in card mode (see `mobileLayout` on
6397
8536
  * `tn-table`). Higher numbers render first; fields ranked beyond
@@ -6428,7 +8567,7 @@ declare class TnTableColumnDirective {
6428
8567
  headerTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
6429
8568
  cellTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
6430
8569
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnTableColumnDirective, never>;
6431
- static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnTableColumnDirective, "[tnColumnDef]", ["tnColumnDef"], { "name": { "alias": "tnColumnDef"; "required": true; "isSignal": true; }; "sortable": { "alias": "sortable"; "required": false; "isSignal": true; }; "width": { "alias": "width"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "cardPriority": { "alias": "cardPriority"; "required": false; "isSignal": true; }; "cardTitle": { "alias": "cardTitle"; "required": false; "isSignal": true; }; "cardHidden": { "alias": "cardHidden"; "required": false; "isSignal": true; }; "cardLabel": { "alias": "cardLabel"; "required": false; "isSignal": true; }; }, {}, ["headerTemplate", "cellTemplate"], never, true, never>;
8570
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnTableColumnDirective, "[tnColumnDef]", ["tnColumnDef"], { "name": { "alias": "tnColumnDef"; "required": true; "isSignal": true; }; "sortable": { "alias": "sortable"; "required": false; "isSignal": true; }; "width": { "alias": "width"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "hideLabel": { "alias": "hideLabel"; "required": false; "isSignal": true; }; "cardPriority": { "alias": "cardPriority"; "required": false; "isSignal": true; }; "cardTitle": { "alias": "cardTitle"; "required": false; "isSignal": true; }; "cardHidden": { "alias": "cardHidden"; "required": false; "isSignal": true; }; "cardLabel": { "alias": "cardLabel"; "required": false; "isSignal": true; }; }, {}, ["headerTemplate", "cellTemplate"], never, true, never>;
6432
8571
  }
6433
8572
  /**
6434
8573
  * Directive to define the expandable detail row template.
@@ -6475,6 +8614,25 @@ declare class TnRowActionsDefDirective {
6475
8614
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnRowActionsDefDirective, "[tnRowActionsDef]", never, {}, {}, never, never, true, never>;
6476
8615
  }
6477
8616
 
8617
+ /**
8618
+ * The name the table's scroll region falls back to when the consumer has not
8619
+ * said what the table holds (#270).
8620
+ *
8621
+ * A focusable element with no accessible name is announced as a bare "group",
8622
+ * which tells a listener that something has been reached and nothing about what
8623
+ * it is. "Table" is a poor name and is deliberately the fallback rather than
8624
+ * the expectation — `scrollRegionAriaLabel` is how a consumer says something
8625
+ * useful, and it is the one input on this component whose default is worth
8626
+ * overriding on sight.
8627
+ *
8628
+ * No dev-mode warning goes with it, unlike `tnAccessibleName`'s fallbacks: this
8629
+ * name is rendered only on a table that is WIDER THAN ITS CONTAINER, which
8630
+ * depends on the consumer's layout rather than on their markup, so the warning
8631
+ * would fire on a viewport rather than on a mistake.
8632
+ *
8633
+ * Exported so specs assert against it by name rather than by a copied literal.
8634
+ */
8635
+ declare const TN_TABLE_SCROLL_REGION_LABEL = "Table";
6478
8636
  interface TnTableDataSource<T = unknown> {
6479
8637
  data?: T[];
6480
8638
  connect?(): T[];
@@ -6511,7 +8669,46 @@ interface TnSortEvent {
6511
8669
  * Above the breakpoint both modes render the regular table.
6512
8670
  */
6513
8671
  type TnTableMobileLayout = 'cards' | 'scroll';
8672
+ /**
8673
+ * Chrome copy `tn-table` renders itself. These were baked into the template as English literals,
8674
+ * which a consumer could not translate at all — there was no input to bind. Provide
8675
+ * {@link TN_TABLE_LABELS} at the app root to wire them to an i18n service.
8676
+ */
8677
+ interface TnTableLabels {
8678
+ /** Accessible name for the card-mode sort control. */
8679
+ sortBy: string;
8680
+ /** Card-mode sort option that clears the sort. */
8681
+ unsorted: string;
8682
+ /** Toggle that reveals the columns card mode collapsed. */
8683
+ moreFields: string;
8684
+ /** Card-mode button that opens a row's detail panel. */
8685
+ details: string;
8686
+ /**
8687
+ * Card-mode direction toggle while unsorted or sorted descending — the label names the
8688
+ * ACTION, and `toggleSortDirection()` starts at ascending from both of those states.
8689
+ */
8690
+ sortAscending: string;
8691
+ /** Card-mode direction toggle while sorted ascending. */
8692
+ sortDescending: string;
8693
+ /** Visually-hidden name for the row-expand COLUMN's header cell, not the per-row control. */
8694
+ expand: string;
8695
+ /** Per-row expand control while its detail row is collapsed. */
8696
+ expandRow: string;
8697
+ /** Per-row expand control while its detail row is open. */
8698
+ collapseRow: string;
8699
+ /** Visually-hidden name for the row actions column. */
8700
+ actions: string;
8701
+ }
8702
+ /** English defaults used when no `TN_TABLE_LABELS` provider is registered. */
8703
+ declare const TN_TABLE_DEFAULT_LABELS: TnTableLabels;
8704
+ /**
8705
+ * DI token for app-wide table chrome labels. Provide either a static object or a
8706
+ * `Signal<TnTableLabels>` — the latter lets every table react to language changes when the
8707
+ * consumer wires it up to an i18n service.
8708
+ */
8709
+ declare const TN_TABLE_LABELS: InjectionToken<TnTableLabels | Signal<TnTableLabels>>;
6514
8710
  declare class TnTableComponent<T = unknown> implements OnInit {
8711
+ protected readonly labels: Signal<TnTableLabels>;
6515
8712
  private destroyRef;
6516
8713
  private elementRef;
6517
8714
  private injector;
@@ -6523,6 +8720,39 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6523
8720
  * loads — was dropped to `<body>` and had to tab back from the top of the document.
6524
8721
  */
6525
8722
  private focusBeforeLoading;
8723
+ /**
8724
+ * Whether the host carries a tab stop, `role="group"` and a name because it
8725
+ * is currently scrolling (#270).
8726
+ *
8727
+ * `:host` is `overflow-x: auto` — the stylesheet says why the scrollport is
8728
+ * the host and not `.tn-table__table` — so a table wider than its container
8729
+ * scrolls HERE, and axe's `scrollable-region-focusable` reports a scroll
8730
+ * container that is neither in the tab order nor holds anything that is.
8731
+ *
8732
+ * Which this table is depends entirely on how it was configured: sortable
8733
+ * headers and clickable rows are `tabindex="0"`, so a table with either
8734
+ * satisfies the rule through its content, and a plain read-only one — the
8735
+ * default of every input on this component — satisfies nothing and leaves its
8736
+ * trailing columns unreachable from a keyboard.
8737
+ *
8738
+ * Gated on the measurement rather than on `isScrollMode()`: that class says
8739
+ * which LAYOUT the container's width selected, and this asks whether the
8740
+ * content actually exceeds the box, which is a different question and the one
8741
+ * axe asks. The measurement, the observers behind it and the rule that holds
8742
+ * the answer true while the host has focus are `tnScrollableRegion`'s.
8743
+ *
8744
+ * A field initializer rather than the constructor, because it registers an
8745
+ * `effect` and so needs an injection context.
8746
+ */
8747
+ protected scrollKeyboardReachable: Signal<boolean>;
8748
+ /**
8749
+ * The scroll region's name, falling back when a consumer passes whitespace.
8750
+ *
8751
+ * Blank is not a name: an `aria-label=" "` names the group as emptily as no
8752
+ * label at all, and axe agrees — the same rule `tnAccessibleName` applies to
8753
+ * every other name in this library.
8754
+ */
8755
+ protected resolvedScrollRegionLabel: Signal<string>;
6526
8756
  dataSource: _angular_core.InputSignal<TnTableDataSource<T> | T[]>;
6527
8757
  displayedColumns: _angular_core.InputSignal<string[]>;
6528
8758
  trackBy: _angular_core.InputSignal<((index: number, item: T) => unknown) | undefined>;
@@ -6534,6 +8764,17 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6534
8764
  */
6535
8765
  emptyDescription: _angular_core.InputSignal<string>;
6536
8766
  emptyIcon: _angular_core.InputSignal<string>;
8767
+ /**
8768
+ * Accessible name for the table's own scroll region, used only while the host
8769
+ * actually scrolls — see `scrollKeyboardReachable` and
8770
+ * `TN_TABLE_SCROLL_REGION_LABEL`.
8771
+ *
8772
+ * Set it to say what the table holds ("Storage pools"). It names the SCROLL
8773
+ * REGION rather than the table: a table's own structure is announced from its
8774
+ * rows and headers, and this is the box around it that a keyboard user stands
8775
+ * on to scroll sideways.
8776
+ */
8777
+ scrollRegionAriaLabel: _angular_core.InputSignal<string>;
6537
8778
  selectable: _angular_core.InputSignal<boolean>;
6538
8779
  expandable: _angular_core.InputSignal<boolean>;
6539
8780
  bordered: _angular_core.InputSignal<boolean>;
@@ -6620,6 +8861,10 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6620
8861
  * rows the `isRowExpandable` predicate rejects are unaffected, since `toggleRowExpansion` gates
6621
8862
  * on it. `rowClick` still emits, so a consumer can both expand and react to the click.
6622
8863
  *
8864
+ * In table mode the expanded state is announced by the chevron, not by the row: `aria-expanded`
8865
+ * is a `treegrid`-row attribute and a plain `table` row cannot hold it (#246). The row points at
8866
+ * the open panel with `aria-controls`, which every role permits.
8867
+ *
6623
8868
  * Applies in card mode too, where activating the card toggles its detail section. Both controls
6624
8869
  * report the state: the card carries `aria-expanded` while it is the trigger (`listitem` does
6625
8870
  * permit it, unlike `aria-selected`), and the "Details" button carries it unconditionally,
@@ -6670,7 +8915,7 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6670
8915
  */
6671
8916
  minWidth: _angular_core.InputSignal<string>;
6672
8917
  /** The floor actually applied to the table — explicit if given, else derived, else none. */
6673
- protected readonly resolvedMinWidth: _angular_core.Signal<string | null>;
8918
+ protected readonly resolvedMinWidth: Signal<string | null>;
6674
8919
  /**
6675
8920
  * How the table adapts when its container is narrower than `cardBreakpoint`.
6676
8921
  * See {@link TnTableMobileLayout}. Defaults to `scroll`, which preserves the
@@ -6708,9 +8953,9 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6708
8953
  * button inside the row, as the file picker does for entering directories.
6709
8954
  */
6710
8955
  rowDoubleClick: _angular_core.OutputEmitterRef<T>;
6711
- columnDefs: _angular_core.Signal<readonly TnTableColumnDirective[]>;
6712
- detailRowDef: _angular_core.Signal<TnDetailRowDefDirective | undefined>;
6713
- rowActionsDef: _angular_core.Signal<TnRowActionsDefDirective | undefined>;
8956
+ columnDefs: Signal<readonly TnTableColumnDirective[]>;
8957
+ detailRowDef: Signal<TnDetailRowDefDirective | undefined>;
8958
+ rowActionsDef: Signal<TnRowActionsDefDirective | undefined>;
6714
8959
  /** Observed host width in px; drives the switch into card mode. */
6715
8960
  private containerWidth;
6716
8961
  private resizeObserver?;
@@ -6748,21 +8993,50 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6748
8993
  private measureContainer;
6749
8994
  ngOnInit(): void;
6750
8995
  /** True when the layout should collapse rows into cards. */
6751
- isCardMode: _angular_core.Signal<boolean>;
8996
+ isCardMode: Signal<boolean>;
6752
8997
  /** True when the layout should keep the table but pin edge columns and scroll. */
6753
- isScrollMode: _angular_core.Signal<boolean>;
6754
- data: _angular_core.Signal<T[]>;
6755
- effectiveDisplayedColumns: _angular_core.Signal<string[]>;
8998
+ isScrollMode: Signal<boolean>;
8999
+ data: Signal<T[]>;
9000
+ effectiveDisplayedColumns: Signal<string[]>;
6756
9001
  /**
6757
9002
  * Every column the table actually renders, including the trailing actions column, which
6758
9003
  * `effectiveDisplayedColumns` does not track because it comes from a content template rather
6759
9004
  * than `displayedColumns`. Used for the detail row's `colspan`, so a detail row spans the full
6760
9005
  * width no matter which structural columns are on.
6761
9006
  */
6762
- totalColumnCount: _angular_core.Signal<number>;
6763
- isAllSelected: _angular_core.Signal<boolean>;
6764
- isIndeterminate: _angular_core.Signal<boolean>;
6765
- trackByFn: _angular_core.Signal<(index: number, item: T) => unknown>;
9007
+ totalColumnCount: Signal<number>;
9008
+ /**
9009
+ * How many rows a select-all can actually select.
9010
+ *
9011
+ * `SelectionModel` stores selections in a `Set`, so `selection.selected.length`
9012
+ * counts DISTINCT rows while `data().length` counts array entries. A `dataSource`
9013
+ * holding the same row reference twice makes the two disagree, and every comparison
9014
+ * of a selection count against a row count has to use this one to stay honest —
9015
+ * see {@link isAllSelected}.
9016
+ */
9017
+ private distinctRowCount;
9018
+ isAllSelected: Signal<boolean>;
9019
+ isIndeterminate: Signal<boolean>;
9020
+ /**
9021
+ * Whether the select-all control has anything to act on.
9022
+ *
9023
+ * Disabling it on an empty table is a correctness guard, not a nicety. Since #236
9024
+ * the checkbox is a real control the user can click, and its `checked` binding is
9025
+ * one-way: the DOM follows `isAllSelected()`, and Angular only writes the attribute
9026
+ * back when that value CHANGES. With no rows, `isAllSelected()` is pinned false —
9027
+ * selecting nothing leaves the count at zero — so a click would flip the input in
9028
+ * the DOM, change no bound value, and leave a checked-looking box over an empty
9029
+ * selection until something else re-rendered it.
9030
+ *
9031
+ * The hit area around the checkbox stands down for the same reason, so the two
9032
+ * cannot disagree about whether the control is live.
9033
+ *
9034
+ * An empty table is the only case that has to be disabled rather than fixed: a
9035
+ * repeated row reference produces the same DOM-versus-model divergence, and
9036
+ * {@link distinctRowCount} resolves that one by making the control work.
9037
+ */
9038
+ canSelectAll: Signal<boolean>;
9039
+ trackByFn: Signal<(index: number, item: T) => unknown>;
6766
9040
  onSortClick(column: string): void;
6767
9041
  isSorted(column: string): boolean;
6768
9042
  /**
@@ -6776,11 +9050,17 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6776
9050
  toggleRowExpansion(row: T): void;
6777
9051
  isRowExpanded(row: T): boolean;
6778
9052
  /**
6779
- * Whether the row element itself acts as the expand/collapse control, which is what makes an
6780
- * `aria-expanded` on it meaningful: a screen-reader user who focuses the row and presses Enter
6781
- * otherwise gets no announcement that anything expanded, since only the chevron carries the
6782
- * state. Also requires `clickable` — without it the row isn't activatable and
6783
- * {@link onRowClick} returns before toggling anything.
9053
+ * Whether the row element itself acts as the expand/collapse control. Requires `clickable` —
9054
+ * without it the row isn't activatable and {@link onRowClick} returns before toggling anything.
9055
+ *
9056
+ * A table row does NOT get `aria-expanded` from this. That attribute is only supported on a
9057
+ * `treegrid` row; on a plain `table` it is ignored, so the state it seemed to publish reached
9058
+ * nobody (#246). The chevron in the `__expand` cell carries it instead, on a `button`, where it
9059
+ * is valid and where a screen-reader user lands by tabbing. What the row does take from this is
9060
+ * `aria-controls`, which is global rather than role-conditional.
9061
+ *
9062
+ * Card mode is the case where the trigger element CAN hold the state — see
9063
+ * {@link isCardExpandTrigger}.
6784
9064
  */
6785
9065
  isRowExpandTrigger(row: T): boolean;
6786
9066
  /**
@@ -6788,8 +9068,8 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6788
9068
  * template. Card mode renders no expansion affordance at all without one, so `aria-expanded`
6789
9069
  * on the card would advertise a state change that produces nothing.
6790
9070
  *
6791
- * Table mode deliberately keeps the looser check: its rows are asserted to carry
6792
- * `aria-expanded` from `expandOnRowClick` alone, and that predates this layout.
9071
+ * `listitem` permits `aria-expanded`, which is what lets the card hold the state its table-row
9072
+ * equivalent cannot.
6793
9073
  */
6794
9074
  isCardExpandTrigger(row: T): boolean;
6795
9075
  /** DOM id for a row's detail panel, so the expand trigger can point `aria-controls` at it. */
@@ -6855,14 +9135,55 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6855
9135
  */
6856
9136
  onSortHeaderClick(column: string, event: Event): void;
6857
9137
  /**
6858
- * Keyboard counterpart of {@link onSortHeaderClick}.
9138
+ * Keyboard counterpart of {@link onSortHeaderClick}.
9139
+ *
9140
+ * Takes `Event` rather than `KeyboardEvent` because Angular types `$event` as `Event` for
9141
+ * the `keydown.enter` / `keydown.space` pseudo-events; narrowing happens here.
9142
+ */
9143
+ onSortHeaderKeydown(column: string, event: Event): void;
9144
+ /** Handles the card-mode sort `<select>` change. */
9145
+ onSortSelectChange(event: Event): void;
9146
+ /**
9147
+ * Whether an event started inside a selection checkbox rather than on the hit area
9148
+ * around it.
9149
+ *
9150
+ * Matched on the component's host class rather than through
9151
+ * {@link isControlTarget}, because the element clicked is usually neither the input
9152
+ * nor the host: `tn-checkbox` renders a `<label>` wrapping the input and its
9153
+ * checkmark, and a click on the checkmark activates the input as the label's default
9154
+ * action. `closest('input')` says no to that click, and the toggle would then happen
9155
+ * twice — once here, once from the label's own activation.
9156
+ *
9157
+ * @param event The DOM event; its `currentTarget` is the hit area.
9158
+ */
9159
+ private isSelectionCheckboxTarget;
9160
+ /**
9161
+ * Click on the hit area around a select-all checkbox, in either layout.
9162
+ *
9163
+ * @param event The originating click.
9164
+ */
9165
+ onSelectAllHitAreaClick(event: Event): void;
9166
+ /**
9167
+ * Enter on the select-all checkbox.
9168
+ *
9169
+ * A native checkbox answers to Space and not to Enter, and the `<th>` this replaced
9170
+ * handled both. Bound on the header's checkbox only, which is where that behaviour
9171
+ * existed — card mode's select-all never had it.
9172
+ *
9173
+ * @param event The originating keydown; typed as `Event` because Angular types
9174
+ * `$event` that way for the `keydown.enter` pseudo-event.
9175
+ */
9176
+ onSelectAllEnter(event: Event): void;
9177
+ /**
9178
+ * Click on the hit area around a row's selection checkbox, in either layout.
6859
9179
  *
6860
- * Takes `Event` rather than `KeyboardEvent` because Angular types `$event` as `Event` for
6861
- * the `keydown.enter` / `keydown.space` pseudo-events; narrowing happens here.
9180
+ * Propagation stops whichever path activates the checkbox: a row is clickable and a
9181
+ * card is activatable, and selecting is not activating.
9182
+ *
9183
+ * @param event The originating click.
9184
+ * @param row The row the cell or card belongs to.
6862
9185
  */
6863
- onSortHeaderKeydown(column: string, event: Event): void;
6864
- /** Handles the card-mode sort `<select>` change. */
6865
- onSortSelectChange(event: Event): void;
9186
+ onRowSelectHitAreaClick(event: Event, row: T): void;
6866
9187
  toggleSelectAll(): void;
6867
9188
  toggleRowSelection(row: T): void;
6868
9189
  isRowSelected(row: T): boolean;
@@ -6873,17 +9194,17 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6873
9194
  * most prominent slot. Falls back to the first displayed column only when every
6874
9195
  * column is hidden, since a card still needs a title.
6875
9196
  */
6876
- cardTitleColumn: _angular_core.Signal<string>;
9197
+ cardTitleColumn: Signal<string>;
6877
9198
  /**
6878
9199
  * Columns rendered as label/value fields in a card, ordered by descending
6879
9200
  * `cardPriority` (ties keep `displayedColumns` order). Excludes the title column
6880
9201
  * and any `cardHidden` columns.
6881
9202
  */
6882
- cardFieldColumns: _angular_core.Signal<string[]>;
9203
+ cardFieldColumns: Signal<string[]>;
6883
9204
  /** Fields shown directly on the card (up to `cardPrimaryCount`). */
6884
- cardPrimaryColumns: _angular_core.Signal<string[]>;
9205
+ cardPrimaryColumns: Signal<string[]>;
6885
9206
  /** Fields tucked behind the "More fields" disclosure. */
6886
- cardSecondaryColumns: _angular_core.Signal<string[]>;
9207
+ cardSecondaryColumns: Signal<string[]>;
6887
9208
  /**
6888
9209
  * Displayed columns that are sortable — populates the card-mode sort menu.
6889
9210
  *
@@ -6897,7 +9218,7 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6897
9218
  * The title column stays eligible: it is excluded from the *fields*, but it is
6898
9219
  * displayed, so sorting by it is meaningful.
6899
9220
  */
6900
- sortableColumns: _angular_core.Signal<string[]>;
9221
+ sortableColumns: Signal<string[]>;
6901
9222
  /**
6902
9223
  * Whether the active sort column is one the table can actually sort by.
6903
9224
  *
@@ -6907,7 +9228,7 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6907
9228
  * non-sortable header does nothing, so the direction toggle is hidden — and
6908
9229
  * {@link toggleSortDirection} refuses — for the same column.
6909
9230
  */
6910
- protected readonly canSortActiveColumn: _angular_core.Signal<boolean>;
9231
+ protected readonly canSortActiveColumn: Signal<boolean>;
6911
9232
  /**
6912
9233
  * Sets (or clears, when passed `''`) the active sort column for card mode.
6913
9234
  * Switching columns resets to ascending, and clearing emits an empty `column` —
@@ -6931,7 +9252,7 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6931
9252
  getCardLabel(column: string): string;
6932
9253
  getCellValue(row: T, column: string): unknown;
6933
9254
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnTableComponent<any>, never>;
6934
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnTableComponent<any>, "tn-table", never, { "dataSource": { "alias": "dataSource"; "required": false; "isSignal": true; }; "displayedColumns": { "alias": "displayedColumns"; "required": false; "isSignal": true; }; "trackBy": { "alias": "trackBy"; "required": false; "isSignal": true; }; "emptyMessage": { "alias": "emptyMessage"; "required": false; "isSignal": true; }; "emptyDescription": { "alias": "emptyDescription"; "required": false; "isSignal": true; }; "emptyIcon": { "alias": "emptyIcon"; "required": false; "isSignal": true; }; "selectable": { "alias": "selectable"; "required": false; "isSignal": true; }; "expandable": { "alias": "expandable"; "required": false; "isSignal": true; }; "bordered": { "alias": "bordered"; "required": false; "isSignal": true; }; "isRowExpandable": { "alias": "isRowExpandable"; "required": false; "isSignal": true; }; "activeRow": { "alias": "activeRow"; "required": false; "isSignal": true; }; "activeWhen": { "alias": "activeWhen"; "required": false; "isSignal": true; }; "activeBg": { "alias": "activeBg"; "required": false; "isSignal": true; }; "activeIndicator": { "alias": "activeIndicator"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "loadingMessage": { "alias": "loadingMessage"; "required": false; "isSignal": true; }; "clickable": { "alias": "clickable"; "required": false; "isSignal": true; }; "expandOnRowClick": { "alias": "expandOnRowClick"; "required": false; "isSignal": true; }; "singleExpand": { "alias": "singleExpand"; "required": false; "isSignal": true; }; "fixedLayout": { "alias": "fixedLayout"; "required": false; "isSignal": true; }; "minColumnWidth": { "alias": "minColumnWidth"; "required": false; "isSignal": true; }; "minWidth": { "alias": "minWidth"; "required": false; "isSignal": true; }; "mobileLayout": { "alias": "mobileLayout"; "required": false; "isSignal": true; }; "cardBreakpoint": { "alias": "cardBreakpoint"; "required": false; "isSignal": true; }; "cardPrimaryCount": { "alias": "cardPrimaryCount"; "required": false; "isSignal": true; }; "sortColumn": { "alias": "sortColumn"; "required": false; "isSignal": true; }; "sortDirection": { "alias": "sortDirection"; "required": false; "isSignal": true; }; }, { "sortChange": "sortChange"; "selectionChange": "selectionChange"; "rowClick": "rowClick"; "rowDoubleClick": "rowDoubleClick"; "sortColumn": "sortColumnChange"; "sortDirection": "sortDirectionChange"; }, ["columnDefs", "detailRowDef", "rowActionsDef"], never, true, [{ directive: typeof TnTestIdDirective; inputs: { "tnTestId": "testId"; }; outputs: {}; }]>;
9255
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnTableComponent<any>, "tn-table", never, { "dataSource": { "alias": "dataSource"; "required": false; "isSignal": true; }; "displayedColumns": { "alias": "displayedColumns"; "required": false; "isSignal": true; }; "trackBy": { "alias": "trackBy"; "required": false; "isSignal": true; }; "emptyMessage": { "alias": "emptyMessage"; "required": false; "isSignal": true; }; "emptyDescription": { "alias": "emptyDescription"; "required": false; "isSignal": true; }; "emptyIcon": { "alias": "emptyIcon"; "required": false; "isSignal": true; }; "scrollRegionAriaLabel": { "alias": "scrollRegionAriaLabel"; "required": false; "isSignal": true; }; "selectable": { "alias": "selectable"; "required": false; "isSignal": true; }; "expandable": { "alias": "expandable"; "required": false; "isSignal": true; }; "bordered": { "alias": "bordered"; "required": false; "isSignal": true; }; "isRowExpandable": { "alias": "isRowExpandable"; "required": false; "isSignal": true; }; "activeRow": { "alias": "activeRow"; "required": false; "isSignal": true; }; "activeWhen": { "alias": "activeWhen"; "required": false; "isSignal": true; }; "activeBg": { "alias": "activeBg"; "required": false; "isSignal": true; }; "activeIndicator": { "alias": "activeIndicator"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "loadingMessage": { "alias": "loadingMessage"; "required": false; "isSignal": true; }; "clickable": { "alias": "clickable"; "required": false; "isSignal": true; }; "expandOnRowClick": { "alias": "expandOnRowClick"; "required": false; "isSignal": true; }; "singleExpand": { "alias": "singleExpand"; "required": false; "isSignal": true; }; "fixedLayout": { "alias": "fixedLayout"; "required": false; "isSignal": true; }; "minColumnWidth": { "alias": "minColumnWidth"; "required": false; "isSignal": true; }; "minWidth": { "alias": "minWidth"; "required": false; "isSignal": true; }; "mobileLayout": { "alias": "mobileLayout"; "required": false; "isSignal": true; }; "cardBreakpoint": { "alias": "cardBreakpoint"; "required": false; "isSignal": true; }; "cardPrimaryCount": { "alias": "cardPrimaryCount"; "required": false; "isSignal": true; }; "sortColumn": { "alias": "sortColumn"; "required": false; "isSignal": true; }; "sortDirection": { "alias": "sortDirection"; "required": false; "isSignal": true; }; }, { "sortChange": "sortChange"; "selectionChange": "selectionChange"; "rowClick": "rowClick"; "rowDoubleClick": "rowDoubleClick"; "sortColumn": "sortColumnChange"; "sortDirection": "sortDirectionChange"; }, ["columnDefs", "detailRowDef", "rowActionsDef"], never, true, [{ directive: typeof TnTestIdDirective; inputs: { "tnTestId": "testId"; }; outputs: {}; }]>;
6935
9256
  }
6936
9257
 
6937
9258
  /**
@@ -7471,11 +9792,6 @@ declare class TnTablePagerComponent {
7471
9792
  * `TnTestIdValue` testId.
7472
9793
  */
7473
9794
  protected childTestId(suffix: string): string;
7474
- /**
7475
- * Normalize the injected token into a Signal so consumers can supply either
7476
- * a plain object or a reactive signal (e.g. derived from a TranslateService's
7477
- * onLangChange) and the pager re-renders when labels change.
7478
- */
7479
9795
  private readonly defaultLabels;
7480
9796
  /** 1-based index of the currently displayed page. */
7481
9797
  currentPage: _angular_core.ModelSignal<number>;
@@ -7509,7 +9825,27 @@ declare class TnTablePagerComponent {
7509
9825
  previousPageLabel: _angular_core.InputSignal<string | undefined>;
7510
9826
  nextPageLabel: _angular_core.InputSignal<string | undefined>;
7511
9827
  lastPageLabel: _angular_core.InputSignal<string | undefined>;
9828
+ /**
9829
+ * Accessible name for the pager's `navigation` landmark — what a screen
9830
+ * reader user picking this pager out of a landmark list hears.
9831
+ *
9832
+ * **Name every pager on a page that has more than one.** Two pagers both
9833
+ * announcing the DI default are one landmark repeated as far as that list is
9834
+ * concerned, which is what #249 was: `landmark-unique` fails and the user has
9835
+ * nothing to choose between them.
9836
+ */
7512
9837
  tablePaginationLabel: _angular_core.InputSignal<string | undefined>;
9838
+ /**
9839
+ * IDREF naming the pager from text already on the page — the heading over the
9840
+ * table it pages, typically, which is the name the user can see.
9841
+ *
9842
+ * Wins over the `aria-label` in the ARIA name computation while it resolves,
9843
+ * so pass one or the other rather than both. The `aria-label` is rendered
9844
+ * beside it either way and is what the pager falls back to if the IDREF is
9845
+ * typo'd or names an element that has not rendered yet — see
9846
+ * `resolvedTablePaginationLabel`.
9847
+ */
9848
+ ariaLabelledby: _angular_core.InputSignal<string | undefined>;
7513
9849
  /** Resolved labels: explicit input takes precedence over the DI default. */
7514
9850
  protected resolvedItemsPerPageLabel: Signal<string>;
7515
9851
  protected resolvedOfLabel: Signal<string>;
@@ -7517,7 +9853,57 @@ declare class TnTablePagerComponent {
7517
9853
  protected resolvedPreviousPageLabel: Signal<string>;
7518
9854
  protected resolvedNextPageLabel: Signal<string>;
7519
9855
  protected resolvedLastPageLabel: Signal<string>;
7520
- protected resolvedTablePaginationLabel: Signal<string>;
9856
+ /**
9857
+ * The landmark name a pager falls back to when the consumer sets no
9858
+ * `tablePaginationLabel` — the DI default, scoped by `testId` when there is
9859
+ * one.
9860
+ *
9861
+ * The scoping is what stops the DEFAULT from being the same string on every
9862
+ * pager, which is the shape #249 reported: two pagers, both announcing "Table
9863
+ * pagination", indistinguishable in a landmark list. `testId` is the only
9864
+ * per-instance identity the pager already has, and a page with two pagers
9865
+ * needs distinct ones anyway — without them the pagers' own child controls
9866
+ * collide on `select-page-size` / `button-first-page` (see the **Multiple
9867
+ * Pagers** story), so the multi-pager case that trips this rule is exactly the
9868
+ * case that already sets it.
9869
+ *
9870
+ * **It is a fallback and not the good answer.** A test id is a developer-facing
9871
+ * token: it is not translated, and `storage` reads as the word rather than as
9872
+ * "Storage pools". A pager whose name matters names itself with
9873
+ * `tablePaginationLabel`, or points at the table's visible heading with
9874
+ * `ariaLabelledby`; this only keeps the unnamed case from being ambiguous as
9875
+ * well as generic.
9876
+ *
9877
+ * Appended in parentheses rather than woven into the sentence, because the
9878
+ * base string comes from `TN_TABLE_PAGER_LABELS` and may be in any language —
9879
+ * there is no word order here to get right, only a translated name and a
9880
+ * scope after it.
9881
+ */
9882
+ private defaultTablePaginationLabel;
9883
+ /**
9884
+ * The name rendered as `aria-label`, always — the explicit
9885
+ * `tablePaginationLabel` when there is one, the scoped default otherwise.
9886
+ *
9887
+ * **It is emitted beside an `ariaLabelledby` rather than withheld under it**,
9888
+ * which is where this differs from `../a11y/accessible-name`, the shared rule
9889
+ * the progressbars and the dialogs take. That rule drops the generic fallback
9890
+ * when the caller supplies an IDREF, so a dangling IDREF surfaces as an
9891
+ * unnamed element instead of being masked by a name that says nothing. The
9892
+ * trade is only worth making where "unnamed" is itself caught: a progressbar
9893
+ * fails `aria-progressbar-name`, an `over` drawer fails `aria-dialog-name`,
9894
+ * and a `side` drawer — a landmark, like this — rests on that module's
9895
+ * dev-mode warning instead. This pager has neither. `landmark-unique` compares
9896
+ * landmarks against each other, so one unnamed pager violates nothing at all;
9897
+ * and the warning is one it cannot take, because the fallback here is a name
9898
+ * the consumer configures through `TN_TABLE_PAGER_LABELS` rather than one
9899
+ * someone forgot, so it would fire on the ordinary case.
9900
+ *
9901
+ * Withholding here would therefore turn a typo in `ariaLabelledby` into a
9902
+ * pager that announces nothing and reports nothing — a worse version of the
9903
+ * defect #249 opened on. So the pager keeps a name it can always fall back to,
9904
+ * and accepts that a dangling IDREF is masked by a generic one.
9905
+ */
9906
+ protected resolvedTablePaginationLabel: Signal<string | undefined>;
7521
9907
  /** Emits the new 1-based page number whenever the user navigates. */
7522
9908
  pageChange: _angular_core.OutputEmitterRef<number>;
7523
9909
  /** Emits the new page-size value when the dropdown changes. */
@@ -7562,7 +9948,7 @@ declare class TnTablePagerComponent {
7562
9948
  /** Forwards the current page/size to the data provider, if one is bound. */
7563
9949
  private pushToProvider;
7564
9950
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnTablePagerComponent, never>;
7565
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnTablePagerComponent, "tn-table-pager", never, { "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "currentPage": { "alias": "currentPage"; "required": false; "isSignal": true; }; "pageSize": { "alias": "pageSize"; "required": false; "isSignal": true; }; "pageSizeOptions": { "alias": "pageSizeOptions"; "required": false; "isSignal": true; }; "totalItems": { "alias": "totalItems"; "required": false; "isSignal": true; }; "dataProvider": { "alias": "dataProvider"; "required": false; "isSignal": true; }; "itemsPerPageLabel": { "alias": "itemsPerPageLabel"; "required": false; "isSignal": true; }; "ofLabel": { "alias": "ofLabel"; "required": false; "isSignal": true; }; "firstPageLabel": { "alias": "firstPageLabel"; "required": false; "isSignal": true; }; "previousPageLabel": { "alias": "previousPageLabel"; "required": false; "isSignal": true; }; "nextPageLabel": { "alias": "nextPageLabel"; "required": false; "isSignal": true; }; "lastPageLabel": { "alias": "lastPageLabel"; "required": false; "isSignal": true; }; "tablePaginationLabel": { "alias": "tablePaginationLabel"; "required": false; "isSignal": true; }; }, { "currentPage": "currentPageChange"; "pageSize": "pageSizeChange"; "pageChange": "pageChange"; "pageSizeChange": "pageSizeChange"; }, never, never, true, never>;
9951
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnTablePagerComponent, "tn-table-pager", never, { "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "currentPage": { "alias": "currentPage"; "required": false; "isSignal": true; }; "pageSize": { "alias": "pageSize"; "required": false; "isSignal": true; }; "pageSizeOptions": { "alias": "pageSizeOptions"; "required": false; "isSignal": true; }; "totalItems": { "alias": "totalItems"; "required": false; "isSignal": true; }; "dataProvider": { "alias": "dataProvider"; "required": false; "isSignal": true; }; "itemsPerPageLabel": { "alias": "itemsPerPageLabel"; "required": false; "isSignal": true; }; "ofLabel": { "alias": "ofLabel"; "required": false; "isSignal": true; }; "firstPageLabel": { "alias": "firstPageLabel"; "required": false; "isSignal": true; }; "previousPageLabel": { "alias": "previousPageLabel"; "required": false; "isSignal": true; }; "nextPageLabel": { "alias": "nextPageLabel"; "required": false; "isSignal": true; }; "lastPageLabel": { "alias": "lastPageLabel"; "required": false; "isSignal": true; }; "tablePaginationLabel": { "alias": "tablePaginationLabel"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "ariaLabelledby"; "required": false; "isSignal": true; }; }, { "currentPage": "currentPageChange"; "pageSize": "pageSizeChange"; "pageChange": "pageChange"; "pageSizeChange": "pageSizeChange"; }, never, never, true, never>;
7566
9952
  }
7567
9953
 
7568
9954
  /**
@@ -8561,6 +10947,26 @@ declare const QuickShortcuts: {
8561
10947
  alt(key: string, platform?: PlatformType): string;
8562
10948
  };
8563
10949
 
10950
+ /**
10951
+ * Reads a label token and hands back a Signal, whichever of the two shapes the app provided.
10952
+ *
10953
+ * Every `TN_*_LABELS` token in this library is declared as `T | Signal<T>` so a consumer can
10954
+ * pass a plain object (the common case — one static bundle for a single-language app) or a
10955
+ * Signal derived from an i18n service, in which case a language switch re-renders the chrome
10956
+ * live instead of needing a reload. Components only ever want the Signal, so normalizing at
10957
+ * the injection point keeps that union from leaking into every read site.
10958
+ *
10959
+ * @param token The label token to inject.
10960
+ * @returns A Signal of the provided labels; a plain object is wrapped in a constant Signal.
10961
+ *
10962
+ * @example
10963
+ * ```typescript
10964
+ * private readonly labels = injectTnLabels(TN_TABLE_LABELS);
10965
+ * // template: {{ labels().sortBy }}
10966
+ * ```
10967
+ */
10968
+ declare function injectTnLabels<T>(token: InjectionToken<T | Signal<T>>): Signal<T>;
10969
+
8564
10970
  declare class FileSizePipe implements PipeTransform {
8565
10971
  transform(value: number): string;
8566
10972
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<FileSizePipe, never>;
@@ -8618,6 +11024,20 @@ declare function labelMarkupToHtml(value: string): string;
8618
11024
  declare function labelMarkupToText(value: string): string;
8619
11025
 
8620
11026
  type SpinnerMode = 'determinate' | 'indeterminate';
11027
+ /**
11028
+ * The accessible name a spinner falls back to when the caller names neither
11029
+ * `ariaLabel` nor `ariaLabelledby` (#202). Same reasoning as
11030
+ * `TN_PROGRESS_BAR_DEFAULT_LABEL`, and the case is sharper here: the spinner
11031
+ * defaults to indeterminate mode, so its unnamed default rendering reached
11032
+ * assistive technology as a progressbar with neither a name nor a value.
11033
+ *
11034
+ * `branded-spinner.component.ts` in this folder already fell back this way —
11035
+ * `ariaLabel() || "Loading..."` inline — so a fallback is the shape this
11036
+ * library had already settled on; what it lacked was the warning. It has both
11037
+ * since #206, through the same `tnAccessibleName` this component uses, which is
11038
+ * why the two constants sit side by side and differ.
11039
+ */
11040
+ declare const TN_SPINNER_DEFAULT_LABEL = "Loading";
8621
11041
  declare class TnSpinnerComponent {
8622
11042
  mode: _angular_core.InputSignal<SpinnerMode>;
8623
11043
  value: _angular_core.InputSignal<number>;
@@ -8625,6 +11045,18 @@ declare class TnSpinnerComponent {
8625
11045
  strokeWidth: _angular_core.InputSignal<number>;
8626
11046
  ariaLabel: _angular_core.InputSignal<string | null>;
8627
11047
  ariaLabelledby: _angular_core.InputSignal<string | null>;
11048
+ /**
11049
+ * The name to render, or `null` to render no `aria-label` attribute — and the
11050
+ * dev-mode warning when the caller named neither input.
11051
+ *
11052
+ * Both halves live in `../a11y/accessible-name`, shared with `tn-progress-bar`
11053
+ * and `tn-branded-spinner` (#206), where the reasoning for each is set out:
11054
+ * an explicit `ariaLabel` always survives, because `aria-labelledby` only wins
11055
+ * the name calculation while its IDREF resolves; the generic fallback is
11056
+ * withheld beside one, because there it would mask a dangling IDREF with a
11057
+ * name that says nothing.
11058
+ */
11059
+ resolvedAriaLabel: _angular_core.Signal<string | null>;
8628
11060
  radius: _angular_core.Signal<number>;
8629
11061
  circumference: _angular_core.Signal<number>;
8630
11062
  strokeDasharray: _angular_core.Signal<string>;
@@ -8634,8 +11066,35 @@ declare class TnSpinnerComponent {
8634
11066
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnSpinnerComponent, "tn-spinner", never, { "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "diameter": { "alias": "diameter"; "required": false; "isSignal": true; }; "strokeWidth": { "alias": "strokeWidth"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "ariaLabelledby"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
8635
11067
  }
8636
11068
 
11069
+ /**
11070
+ * The name this spinner falls back to when the caller names neither `ariaLabel`
11071
+ * nor `ariaLabelledby`. Same reasoning as `TN_SPINNER_DEFAULT_LABEL` and
11072
+ * `TN_PROGRESS_BAR_DEFAULT_LABEL`.
11073
+ *
11074
+ * It differs from `TN_SPINNER_DEFAULT_LABEL` — `"Loading..."` against
11075
+ * `"Loading"` — only because this is the string the component already rendered,
11076
+ * inline in its host binding, before #206 gave it the shared resolver. Aligning
11077
+ * the two would change what a screen reader announces for every unnamed branded
11078
+ * spinner already in the wild, which is a louder change than the consistency
11079
+ * fix it would be part of. Exported so specs assert against it by name rather
11080
+ * than by a copied string literal.
11081
+ */
11082
+ declare const TN_BRANDED_SPINNER_DEFAULT_LABEL = "Loading...";
8637
11083
  declare class TnBrandedSpinnerComponent implements OnInit, OnDestroy, AfterViewInit {
8638
11084
  ariaLabel: _angular_core.InputSignal<string | null>;
11085
+ ariaLabelledby: _angular_core.InputSignal<string | null>;
11086
+ /**
11087
+ * The name to render, or `null` to render no `aria-label` attribute — and the
11088
+ * dev-mode warning when the caller named neither input.
11089
+ *
11090
+ * This component is why `../a11y/accessible-name` exists (#206). It carried a
11091
+ * fallback inline in its host binding, so it never failed
11092
+ * `aria-progressbar-name` and the two fixes that gave the other progressbars
11093
+ * an `ariaLabelledby` input and a warning both passed it by. Routing it
11094
+ * through the shared resolver is what makes it named by the same rule rather
11095
+ * than by a third one nobody chose.
11096
+ */
11097
+ resolvedAriaLabel: _angular_core.Signal<string | null>;
8639
11098
  private paths;
8640
11099
  private animationId;
8641
11100
  private isAnimating;
@@ -8651,16 +11110,48 @@ declare class TnBrandedSpinnerComponent implements OnInit, OnDestroy, AfterViewI
8651
11110
  private animateSequence;
8652
11111
  private tween;
8653
11112
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnBrandedSpinnerComponent, never>;
8654
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnBrandedSpinnerComponent, "tn-branded-spinner", never, { "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
11113
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnBrandedSpinnerComponent, "tn-branded-spinner", never, { "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "ariaLabelledby"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
8655
11114
  }
8656
11115
 
8657
11116
  type ProgressBarMode = 'determinate' | 'indeterminate' | 'buffer';
11117
+ /**
11118
+ * The accessible name a bar falls back to when the caller names neither
11119
+ * `ariaLabel` nor `ariaLabelledby` (#202).
11120
+ *
11121
+ * Deliberately generic, and deliberately not silent: the host carries
11122
+ * `role="progressbar"` unconditionally, so without a fallback the default
11123
+ * rendering is a progressbar assistive technology announces with no name at all
11124
+ * — "progress bar, 40%", with nothing to say what is progressing. The
11125
+ * alternative fix, withholding the role until there is a name for it, trades
11126
+ * that for no announcement whatever, which is worse: a screen reader would not
11127
+ * learn that anything is in progress, and on a determinate bar it would lose
11128
+ * the value too.
11129
+ *
11130
+ * A generic name is still a poor one, so it is paired with the dev-mode warning
11131
+ * `tnAccessibleName` raises. Exported so specs assert against it by name rather
11132
+ * than by a copied string literal.
11133
+ */
11134
+ declare const TN_PROGRESS_BAR_DEFAULT_LABEL = "Progress";
8658
11135
  declare class TnProgressBarComponent {
8659
11136
  mode: _angular_core.InputSignal<ProgressBarMode>;
8660
11137
  value: _angular_core.InputSignal<number>;
8661
11138
  bufferValue: _angular_core.InputSignal<number>;
8662
11139
  ariaLabel: _angular_core.InputSignal<string | null>;
8663
11140
  ariaLabelledby: _angular_core.InputSignal<string | null>;
11141
+ /**
11142
+ * The name to render, or `null` to render no `aria-label` attribute — and the
11143
+ * dev-mode warning when the caller named neither input.
11144
+ *
11145
+ * Both halves live in `../a11y/accessible-name`, shared with `tn-spinner` and
11146
+ * `tn-branded-spinner` (#206), where the reasoning for each is set out: why an
11147
+ * explicit `ariaLabel` always survives, and why the generic fallback is
11148
+ * withheld beside an `ariaLabelledby`.
11149
+ *
11150
+ * A field initializer rather than the constructor, because it registers an
11151
+ * `effect` and so needs an injection context; this is one, and it keeps the
11152
+ * signal beside the inputs it reads.
11153
+ */
11154
+ resolvedAriaLabel: _angular_core.Signal<string | null>;
8664
11155
  /**
8665
11156
  * Gets the transform value for the primary progress bar
8666
11157
  */
@@ -8680,13 +11171,113 @@ declare class TnProgressBarComponent {
8680
11171
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnProgressBarComponent, "tn-progress-bar", never, { "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "bufferValue": { "alias": "bufferValue"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "ariaLabelledby"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
8681
11172
  }
8682
11173
 
11174
+ /**
11175
+ * The accessible name this bar falls back to when the caller names neither
11176
+ * `ariaLabel` nor `ariaLabelledby` (#209).
11177
+ *
11178
+ * The same string and the same reasoning as `TN_PROGRESS_BAR_DEFAULT_LABEL`
11179
+ * next door, and deliberately a separate constant rather than an import of it:
11180
+ * `tnAccessibleName` takes a fallback PER COMPONENT because the least-bad
11181
+ * generic name differs by what the component is ("Progress" for a bar,
11182
+ * "Loading" for a spinner), and sharing the binding would make a future
11183
+ * divergence in one a silent change to the other. Exported so specs assert
11184
+ * against it by name rather than by a copied string literal.
11185
+ */
11186
+ declare const TN_PARTICLE_PROGRESS_BAR_DEFAULT_LABEL = "Progress";
11187
+ /**
11188
+ * THE DECISION #209 ASKED FOR: THIS IS A PROGRESSBAR, NOT DECORATION
11189
+ * ------------------------------------------------------------------
11190
+ * Both readings were open. Before the change the host carried a class and
11191
+ * nothing else — no role, no name, no value — and because it never claimed
11192
+ * `role="progressbar"` it could not fail `aria-progressbar-name` either, so
11193
+ * every axe-based check in this library was silent on it by construction.
11194
+ * Measured on the unchanged component under jsdom, sweeping `svg-img-alt` and
11195
+ * the four rules `particle-progress-bar-a11y.spec.ts` keeps: 0 violations, 0
11196
+ * passes, 0 incomplete, on every one of them. Not clean — unexamined. That
11197
+ * spec keeps the measurement as its first test.
11198
+ *
11199
+ * It is a progressbar because it is a WHOLE indicator rather than an overlay on
11200
+ * someone else's. The SVG draws its own background track and its own fill rect,
11201
+ * and `fill` sizes the second against the first; that is a determinate progress
11202
+ * value, whatever the particles on top of it are doing. The alternative reading
11203
+ * — an ambient flourish shown BESIDE a real indicator, where a role would be
11204
+ * the redundant second announcement #203 was about — needs the real indicator
11205
+ * to exist, and nothing here provides one.
11206
+ *
11207
+ * The usage evidence points the same way, weakly but only in one direction. Its
11208
+ * only consumer in this repository is its own Storybook story: it has never
11209
+ * been placed next to a `tn-progress-bar`, so the redundancy risk is
11210
+ * hypothetical. It IS exported from `public-api.ts`, so consumers outside this
11211
+ * repository may already be showing it as the only progress on a screen — and
11212
+ * that asymmetry is what settles it. Choosing `aria-hidden` would assert a
11213
+ * usage constraint the library cannot enforce, and when that assertion is wrong
11214
+ * a screen-reader user gets nothing at all where a sighted user sees a bar
11215
+ * filling — strictly worse than the unnamed progressbars #202/#205/#206 fixed.
11216
+ * Choosing the role when the component really is ambient costs a redundant
11217
+ * announcement a consumer can silence with `aria-hidden` on its own wrapper.
11218
+ * The two errors are not the same size.
11219
+ *
11220
+ * The canvas IS decoration, and that is a separate question from what the host
11221
+ * is. It draws particles and carries no information the fill rect does not, so
11222
+ * the whole drawing is hidden and the ARIA value is what conveys the progress —
11223
+ * see the `aria-hidden` on the `<svg>` in the template.
11224
+ *
11225
+ * WHY THE VALUE IS A PERCENTAGE AND `fill` IS NOT
11226
+ * ----------------------------------------------
11227
+ * `fill` is a px length along the track, not a percentage — the story's control
11228
+ * runs it 0–600 while `width` runs 200–800, so the same `fill` means different
11229
+ * progress at different widths. `aria-valuenow` is reported on 0–100 instead,
11230
+ * matching `tn-progress-bar`, so that two bars from one library do not announce
11231
+ * on two different scales and no layout px reaches the accessibility tree.
11232
+ * Assistive technology derives the percentage from the range either way; what
11233
+ * this fixes is which range a consumer reads in the DOM.
11234
+ */
8683
11235
  declare class TnParticleProgressBarComponent implements AfterViewInit, OnDestroy {
8684
11236
  speed: _angular_core.InputSignal<"medium" | "slow" | "fast" | "ludicrous">;
8685
11237
  color: _angular_core.InputSignal<string>;
8686
11238
  height: _angular_core.InputSignal<number>;
8687
11239
  width: _angular_core.InputSignal<number>;
8688
11240
  fill: _angular_core.InputSignal<number>;
11241
+ ariaLabel: _angular_core.InputSignal<string | null>;
11242
+ ariaLabelledby: _angular_core.InputSignal<string | null>;
8689
11243
  canvasRef: _angular_core.Signal<ElementRef<HTMLCanvasElement>>;
11244
+ /** Exposed to the template so the rects and the ARIA value share one inset. */
11245
+ readonly trackInset = 50;
11246
+ /**
11247
+ * The name to render, or `null` to render no `aria-label` at all — and the
11248
+ * dev-mode warning when the caller named neither input.
11249
+ *
11250
+ * Both halves live in `../a11y/accessible-name`, shared with the other three
11251
+ * progressbars in this library (#206), where the reasoning for each is set
11252
+ * out: why an explicit `ariaLabel` always survives, and why the generic
11253
+ * fallback is withheld beside an `ariaLabelledby`. Routed through that helper
11254
+ * rather than given a rule of its own, which is what #209 asked for — a
11255
+ * fourth naming rule is how `tn-branded-spinner` ended up divergent.
11256
+ *
11257
+ * A field initializer rather than the constructor, because it registers an
11258
+ * `effect` and so needs an injection context; this is one, and it keeps the
11259
+ * signal beside the inputs it reads.
11260
+ */
11261
+ resolvedAriaLabel: _angular_core.Signal<string | null>;
11262
+ /** The drawable length of the track: the SVG width less the inset at each end. */
11263
+ trackLength: _angular_core.Signal<number>;
11264
+ /**
11265
+ * `fill` as a percentage of the track, or `null` when there is no track to
11266
+ * measure against.
11267
+ *
11268
+ * Clamped, because `fill` is not: the story alone can drive it to 600 against
11269
+ * a 500px track, where the fill rect simply overflows. `aria-valuenow` may not
11270
+ * exceed `aria-valuemax`, and 100 is also what such a bar visually reads as —
11271
+ * a track filled end to end. The clamp is on the announcement only; nothing
11272
+ * here changes what is drawn.
11273
+ *
11274
+ * `null` on a non-positive track — `width` at or below twice the inset — is
11275
+ * what makes the host announce as an INDETERMINATE progressbar rather than
11276
+ * carrying a value derived from a division by zero or a negative range. The
11277
+ * role stays either way, because "something is in progress" is true even when
11278
+ * how far is not answerable.
11279
+ */
11280
+ valuePercent: _angular_core.Signal<number | null>;
8690
11281
  private ctx;
8691
11282
  private particles;
8692
11283
  private shades;
@@ -8714,7 +11305,7 @@ declare class TnParticleProgressBarComponent implements AfterViewInit, OnDestroy
8714
11305
  */
8715
11306
  private generateDarkerShades;
8716
11307
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnParticleProgressBarComponent, never>;
8717
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnParticleProgressBarComponent, "tn-particle-progress-bar", never, { "speed": { "alias": "speed"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "height": { "alias": "height"; "required": false; "isSignal": true; }; "width": { "alias": "width"; "required": false; "isSignal": true; }; "fill": { "alias": "fill"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
11308
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnParticleProgressBarComponent, "tn-particle-progress-bar", never, { "speed": { "alias": "speed"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "height": { "alias": "height"; "required": false; "isSignal": true; }; "width": { "alias": "width"; "required": false; "isSignal": true; }; "fill": { "alias": "fill"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "ariaLabelledby"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
8718
11309
  }
8719
11310
 
8720
11311
  interface DateRange {
@@ -9769,8 +12360,9 @@ declare class TnSliderThumbDirective implements ControlValueAccessor, OnInit, On
9769
12360
  value: () => number;
9770
12361
  labelPrefix: () => string;
9771
12362
  labelSuffix: () => string;
9772
- ariaLabel: () => string | undefined;
9773
- ariaLabelledby: () => string | undefined;
12363
+ resolvedAriaLabel: () => string | null;
12364
+ explicitAriaLabelledby: () => string | null;
12365
+ fieldAriaLabelledby: () => string | null;
9774
12366
  updateValue: (value: number) => void;
9775
12367
  markTouched: () => void;
9776
12368
  getSliderRect: () => DOMRect;
@@ -9817,12 +12409,29 @@ declare class TnSliderThumbDirective implements ControlValueAccessor, OnInit, On
9817
12409
  private onGlobalTouchEnd;
9818
12410
  private updateValueFromPosition;
9819
12411
  /**
9820
- * Resolve the accessible name for the range input: the parent slider's
9821
- * `aria-label`/`aria-labelledby` input when set, otherwise a value placed
9822
- * directly on the `<input tnSliderThumb>`. Returning the fallback keeps the
9823
- * host binding from wiping a directly-set label. Null removes the attribute.
12412
+ * The `aria-label` for the range input: the parent slider's input when set,
12413
+ * otherwise a value placed directly on the `<input tnSliderThumb>`. Returning
12414
+ * the fallback keeps the host binding from wiping a directly-set label. Null
12415
+ * removes the attribute.
9824
12416
  */
9825
12417
  ariaLabel(): string | null;
12418
+ /**
12419
+ * The `aria-labelledby` for the range input — and the one place the precedence
12420
+ * between an explicit name and an inherited one is decided.
12421
+ *
12422
+ * An explicit reference comes first, from the slider's input or from this
12423
+ * input itself. **An enclosing `tn-form-field`'s label comes last, and only
12424
+ * when nothing has named the control directly.** That ordering is the point:
12425
+ * `aria-labelledby` beats `aria-label` in the ARIA name calculation whenever
12426
+ * it resolves, so emitting the field's label beside an `aria-label` written on
12427
+ * this input would render both attributes and announce the FIELD's label —
12428
+ * silently replacing the name the consumer wrote on the control (#235).
12429
+ *
12430
+ * The slider's own `ariaLabel` input needs no such check here, because
12431
+ * `injectTnFormFieldAria` already withholds the field's label whenever it is
12432
+ * set; this covers the label that is written past the slider, onto the
12433
+ * projected input, which the slider's inputs never see.
12434
+ */
9826
12435
  ariaLabelledby(): string | null;
9827
12436
  /**
9828
12437
  * Commit a value that originated outside the native input (a thumb drag).
@@ -9860,13 +12469,63 @@ declare class TnSliderComponent implements ControlValueAccessor, OnDestroy, Afte
9860
12469
  /**
9861
12470
  * Accessible name forwarded to the inner range input — the focusable element
9862
12471
  * screen readers actually announce. Set this (or `aria-labelledby`) when the
9863
- * slider isn't already labelled by a `tn-form-field`/`<label>`, otherwise a
12472
+ * slider isn't already inside a labelled `tn-form-field`, otherwise a
9864
12473
  * standalone `<tn-slider><input tnSliderThumb></tn-slider>` announces only
9865
- * "slider". A label set directly on the `input[tnSliderThumb]` is used as a
9866
- * fallback when neither is provided here.
12474
+ * "slider".
12475
+ *
12476
+ * Precedence is per attribute: each of `aria-label` and `aria-labelledby` is
12477
+ * taken from the matching input here, else from one written directly on the
12478
+ * `input[tnSliderThumb]`. Between the two attributes, ARIA's own rule decides
12479
+ * — `aria-labelledby` wins wherever it resolves.
12480
+ *
12481
+ * An enclosing `tn-form-field`'s label comes after all of those, and only when
12482
+ * nothing above has named the control: it is chrome the consumer did not write
12483
+ * on the control. See `TnSliderThumbDirective.ariaLabelledby`, where the
12484
+ * ordering is applied.
9867
12485
  */
9868
12486
  ariaLabel: _angular_core.InputSignal<string | undefined>;
9869
12487
  ariaLabelledby: _angular_core.InputSignal<string | undefined>;
12488
+ /**
12489
+ * ARIA wiring from an enclosing `tn-form-field`, read for `labelledby` alone:
12490
+ * #235 is about the range input having no accessible name, and the field's
12491
+ * `describedby`/`invalid`/`required` are a separate question this slider has
12492
+ * never answered either way.
12493
+ *
12494
+ * Called with NO argument, so it reports the field's label id unconditioned.
12495
+ * Handing it the `ariaLabel` input would have it suppress the field itself —
12496
+ * on truthiness, so a whitespace-only label would cancel the field while being
12497
+ * dropped as no name, leaving nothing — and it would only do so while this
12498
+ * field is initialised after the one it reads, since a signal captured before
12499
+ * its own initialiser runs arrives as `undefined` and is swallowed by an
12500
+ * optional call. The suppression is the thumb's, where the rest of the
12501
+ * precedence already lives and where it is visible.
12502
+ */
12503
+ private readonly fieldAria;
12504
+ /**
12505
+ * The `aria-label` the thumb should render, or `null` for none.
12506
+ *
12507
+ * Blank is not a name: `aria-label=""` names the input as emptily as no
12508
+ * attribute at all, while satisfying axe's `label` rule — a green check on a
12509
+ * control a screen reader announces as "slider" (#235). Same reasoning as
12510
+ * `a11y/accessible-name.ts`, which the three progressbars share.
12511
+ */
12512
+ readonly resolvedAriaLabel: _angular_core.Signal<string | null>;
12513
+ /**
12514
+ * The `ariaLabelledby` INPUT, blank normalised away, or `null` when unset.
12515
+ *
12516
+ * Kept separate from {@link fieldAriaLabelledby} rather than folded into one
12517
+ * resolved value, because the two rank differently against a label written on
12518
+ * the projected input — see `TnSliderThumbDirective.ariaLabelledby`.
12519
+ */
12520
+ readonly explicitAriaLabelledby: _angular_core.Signal<string | null>;
12521
+ /**
12522
+ * The enclosing `tn-form-field`'s label id, or `null` outside one.
12523
+ *
12524
+ * With no field and no input, a slider stays unnamed — deliberately: a
12525
+ * generic fallback ("Slider") would satisfy axe while announcing nothing the
12526
+ * user can act on, and only the consumer knows what this slider controls.
12527
+ */
12528
+ readonly fieldAriaLabelledby: _angular_core.Signal<string | null>;
9870
12529
  thumbDirective: _angular_core.Signal<TnSliderThumbDirective>;
9871
12530
  sliderContainer: _angular_core.Signal<ElementRef<HTMLDivElement>>;
9872
12531
  private onChange;
@@ -10175,11 +12834,243 @@ interface ButtonToggleHarnessFilters extends BaseHarnessFilters {
10175
12834
  label?: string | RegExp;
10176
12835
  }
10177
12836
 
12837
+ /**
12838
+ * The visual half of `tnTooltip`, and — while it is only shown on hover — only the visual half
12839
+ * (#203).
12840
+ *
12841
+ * WHY THE HOVER PANEL IS `aria-hidden` AND CARRIES NO ROLE
12842
+ * --------------------------------------------------------
12843
+ * The accessible text is `TnTooltipDirective`'s job, and it routes it through
12844
+ * CDK's `AriaDescriber` — a persistent visually-hidden element referenced by
12845
+ * `aria-describedby` on the interactive control, so the reference never dangles
12846
+ * while this overlay comes and goes. That is one tooltip, described once.
12847
+ *
12848
+ * This node used to ALSO claim `role="tooltip"`, which put a second tooltip in
12849
+ * the accessibility tree for the same message: the describer's (named, and
12850
+ * referenced by the control) and this one (named by nothing when the message is
12851
+ * empty or markup-only). axe scores that `aria-tooltip-name`, WCAG 4.1.2.
12852
+ * Of the two models the ticket set out — make the overlay the accessible
12853
+ * tooltip, or make it decorative — decorative is the one the rest of the
12854
+ * directive is already built for, and it is what `pointer-events: none` on
12855
+ * `:host` already says: this element cannot be hovered, clicked or interacted
12856
+ * with. Hiding it removes the duplicate rather than moving the name onto it.
12857
+ *
12858
+ * `aria-hidden="true"` is bound to `sticky` rather than to a visibility signal.
12859
+ * The node is exposed in no state a hover tooltip can be in, so there is no
12860
+ * visibility for it to reflect — the old hard-coded `aria-hidden="false"` was
12861
+ * the bug, because that value DID depend on state it was not tracking.
12862
+ *
12863
+ * AND WHY A PINNED ONE IS THE OPPOSITE
12864
+ * ------------------------------------
12865
+ * `sticky` is the one state where the panel is more than decoration: it takes
12866
+ * pointer events, holds focusable content, and renders a dismiss button, so
12867
+ * hiding it would put focusable nodes inside an `aria-hidden` subtree and make
12868
+ * the very content pinning exists to reach unreachable. A pinned panel is
12869
+ * therefore an exposed, named `dialog` (see `sticky`), and the describer's
12870
+ * description on the host stands alongside it rather than in place of it.
12871
+ */
10178
12872
  declare class TnTooltipComponent {
10179
12873
  message: _angular_core.InputSignal<string>;
12874
+ /**
12875
+ * Optional DOM id for the rendered element. Omitted entirely when empty
12876
+ * rather than rendered as `id=""`, which no `aria-describedby` or selector can
12877
+ * reference — a silently dangling hook.
12878
+ */
10180
12879
  id: _angular_core.InputSignal<string>;
12880
+ /**
12881
+ * Pinned ("sticky") mode. The tooltip stops being click-through so its content can be
12882
+ * interacted with, and a dismiss button is rendered next to the message.
12883
+ *
12884
+ * It also changes what the panel *is* for assistive tech: ARIA's `tooltip` role is specified as
12885
+ * non-focusable, non-interactive content that something else is described by, so a screen
12886
+ * reader may flatten it to a text description and never expose the link or the dismiss button -
12887
+ * exactly what pinning exists to make reachable. A pinned panel is therefore a `dialog`, named
12888
+ * by `panelAriaLabel`.
12889
+ *
12890
+ * It is a *non-modal* dialog and deliberately traps nothing: Tab past the dismiss button leaves
12891
+ * the panel while it stays open. Where focus lands next is wherever the panel sits in the tab
12892
+ * order, which is the end of it - CDK appends its overlay container as the last child of
12893
+ * `<body>` - so in practice Tab leaves the document for the browser's own chrome rather than
12894
+ * continuing after the host. Not trapping is the right shape for a popup the user can also
12895
+ * leave by Escape or by clicking outside, and it keeps a tooltip from holding the keyboard
12896
+ * hostage.
12897
+ */
12898
+ sticky: _angular_core.InputSignal<boolean>;
12899
+ /** Accessible name for the dismiss button, so consumers can localize it. */
12900
+ closeAriaLabel: _angular_core.InputSignal<string>;
12901
+ /**
12902
+ * Accessible name for the pinned panel (see `sticky`), so consumers can localize it.
12903
+ *
12904
+ * A short static name rather than the message: a screen reader reads a dialog's name on entry
12905
+ * and then its content, so naming it after the message would announce that message twice —
12906
+ * three times counting the host's own description.
12907
+ */
12908
+ panelAriaLabel: _angular_core.InputSignal<string>;
12909
+ /** Emitted when the user activates the dismiss button. */
12910
+ onDismiss: _angular_core.OutputEmitterRef<void>;
12911
+ private panel;
12912
+ /**
12913
+ * Moves focus onto the tooltip panel. Used when sticky mode is entered from the keyboard, so
12914
+ * the tooltip's content is reachable without a pointer: from the panel, Tab walks the message
12915
+ * (links included) and then the dismiss button. The panel is only focusable in sticky mode,
12916
+ * so this is a no-op otherwise.
12917
+ */
12918
+ focusPanel(): void;
10181
12919
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnTooltipComponent, never>;
10182
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnTooltipComponent, "tn-tooltip", never, { "message": { "alias": "message"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
12920
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnTooltipComponent, "tn-tooltip", never, { "message": { "alias": "message"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "sticky": { "alias": "sticky"; "required": false; "isSignal": true; }; "closeAriaLabel": { "alias": "closeAriaLabel"; "required": false; "isSignal": true; }; "panelAriaLabel": { "alias": "panelAriaLabel"; "required": false; "isSignal": true; }; }, { "onDismiss": "onDismiss"; }, never, never, true, never>;
12921
+ }
12922
+
12923
+ /**
12924
+ * Harness for interacting with a tooltip rendered by `tnTooltip` in tests.
12925
+ *
12926
+ * Tooltips render in a CDK overlay **outside** the component tree, so the regular
12927
+ * `TestbedHarnessEnvironment.loader(fixture)` won't find them. Use
12928
+ * `TnTooltipTesting.rootLoader(fixture)` instead — it searches the whole document.
12929
+ *
12930
+ * @example
12931
+ * ```typescript
12932
+ * import { TnTooltipHarness, TnTooltipTesting } from '@truenas/ui-components';
12933
+ *
12934
+ * const rootLoader = TnTooltipTesting.rootLoader(fixture);
12935
+ *
12936
+ * // Hover tooltip: show it, then read it
12937
+ * host.dispatchEvent(new MouseEvent('mouseenter'));
12938
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
12939
+ * expect(await tooltip.getText()).toBe('Pool is online');
12940
+ *
12941
+ * // Sticky tooltip: click the host to pin it, then dismiss it
12942
+ * host.click();
12943
+ * const pinned = await rootLoader.getHarness(TnTooltipHarness);
12944
+ * expect(await pinned.isSticky()).toBe(true);
12945
+ * await pinned.dismiss();
12946
+ * ```
12947
+ */
12948
+ declare class TnTooltipHarness extends ComponentHarness {
12949
+ /**
12950
+ * The selector for the host element of a `TnTooltipComponent` instance.
12951
+ */
12952
+ static hostSelector: string;
12953
+ private _panel;
12954
+ private _message;
12955
+ private _closeButton;
12956
+ /**
12957
+ * Gets a `HarnessPredicate` that can be used to search for a tooltip with specific
12958
+ * attributes. Useful when more than one tooltip is on screen — a pinned one plus a hover
12959
+ * one, for instance.
12960
+ *
12961
+ * @param options Options for filtering which tooltip instances are considered a match.
12962
+ * @returns A `HarnessPredicate` configured with the given options.
12963
+ *
12964
+ * @example
12965
+ * ```typescript
12966
+ * // Find a tooltip by its text
12967
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness.with({ text: 'Pool is online' }));
12968
+ *
12969
+ * // Find only the pinned one
12970
+ * const pinned = await rootLoader.getHarness(TnTooltipHarness.with({ sticky: true }));
12971
+ * ```
12972
+ */
12973
+ static with(options?: TooltipHarnessFilters): HarnessPredicate<TnTooltipHarness>;
12974
+ /**
12975
+ * Gets the tooltip's text content, with any markup in the message stripped.
12976
+ *
12977
+ * @returns Promise resolving to the tooltip's text.
12978
+ *
12979
+ * @example
12980
+ * ```typescript
12981
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
12982
+ * expect(await tooltip.getText()).toBe('Pool is online');
12983
+ * ```
12984
+ */
12985
+ getText(): Promise<string>;
12986
+ /**
12987
+ * Checks whether the tooltip is pinned open (sticky mode), i.e. interactive and dismissible
12988
+ * rather than tied to the pointer.
12989
+ *
12990
+ * @returns Promise resolving to true if the tooltip is pinned.
12991
+ *
12992
+ * @example
12993
+ * ```typescript
12994
+ * host.click();
12995
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
12996
+ * expect(await tooltip.isSticky()).toBe(true);
12997
+ * ```
12998
+ */
12999
+ isSticky(): Promise<boolean>;
13000
+ /**
13001
+ * Gets the accessible name of the dismiss button, or null when the tooltip isn't pinned.
13002
+ *
13003
+ * @returns Promise resolving to the dismiss button's aria-label.
13004
+ *
13005
+ * @example
13006
+ * ```typescript
13007
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
13008
+ * expect(await tooltip.getDismissLabel()).toBe('Close tooltip');
13009
+ * ```
13010
+ */
13011
+ getDismissLabel(): Promise<string | null>;
13012
+ /**
13013
+ * Clicks the tooltip's dismiss button. Throws if the tooltip isn't pinned, since the button
13014
+ * only exists in sticky mode.
13015
+ *
13016
+ * @returns Promise that resolves when the tooltip has been dismissed.
13017
+ *
13018
+ * @example
13019
+ * ```typescript
13020
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
13021
+ * await tooltip.dismiss();
13022
+ * ```
13023
+ */
13024
+ dismiss(): Promise<void>;
13025
+ /**
13026
+ * Clicks an element inside the tooltip's message — a link, typically. Only meaningful in
13027
+ * sticky mode, where the tooltip stops being click-through.
13028
+ *
13029
+ * @param selector CSS selector of the element to click, relative to the message.
13030
+ * @returns Promise that resolves when the click action is complete.
13031
+ *
13032
+ * @example
13033
+ * ```typescript
13034
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
13035
+ * await tooltip.clickContent('a');
13036
+ * ```
13037
+ */
13038
+ clickContent(selector: string): Promise<void>;
13039
+ }
13040
+ /**
13041
+ * A set of criteria that can be used to filter a list of `TnTooltipHarness` instances.
13042
+ */
13043
+ interface TooltipHarnessFilters extends BaseHarnessFilters {
13044
+ /** Filters by the tooltip's text. Supports string or regex matching. */
13045
+ text?: string | RegExp;
13046
+ /** Filters by whether the tooltip is pinned open (sticky mode). */
13047
+ sticky?: boolean;
13048
+ }
13049
+
13050
+ /**
13051
+ * Test utilities for working with `TnTooltipHarness`.
13052
+ *
13053
+ * Tooltips are portaled into the CDK overlay outside the component tree, so a regular
13054
+ * `TestbedHarnessEnvironment.loader()` won't find them. Use `TnTooltipTesting.rootLoader()`
13055
+ * to get a loader that can.
13056
+ *
13057
+ * @example
13058
+ * ```typescript
13059
+ * import { TnTooltipTesting, TnTooltipHarness } from '@truenas/ui-components';
13060
+ *
13061
+ * const rootLoader = TnTooltipTesting.rootLoader(fixture);
13062
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
13063
+ * ```
13064
+ */
13065
+ declare class TnTooltipTesting {
13066
+ /**
13067
+ * Creates a `HarnessLoader` that searches the entire document, including the CDK overlays
13068
+ * that tooltips are rendered into.
13069
+ *
13070
+ * @param fixture The component fixture for the test.
13071
+ * @returns A `HarnessLoader` capable of finding tooltip harnesses.
13072
+ */
13073
+ static rootLoader(fixture: ComponentFixture<unknown>): HarnessLoader;
10183
13074
  }
10184
13075
 
10185
13076
  interface TnConfirmDialogData {
@@ -10234,7 +13125,56 @@ declare class TnDialog {
10234
13125
  static ɵprov: _angular_core.ɵɵInjectableDeclaration<TnDialog>;
10235
13126
  }
10236
13127
 
13128
+ /**
13129
+ * Chrome copy `tn-dialog-shell` renders itself. These were baked into the template as English
13130
+ * literals, which a consumer could not translate at all — there was no input to bind. Provide
13131
+ * {@link TN_DIALOG_CHROME_LABELS} at the app root to wire them to an i18n service.
13132
+ *
13133
+ * "Chrome" rather than plain `TnDialogLabels` to keep this bundle a clear letter apart from
13134
+ * {@link TN_DIALOG_SHELL_DEFAULT_LABEL}, which is a different thing entirely — the fallback
13135
+ * accessible name for the dialog surface itself, not copy the header renders.
13136
+ */
13137
+ interface TnDialogChromeLabels {
13138
+ /** Accessible name for the header close (X) button. */
13139
+ close: string;
13140
+ /** Accessible name for the fullscreen toggle while the dialog is windowed. */
13141
+ enterFullscreen: string;
13142
+ /** Accessible name for the fullscreen toggle while the dialog is fullscreen. */
13143
+ exitFullscreen: string;
13144
+ }
13145
+ /** English defaults used when no `TN_DIALOG_CHROME_LABELS` provider is registered. */
13146
+ declare const TN_DIALOG_DEFAULT_CHROME_LABELS: TnDialogChromeLabels;
13147
+ /**
13148
+ * DI token for app-wide dialog chrome labels. Provide either a static object or a
13149
+ * `Signal<TnDialogChromeLabels>` — the latter lets every dialog react to language changes when
13150
+ * the consumer wires it up to an i18n service.
13151
+ */
13152
+ declare const TN_DIALOG_CHROME_LABELS: InjectionToken<TnDialogChromeLabels | Signal<TnDialogChromeLabels>>;
13153
+ /**
13154
+ * The accessible name a dialog falls back to when it renders no `title` and the
13155
+ * caller named it through neither this component nor the `DialogConfig` (#219).
13156
+ *
13157
+ * `title` defaults to `''`, so the DEFAULT rendering of this component put an
13158
+ * empty `<h2>` in the header and left the CDK container with no naming
13159
+ * attribute at all — measured as `empty-heading` on the heading and
13160
+ * `aria-dialog-name` on the dialog. A dialog with no name is announced as
13161
+ * "dialog" and nothing else, which is the whole of what a screen-reader user is
13162
+ * told about a surface that just took over the page and trapped their focus.
13163
+ *
13164
+ * "Dialog" is a poor name, and says almost exactly what the role already says.
13165
+ * It is still better than the two alternatives: leaving the surface unnamed, or
13166
+ * withholding `role="dialog"` until there is a name — the latter would move a
13167
+ * listener into a focus trap with no announcement that anything had opened. So
13168
+ * it is paired with the dev-mode warning `tnAccessibleName` raises, which is
13169
+ * what keeps the fallback from becoming a quiet way to ship a nameless dialog.
13170
+ *
13171
+ * Exported so specs assert against it by name rather than by a copied literal.
13172
+ */
13173
+ declare const TN_DIALOG_SHELL_DEFAULT_LABEL = "Dialog";
10237
13174
  declare class TnDialogShellComponent implements OnInit {
13175
+ protected readonly labels: Signal<TnDialogChromeLabels>;
13176
+ /** Accessible name for the fullscreen toggle, following its current state. */
13177
+ protected readonly fullscreenLabel: Signal<string>;
10238
13178
  title: _angular_core.InputSignal<string>;
10239
13179
  showFullscreenButton: _angular_core.InputSignal<boolean>;
10240
13180
  /**
@@ -10260,6 +13200,22 @@ declare class TnDialogShellComponent implements OnInit {
10260
13200
  * provided (useful when more than one dialog can be open).
10261
13201
  */
10262
13202
  testId: _angular_core.InputSignal<TnTestIdValue>;
13203
+ /**
13204
+ * Accessible name for the dialog itself, for a dialog that renders no `title`.
13205
+ *
13206
+ * A `title` outranks it: the heading is what the user can see, and
13207
+ * `aria-labelledby` wins the ARIA name calculation while it resolves. The
13208
+ * attribute is still rendered beside the heading rather than suppressed — see
13209
+ * `tnAccessibleName`, which owns that rule for every component in this
13210
+ * library, and the reason it is safer than the alternative.
13211
+ */
13212
+ ariaLabel: _angular_core.InputSignal<string | null>;
13213
+ /**
13214
+ * IDREF naming the dialog from text elsewhere on the page, for a dialog that
13215
+ * renders no `title`. Same precedence: a `title` wins, because it is the
13216
+ * visible heading.
13217
+ */
13218
+ ariaLabelledby: _angular_core.InputSignal<string | null>;
10263
13219
  /**
10264
13220
  * The `testId` base normalized to a flat segment array. Nothing is dropped
10265
13221
  * here — `composeTestId` (via the `[tnTestId]` directive) filters falsy
@@ -10276,9 +13232,9 @@ declare class TnDialogShellComponent implements OnInit {
10276
13232
  * close-button ids and lets automation target "all close buttons" with one
10277
13233
  * selector.
10278
13234
  */
10279
- protected closeTestId: _angular_core.Signal<(string | number | null | undefined)[]>;
13235
+ protected closeTestId: Signal<(string | number | null | undefined)[]>;
10280
13236
  /** Role-first test-id segments for the fullscreen button: `button-fullscreen[-<base>]`. */
10281
- protected fullscreenTestId: _angular_core.Signal<(string | number | null | undefined)[]>;
13237
+ protected fullscreenTestId: Signal<(string | number | null | undefined)[]>;
10282
13238
  /** Stable id for the title heading, referenced by the dialog's aria-labelledby. */
10283
13239
  readonly titleId: string;
10284
13240
  isFullscreen: _angular_core.WritableSignal<boolean>;
@@ -10287,14 +13243,52 @@ declare class TnDialogShellComponent implements OnInit {
10287
13243
  private document;
10288
13244
  private host;
10289
13245
  private data;
13246
+ /**
13247
+ * Whether there is a heading to render, and to name the dialog from.
13248
+ *
13249
+ * Trimmed, because a whitespace-only title renders a heading that looks empty
13250
+ * to a sighted user and names the dialog with nothing — which is the state
13251
+ * this ticket fixed, arriving by a second route.
13252
+ */
13253
+ protected hasTitle: Signal<boolean>;
13254
+ /**
13255
+ * What the dialog is named by, in the order ARIA resolves: the visible
13256
+ * heading when there is one, then the caller's IDREF, then one passed to
13257
+ * `TnDialog.open` in the `DialogConfig`.
13258
+ *
13259
+ * The config is consulted so that the fallback below cannot overwrite a name
13260
+ * the opener supplied through CDK's own route. Reading it back through
13261
+ * `ref.config` rather than leaving CDK's binding to render it, because this
13262
+ * component writes both attributes onto that element and would otherwise
13263
+ * clear one it did not set.
13264
+ *
13265
+ * Guarded, even though `DialogRef.config` is non-optional: a shell rendered
13266
+ * directly in a consumer's test gets its `DialogRef` from a mock provider,
13267
+ * which supplies the methods but no config. Naming then falls back to the
13268
+ * component's own inputs, which is what such a test is exercising anyway.
13269
+ */
13270
+ private resolvedAriaLabelledby;
13271
+ /** An explicit label, from this component's input or from the `DialogConfig`. */
13272
+ private explicitAriaLabel;
13273
+ /**
13274
+ * The name to render as `aria-label`, or `null` to render none — and the
13275
+ * dev-mode warning when the dialog has no name from any route.
13276
+ *
13277
+ * Both halves live in `../a11y/accessible-name`, shared with `tn-side-panel`,
13278
+ * `tn-drawer` and the three progressbars, where the reasoning for each branch
13279
+ * is set out. `title` reaches it as the `ariaLabelledby` above, so a titled
13280
+ * dialog is named, takes no fallback and raises no warning.
13281
+ */
13282
+ private resolvedAriaLabel;
10290
13283
  constructor();
13284
+ private applyName;
10291
13285
  ngOnInit(): void;
10292
13286
  close(result?: unknown): void;
10293
13287
  toggleFullscreen(): void;
10294
13288
  private enterFullscreen;
10295
13289
  private exitFullscreen;
10296
13290
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnDialogShellComponent, never>;
10297
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnDialogShellComponent, "tn-dialog-shell", never, { "title": { "alias": "title"; "required": false; "isSignal": true; }; "showFullscreenButton": { "alias": "showFullscreenButton"; "required": false; "isSignal": true; }; "showCloseButton": { "alias": "showCloseButton"; "required": false; "isSignal": true; }; "hideContent": { "alias": "hideContent"; "required": false; "isSignal": true; }; "hideActions": { "alias": "hideActions"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, {}, never, ["*", "[tnDialogAction]"], true, never>;
13291
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnDialogShellComponent, "tn-dialog-shell", never, { "title": { "alias": "title"; "required": false; "isSignal": true; }; "showFullscreenButton": { "alias": "showFullscreenButton"; "required": false; "isSignal": true; }; "showCloseButton": { "alias": "showCloseButton"; "required": false; "isSignal": true; }; "hideContent": { "alias": "hideContent"; "required": false; "isSignal": true; }; "hideActions": { "alias": "hideActions"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "ariaLabelledby"; "required": false; "isSignal": true; }; }, {}, never, ["*", "[tnDialogAction]"], true, never>;
10298
13292
  }
10299
13293
 
10300
13294
  /**
@@ -10346,7 +13340,9 @@ declare class TnDialogHarness extends ComponentHarness {
10346
13340
  */
10347
13341
  static with(options?: DialogHarnessFilters): HarnessPredicate<TnDialogHarness>;
10348
13342
  /**
10349
- * Gets the dialog's title text.
13343
+ * Gets the dialog's title text, or `''` for a dialog opened with no title —
13344
+ * which renders no heading at all (#219), and is named by `ariaLabel` or
13345
+ * `ariaLabelledby` instead.
10350
13346
  *
10351
13347
  * @returns Promise resolving to the dialog title.
10352
13348
  *
@@ -10436,8 +13432,11 @@ declare class TnDialogHarness extends ComponentHarness {
10436
13432
  */
10437
13433
  toggleFullscreen(): Promise<void>;
10438
13434
  /**
10439
- * Whether the dialog is currently in fullscreen mode.
10440
- * Checks the aria-label of the fullscreen button.
13435
+ * Whether the dialog is currently in fullscreen mode, read from the fullscreen toggle's
13436
+ * `data-fullscreen`. Deliberately not derived from the button's `aria-label`: that label is
13437
+ * app-configurable through `TN_DIALOG_CHROME_LABELS`, so matching display text would report
13438
+ * `false` forever the moment a consumer translated the chrome — silently, in exactly the apps
13439
+ * the token exists for. Same call `TnTableHarness.getCardSortDirection` makes.
10441
13440
  *
10442
13441
  * @returns Promise resolving to true if fullscreen, false if not or if no fullscreen button.
10443
13442
  */
@@ -10488,6 +13487,53 @@ declare class TnDialogTesting {
10488
13487
  static rootLoader(fixture: ComponentFixture<unknown>): HarnessLoader;
10489
13488
  }
10490
13489
 
13490
+ /**
13491
+ * The accessible name an open panel falls back to when it has no `title` and the
13492
+ * caller named neither `ariaLabel` nor `ariaLabelledby` (#214).
13493
+ *
13494
+ * `title` defaults to `''`, so the DEFAULT rendering of this component was a
13495
+ * `role="dialog"` with `aria-labelledby` pointing at an empty `<h2>` — measured
13496
+ * as an `aria-dialog-name` violation, alongside `empty-heading`. A dialog with no
13497
+ * name is announced as "dialog" and nothing else, which is the whole of what a
13498
+ * screen-reader user gets told about a surface that just covered the page.
13499
+ *
13500
+ * Withholding `role="dialog"` until there is a name would be the other way to
13501
+ * clear the rule, and it is worse: the panel traps focus either way, so a
13502
+ * listener would be moved into a region with no announcement that anything had
13503
+ * opened. A generic name is still a poor one, so it is paired with the dev-mode
13504
+ * warning `tnAccessibleName` raises.
13505
+ *
13506
+ * Exported so specs assert against it by name rather than by a copied literal.
13507
+ */
13508
+ declare const TN_SIDE_PANEL_DEFAULT_LABEL = "Side panel";
13509
+ /**
13510
+ * The name given to the scrolling content region once it becomes focusable
13511
+ * (#248).
13512
+ *
13513
+ * A focusable element with no accessible name is announced as a bare "group",
13514
+ * which tells a listener that something has been reached and nothing about what
13515
+ * it is. It names the region rather than repeating the panel's own title: the
13516
+ * dialog announces that on entry, so a second copy of it here would say the
13517
+ * same words twice and still not distinguish the part that scrolls.
13518
+ *
13519
+ * Overridable through `contentAriaLabel`, on the same reasoning as
13520
+ * `closeButtonAriaLabel` — a string this library renders into a consumer's UI
13521
+ * has to be translatable. Exported so specs assert against it by name rather
13522
+ * than by a copied literal.
13523
+ */
13524
+ declare const TN_SIDE_PANEL_CONTENT_LABEL = "Panel content";
13525
+ /**
13526
+ * How far the content has to exceed the region before the region counts as
13527
+ * scrolling (#248).
13528
+ *
13529
+ * **This is now `TN_SCROLLABLE_REGION_TOLERANCE_PX`**, which is axe's own 13px
13530
+ * buffer and lives with the measurement it belongs to (#270). The alias stays
13531
+ * because it is what `side-panel-scrollable-content.spec.ts` pins against axe
13532
+ * from both sides, and that spec is a guard on this component rather than on
13533
+ * the helper — see `../a11y/scrollable-region.ts` for what the number is and
13534
+ * why it is copied from the rule at all.
13535
+ */
13536
+ declare const TN_SIDE_PANEL_OVERFLOW_TOLERANCE_PX = 13;
10491
13537
  /**
10492
13538
  * Directive to mark an element as a side-panel footer action.
10493
13539
  *
@@ -10517,12 +13563,49 @@ declare class TnSidePanelHeaderActionDirective {
10517
13563
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnSidePanelHeaderActionDirective, never>;
10518
13564
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnSidePanelHeaderActionDirective, "[tnSidePanelHeaderAction]", never, {}, {}, never, never, true, never>;
10519
13565
  }
13566
+ /**
13567
+ * A modal side panel: `role="dialog"` with `aria-modal="true"`, focus trapped
13568
+ * while it is open and restored to the opener when it closes.
13569
+ *
13570
+ * FOCUS ON OPEN
13571
+ * -------------
13572
+ * Opening moves focus to the panel container, whatever you projected into it,
13573
+ * so that a screen reader announces the dialog it has just entered before any
13574
+ * control in it. The first Tab then reaches the close button.
13575
+ *
13576
+ * **`[cdkFocusInitial]` is not honoured** (#227). It used to be, through the
13577
+ * CDK auto-capture this replaced, and `cdkTrapFocus` is still on the panel — so
13578
+ * the marker looks like it should work and does not. To focus a control of your
13579
+ * own, focus it yourself once the panel is open; the component leaves focus
13580
+ * alone as soon as it is inside the panel. `lib/a11y/initial-focus.ts` holds
13581
+ * the reasoning for capturing the container rather than a control.
13582
+ */
10520
13583
  declare class TnSidePanelComponent implements OnDestroy {
10521
13584
  private iconRegistry;
10522
13585
  private document;
10523
13586
  private destroyRef;
10524
13587
  private overlayRef;
13588
+ private panelRef;
13589
+ private contentRef;
10525
13590
  protected initialized: _angular_core.WritableSignal<boolean>;
13591
+ /**
13592
+ * Whether the content region carries the tab stop, its role and its name
13593
+ * (#248) — which is NOT the same question as whether it currently overflows.
13594
+ *
13595
+ * The measurement, the two observers that keep it current and the focus rule
13596
+ * that decides when the attributes may be taken off again are all
13597
+ * `tnScrollableRegion`'s (#270); this component decides only what to put on
13598
+ * the element, which is the part that differs between the five regions in
13599
+ * this library that scroll. `../a11y/scrollable-region.ts` sets out why the
13600
+ * answer is held true while the region has focus, and why `role` and
13601
+ * `aria-label` are gated on the same signal as `tabindex` rather than left on.
13602
+ *
13603
+ * Read in `afterNextRender`, which is where the helper takes its first
13604
+ * measurement — before the overlay is portaled to `<body>` below, which does
13605
+ * not affect it: `.tn-side-panel__overlay` is `position: fixed; inset: 0`, so
13606
+ * its size comes from the viewport rather than from its parent.
13607
+ */
13608
+ protected contentKeyboardReachable: _angular_core.Signal<boolean>;
10526
13609
  open: _angular_core.ModelSignal<boolean>;
10527
13610
  title: _angular_core.InputSignal<string>;
10528
13611
  width: _angular_core.InputSignal<string>;
@@ -10552,13 +13635,83 @@ declare class TnSidePanelComponent implements OnDestroy {
10552
13635
  * of context otherwise hears only "Dismiss".
10553
13636
  */
10554
13637
  closeButtonAriaLabel: _angular_core.InputSignal<string>;
13638
+ /**
13639
+ * Accessible name for the scrolling content region, which is named only while
13640
+ * it is focusable — see `TN_SIDE_PANEL_CONTENT_LABEL`. Override it to
13641
+ * translate it, or to say what the region holds ("Dataset properties").
13642
+ */
13643
+ contentAriaLabel: _angular_core.InputSignal<string>;
13644
+ /**
13645
+ * Accessible name for the panel itself, for a panel that renders no `title`.
13646
+ *
13647
+ * A `title` outranks it: the heading is what the user can see, and
13648
+ * `aria-labelledby` wins the ARIA name calculation while it resolves. The
13649
+ * attribute is still rendered beside the heading rather than suppressed — see
13650
+ * `tnAccessibleName`, which owns that rule for every component in this
13651
+ * library, and the reason it is safer than the alternative.
13652
+ */
13653
+ ariaLabel: _angular_core.InputSignal<string | null>;
13654
+ /**
13655
+ * IDREF naming the panel from text elsewhere on the page, for a panel that
13656
+ * renders no `title`. Same precedence: a `title` wins, because it is the
13657
+ * visible heading.
13658
+ */
13659
+ ariaLabelledby: _angular_core.InputSignal<string | null>;
13660
+ /**
13661
+ * Fires once the panel has finished opening.
13662
+ *
13663
+ * "Finished" means the open transition ended, OR that it was going to take
13664
+ * longer than `TN_TRANSITION_FALLBACK_MS` to say so — which is what a user
13665
+ * with `prefers-reduced-motion: reduce` gets, since this component's own
13666
+ * stylesheet zeroes the duration for them and a transition that does not run
13667
+ * fires no `transitionend` (#218). A consumer may assume the panel has
13668
+ * reached its open state and that `open()` is true; it may NOT assume the
13669
+ * animation is visually complete, because for that user there was none.
13670
+ */
10555
13671
  opened: _angular_core.OutputEmitterRef<void>;
13672
+ /**
13673
+ * Fires once the panel has finished closing. Same guarantee as `opened`, and
13674
+ * the same caveat: it reports the state, not the animation.
13675
+ *
13676
+ * Focus restoration does NOT hang off this — it happens as soon as the panel
13677
+ * closes (#214). See the effect in the constructor.
13678
+ */
10556
13679
  closed: _angular_core.OutputEmitterRef<void>;
10557
13680
  private actionContent;
10558
13681
  protected hasActions: _angular_core.Signal<boolean>;
10559
13682
  readonly panelId: string;
10560
13683
  readonly titleId: string;
13684
+ /**
13685
+ * Whether there is a heading to render, and to name the dialog from.
13686
+ *
13687
+ * Trimmed, because a whitespace-only title renders a heading that looks empty
13688
+ * to a sighted user and names the dialog with nothing — which is the state
13689
+ * that failed `aria-dialog-name` before #214, arriving by a second route.
13690
+ */
13691
+ protected hasTitle: _angular_core.Signal<boolean>;
13692
+ /**
13693
+ * What the dialog is named by, in the order ARIA resolves: the visible heading
13694
+ * when there is one, the caller's IDREF otherwise.
13695
+ */
13696
+ protected resolvedAriaLabelledby: _angular_core.Signal<string | null>;
13697
+ /**
13698
+ * The name to render as `aria-label`, or `null` to render none — and the
13699
+ * dev-mode warning when the panel has no name from any route.
13700
+ *
13701
+ * Both halves live in `../a11y/accessible-name`, shared with the three
13702
+ * progressbars, where the reasoning for each branch is set out. `title` reaches
13703
+ * it as the `ariaLabelledby` above, so a titled panel is named, takes no
13704
+ * fallback and raises no warning.
13705
+ */
13706
+ protected resolvedAriaLabel: _angular_core.Signal<string | null>;
10561
13707
  private previouslyFocusedElement;
13708
+ /**
13709
+ * Decides when an open or a close counts as finished, so that the outputs
13710
+ * above fire exactly once per change whether or not a transition ran. A field
13711
+ * initializer rather than the constructor, because it registers an `effect`
13712
+ * and so needs an injection context.
13713
+ */
13714
+ private lifecycle;
10562
13715
  constructor();
10563
13716
  ngOnDestroy(): void;
10564
13717
  protected dismiss(): void;
@@ -10568,7 +13721,7 @@ declare class TnSidePanelComponent implements OnDestroy {
10568
13721
  private restoreFocus;
10569
13722
  private registerMdiIcons;
10570
13723
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnSidePanelComponent, never>;
10571
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnSidePanelComponent, "tn-side-panel", never, { "open": { "alias": "open"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "width": { "alias": "width"; "required": false; "isSignal": true; }; "hasBackdrop": { "alias": "hasBackdrop"; "required": false; "isSignal": true; }; "closeOnBackdropClick": { "alias": "closeOnBackdropClick"; "required": false; "isSignal": true; }; "closeOnEscape": { "alias": "closeOnEscape"; "required": false; "isSignal": true; }; "closeGuard": { "alias": "closeGuard"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "closeButtonTestId": { "alias": "closeButtonTestId"; "required": false; "isSignal": true; }; "closeButtonAriaLabel": { "alias": "closeButtonAriaLabel"; "required": false; "isSignal": true; }; }, { "open": "openChange"; "opened": "opened"; "closed": "closed"; }, ["actionContent"], ["[tnSidePanelHeaderAction]", "*", "[tnSidePanelAction]"], true, never>;
13724
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnSidePanelComponent, "tn-side-panel", never, { "open": { "alias": "open"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "width": { "alias": "width"; "required": false; "isSignal": true; }; "hasBackdrop": { "alias": "hasBackdrop"; "required": false; "isSignal": true; }; "closeOnBackdropClick": { "alias": "closeOnBackdropClick"; "required": false; "isSignal": true; }; "closeOnEscape": { "alias": "closeOnEscape"; "required": false; "isSignal": true; }; "closeGuard": { "alias": "closeGuard"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "closeButtonTestId": { "alias": "closeButtonTestId"; "required": false; "isSignal": true; }; "closeButtonAriaLabel": { "alias": "closeButtonAriaLabel"; "required": false; "isSignal": true; }; "contentAriaLabel": { "alias": "contentAriaLabel"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "ariaLabelledby"; "required": false; "isSignal": true; }; }, { "open": "openChange"; "opened": "opened"; "closed": "closed"; }, ["actionContent"], ["[tnSidePanelHeaderAction]", "*", "[tnSidePanelAction]"], true, never>;
10572
13725
  }
10573
13726
 
10574
13727
  interface SidePanelHarnessFilters extends BaseHarnessFilters {
@@ -10594,7 +13747,17 @@ declare class TnSidePanelHarness extends ComponentHarness {
10594
13747
  * Uses the data-tn-panel attribute to correlate the host with its overlay.
10595
13748
  */
10596
13749
  private getOverlay;
10597
- /** Get the panel title text. */
13750
+ /**
13751
+ * Get the panel title text, or `''` for a panel that renders no title.
13752
+ *
13753
+ * `locatorForOptional`, not `locatorFor`: since #214 a panel with no `title`
13754
+ * renders no heading element at all — an `<h2>` with nothing in it is an
13755
+ * `empty-heading` violation — and `locatorFor` throws on a selector that
13756
+ * matches nothing. That would turn `getTitle()` into an error rather than an
13757
+ * empty string, and would make `TnSidePanelHarness.with({title})` REJECT on an
13758
+ * untitled panel instead of simply not matching it, so a filter aimed at one
13759
+ * panel would fail on the presence of another.
13760
+ */
10598
13761
  getTitle(): Promise<string>;
10599
13762
  /** Whether the panel is currently open. */
10600
13763
  isOpen(): Promise<boolean>;
@@ -10618,6 +13781,17 @@ declare class TnStepComponent {
10618
13781
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnStepComponent, "tn-step", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "optional": { "alias": "optional"; "required": false; "isSignal": true; }; "completed": { "alias": "completed"; "required": false; "isSignal": true; }; "hasError": { "alias": "hasError"; "required": false; "isSignal": true; }; "errorMessage": { "alias": "errorMessage"; "required": false; "isSignal": true; }; "data": { "alias": "data"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
10619
13782
  }
10620
13783
 
13784
+ /**
13785
+ * Screen-reader text for a step whose header shows the error glyph.
13786
+ *
13787
+ * Exported so specs assert by name rather than by a copied literal, the same
13788
+ * reason `TN_SPINNER_DEFAULT_LABEL` is. It is not an override point — nothing
13789
+ * reads a token or an input in its place, so a consumer cannot change what the
13790
+ * stepper says here.
13791
+ */
13792
+ declare const TN_STEPPER_STATUS_ERROR = "Error";
13793
+ /** Screen-reader text for a step whose header shows the completed state. */
13794
+ declare const TN_STEPPER_STATUS_COMPLETED = "Completed";
10621
13795
  declare class TnStepperComponent {
10622
13796
  orientation: _angular_core.InputSignal<"auto" | "horizontal" | "vertical">;
10623
13797
  linear: _angular_core.InputSignal<boolean>;
@@ -10645,6 +13819,7 @@ declare class TnStepperComponent {
10645
13819
  isVertical: _angular_core.Signal<boolean>;
10646
13820
  readonly stepEditable: _angular_core.Signal<boolean[]>;
10647
13821
  readonly stepGated: _angular_core.Signal<boolean[]>;
13822
+ readonly stepStatusText: _angular_core.Signal<(string | null)[]>;
10648
13823
  selectStep(index: number): void;
10649
13824
  canSelectStep(index: number): boolean;
10650
13825
  next(): void;
@@ -11569,7 +14744,11 @@ declare enum TnToastPosition {
11569
14744
  Bottom = "bottom"
11570
14745
  }
11571
14746
  interface TnToastConfig {
11572
- /** Auto-dismiss duration in milliseconds. Default: 4000. Set to 0 to disable. */
14747
+ /**
14748
+ * How long the toast stays on screen, in milliseconds, counted from when it
14749
+ * appears rather than from the `open()` call. Default: 4000. Set to 0 to
14750
+ * disable auto-dismissal.
14751
+ */
11573
14752
  duration?: number;
11574
14753
  /** Visual style of the toast. Default: TnToastType.Info. */
11575
14754
  type?: TnToastType;
@@ -11591,12 +14770,46 @@ declare class TnToastComponent {
11591
14770
  position: _angular_core.WritableSignal<TnToastPosition>;
11592
14771
  visible: _angular_core.WritableSignal<boolean>;
11593
14772
  icon: _angular_core.Signal<string>;
14773
+ /**
14774
+ * The live-region role, which is also the only thing declaring how urgently the
14775
+ * toast is announced: `alert` implies `aria-live="assertive"` and `status`
14776
+ * implies `polite`.
14777
+ *
14778
+ * The template carried `role="alert"` and `aria-live="polite"` together (#190).
14779
+ * An explicit `aria-live` overrides the role's implicit one, so every toast was
14780
+ * announced politely — including `error`, the one type that needs to interrupt.
14781
+ * Deriving the role from the type and leaving `aria-live` off keeps a single
14782
+ * source: there is no second attribute left to disagree with this one.
14783
+ *
14784
+ * WHICH types get `alert` is shared with banner rather than decided here
14785
+ * (#194): #190 mapped `warning` to `status` while banner mapped it to
14786
+ * `alert`, and `../a11y/live-region.ts` is now the one place that answers it.
14787
+ */
14788
+ role: _angular_core.Signal<"alert" | "status">;
11594
14789
  onAction: () => void;
11595
14790
  onDismiss: () => void;
11596
14791
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnToastComponent, never>;
11597
14792
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnToastComponent, "tn-toast", never, {}, {}, never, never, true, never>;
11598
14793
  }
11599
14794
 
14795
+ /**
14796
+ * How long the live region is left attached and empty before the message is
14797
+ * written into it, in milliseconds.
14798
+ *
14799
+ * WHY A TIMER AND NOT THE ANIMATION FRAME THE TRANSITION RIDES
14800
+ * -----------------------------------------------------------
14801
+ * A `requestAnimationFrame` callback runs BEFORE that frame's style, layout and
14802
+ * accessibility-tree update. Attaching the region in one task and populating it
14803
+ * from the next frame's callback therefore commits both mutations in a single
14804
+ * accessibility-tree update — which is the already-populated insertion this
14805
+ * deferral exists to avoid, still there and harder to see. The region needs a
14806
+ * rendering pass of its own, which means yielding past one.
14807
+ *
14808
+ * 100ms is what `@angular/cdk`'s `LiveAnnouncer` waits before writing into its
14809
+ * own region — a dependency of this project already, and the closest thing to a
14810
+ * measured number available.
14811
+ */
14812
+ declare const TN_TOAST_ANNOUNCE_DELAY_MS = 100;
11600
14813
  declare class TnToastRef {
11601
14814
  private readonly _onAction;
11602
14815
  private readonly _afterDismissed;
@@ -11619,6 +14832,12 @@ declare class TnToastService {
11619
14832
  /**
11620
14833
  * Opens a toast notification.
11621
14834
  *
14835
+ * The toast is attached synchronously, but its message and its enter
14836
+ * transition both land `TN_TOAST_ANNOUNCE_DELAY_MS` later — the delay is what
14837
+ * makes the text a *change* to a live region a screen reader is already
14838
+ * watching. A test reading `.tn-toast__message` must let that elapse; one
14839
+ * asserting on the call rather than the DOM should use `TnToastMock`.
14840
+ *
11622
14841
  * @param message The message to display.
11623
14842
  * @param actionOrConfig Optional action button text, or config object.
11624
14843
  * @param config Optional config when action is provided as second arg.
@@ -12001,5 +15220,5 @@ declare const TN_THEME_DEFINITIONS: readonly TnThemeDefinition[];
12001
15220
  */
12002
15221
  declare const THEME_MAP: Map<TnTheme, TnThemeDefinition>;
12003
15222
 
12004
- export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, THEME_MAP, THEME_STORAGE_KEY, TN_CALENDAR_INTL, TN_CALENDAR_INTL_DEFAULTS, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_ERRORS, TN_RADIO_GROUP, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, 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, TnFormFieldComponent, TnFormFieldHarness, 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, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, allowsCurrentDirectorySelection, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, getSelectableTypes, injectTnCalendarIntl, injectTnFormFieldAria, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
12005
- export type { AutocompleteHarnessFilters, BannerHarnessFilters, ButtonHarnessFilters, ButtonToggleHarnessFilters, CalendarCell, CalendarCellFill, CalendarCellHarnessFilters, CalendarHarnessFilters, CheckboxHarnessFilters, ChipColor, ChipHarnessFilters, DateInputHarnessFilters, DateRange, DateRangeInputHarnessFilters, DialogHarnessFilters, EmptyHarnessFilters, ExpansionPanelHarnessFilters, FileInputHarnessFilters, FilePickerCallbacks, FilePickerCreateAction, FilePickerCreateActionEvent, FilePickerError, FilePickerHarnessFilters, FilePickerMode, FileSystemItem, FileSystemItemType, FlatTreeControlOptions, FormFieldHarnessFilters, FormSectionHarnessFilters, IconButtonHarnessFilters, IconHarnessFilters, IconLibrary, IconLibraryType, IconResult, IconSize, IconSource, IconTestingMockOverrides, InputHarnessFilters, KeyCombination, LabelMarkupSegment, LabelMarkupSegmentType, LabelType, LucideIconOptions, MenuHarnessFilters, MockIconRegistry, MockSpriteLoader, NestedTreeControlOptions, PathSegment, PlatformType, ProgressBarMode, RadioGroupHarnessFilters, RadioHarnessFilters, ResolvedIcon, SelectHarnessFilters, ShortcutHandler, SidePanelHarnessFilters, SizeStandard, SlideToggleColor, SlideToggleHarnessFilters, SpinnerMode, SpriteConfig, StepperHarnessFilters, SubscriptSizing, TabChangeEvent, TabHarnessFilters, TabPanelHarnessFilters, TabsHarnessFilters, TnAutocompleteOption, TnBannerType, TnButtonToggleType, TnCalendarIntl, TnCalendarIntlInput, TnCalendarView, TnCardAction, TnCardControl, TnCardFooterLink, TnCardHeaderStatus, TnChipInputHarnessFilters, TnChipInputOption, TnConfirmDialogData, TnDialogDefaults, TnDialogOpenTarget, TnDrawerMode, TnDrawerPosition, TnEmptySize, TnFlatTreeNode, TnFormFieldAriaBindings, TnFormFieldContext, TnFormFieldErrorMessage, TnFormFieldErrorMessages, TnFormFieldErrorResolver, TnMenuItem, TnOptionTestIdSource, TnRadioGroupApi, TnRadioOption, TnSelectOption, TnSelectOptionGroup, TnSelectionChange, TnSortEvent, TnTableDataProvider, TnTableDataSource, TnTableHarnessFilters, TnTableMobileLayout, TnTablePagerHarnessFilters, TnTablePagerLabels, TnTablePagination, TnTestAttrName, TnTestIdValue, TnThemeDefinition, TnToastCall, TnToastConfig, TnTreeExpansion, TnTreeHarnessFilters, TnTreeNodeHarnessFilters, TnTreeVirtualNodeData, TnTreeVirtualScrollViewHarnessFilters, TooltipPosition, YearCell };
15223
+ 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 };
15224
+ export type { AutocompleteHarnessFilters, BannerActionHarnessFilters, BannerHarnessFilters, ButtonHarnessFilters, ButtonToggleHarnessFilters, CalendarCell, CalendarCellFill, CalendarCellHarnessFilters, CalendarHarnessFilters, CheckboxHarnessFilters, ChipColor, ChipHarnessFilters, DateInputHarnessFilters, DateRange, DateRangeInputHarnessFilters, DialogHarnessFilters, EmptyHarnessFilters, ExpansionPanelHarnessFilters, FileInputHarnessFilters, FilePickerCallbacks, FilePickerCreateAction, FilePickerCreateActionEvent, FilePickerError, FilePickerHarnessFilters, FilePickerMode, FileSystemItem, FileSystemItemType, FlatTreeControlOptions, FormFieldHarnessFilters, FormSectionHarnessFilters, IconButtonHarnessFilters, IconHarnessFilters, IconLibrary, IconLibraryType, IconResult, IconSize, IconSource, IconTestingMockOverrides, InputHarnessFilters, KeyCombination, LabelMarkupSegment, LabelMarkupSegmentType, LabelType, LucideIconOptions, MenuHarnessFilters, MockIconRegistry, MockSpriteLoader, NestedTreeControlOptions, ParsedSize, PathSegment, PlatformType, ProgressBarMode, RadioGroupHarnessFilters, RadioHarnessFilters, ResolveErrorMessageOptions, ResolvedIcon, SelectHarnessFilters, ShortcutHandler, SidePanelHarnessFilters, SizeStandard, SlideToggleColor, SlideToggleHarnessFilters, SpinnerMode, SpriteConfig, StepperHarnessFilters, SubscriptSizing, TabChangeEvent, TabHarnessFilters, TabPanelHarnessFilters, TabsHarnessFilters, TnAutocompleteLabels, TnAutocompleteOption, TnBannerType, TnButtonToggleType, TnCalendarIntl, TnCalendarIntlInput, TnCalendarView, TnCardAction, TnCardControl, TnCardFooterLink, TnCardHeaderStatus, TnChipInputHarnessFilters, TnChipInputOption, TnConfirmDialogData, TnDialogChromeLabels, TnDialogDefaults, TnDialogOpenTarget, TnDrawerMode, TnDrawerPosition, TnEmptySize, TnFlatTreeNode, TnFormErrorsHarnessFilters, TnFormFieldAriaBindings, TnFormFieldContext, TnFormFieldErrorMessage, TnFormFieldErrorMessages, TnFormFieldErrorResolver, TnFormListContext, TnFormListHarnessFilters, TnFormListItemHarnessFilters, TnMenuItem, TnOptionTestIdSource, TnRadioGroupApi, TnRadioOption, TnSelectLabels, TnSelectOption, TnSelectOptionGroup, TnSelectionChange, TnSortEvent, TnTableDataProvider, TnTableDataSource, TnTableHarnessFilters, TnTableLabels, TnTableMobileLayout, TnTablePagerHarnessFilters, TnTablePagerLabels, TnTablePagination, TnTestAttrName, TnTestIdValue, TnThemeDefinition, TnToastCall, TnToastConfig, TnTreeExpansion, TnTreeHarnessFilters, TnTreeNodeHarnessFilters, TnTreeVirtualNodeData, TnTreeVirtualScrollViewHarnessFilters, TooltipHarnessFilters, TooltipPosition, YearCell };