@truenas/ui-components 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  *
@@ -1814,6 +2611,20 @@ declare function formatSize(bytes: number | string | null | undefined, standard?
1814
2611
  */
1815
2612
  declare function parseSize(raw: string | number | null | undefined, defaultUnit?: string, standard?: SizeStandard): number | null;
1816
2613
 
2614
+ declare enum InputType {
2615
+ Email = "email",
2616
+ Number = "number",
2617
+ Password = "password",
2618
+ PlainText = "text",
2619
+ /**
2620
+ * Data-size field: the form model holds a raw byte count (a number), while the
2621
+ * field displays/accepts a human-readable string (e.g. `2 GiB`, `500M`, `2 TB`).
2622
+ * See `tn-input`'s `sizeStandard` / `sizeDefaultUnit` inputs to tune formatting
2623
+ * and bare-number parsing.
2624
+ */
2625
+ Size = "size"
2626
+ }
2627
+
1817
2628
  declare class TnInputComponent implements AfterViewInit, OnDestroy, ControlValueAccessor {
1818
2629
  inputEl: _angular_core.Signal<ElementRef<HTMLInputElement | HTMLTextAreaElement>>;
1819
2630
  inputType: _angular_core.InputSignal<InputType>;
@@ -2598,6 +3409,16 @@ declare class TnChipComponent implements AfterViewInit, OnDestroy {
2598
3409
  classes: _angular_core.Signal<string[]>;
2599
3410
  handleClick(event: MouseEvent): void;
2600
3411
  handleClose(event: MouseEvent): void;
3412
+ /**
3413
+ * Handles the chip's Delete/Backspace dismiss shortcut. Bound to both the
3414
+ * body and the close button so the shortcut works wherever focus sits inside
3415
+ * the chip; the wrapper between them carries no role and is not focusable,
3416
+ * so it is not a legitimate place to hang a key handler.
3417
+ *
3418
+ * Enter and Space are deliberately absent: the body is a native `<button>`,
3419
+ * which already turns both into a `click`. Handling them here as well would
3420
+ * emit `onClick` twice per keypress.
3421
+ */
2601
3422
  handleKeyDown(event: KeyboardEvent): void;
2602
3423
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnChipComponent, never>;
2603
3424
  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 +3448,7 @@ declare class TnChipHarness extends ComponentHarness {
2627
3448
  */
2628
3449
  static hostSelector: string;
2629
3450
  private _chip;
3451
+ private _body;
2630
3452
  private _label;
2631
3453
  private _icon;
2632
3454
  private _closeButton;
@@ -2863,6 +3685,20 @@ declare class TnChipInputComponent<T = string> implements ControlValueAccessor,
2863
3685
  testId: _angular_core.InputSignal<TnTestIdValue>;
2864
3686
  /** Test-id base, falling back to the bound control name when `testId` is unset. */
2865
3687
  protected resolvedTestId: _angular_core.Signal<TnTestIdValue>;
3688
+ /**
3689
+ * Optional extractor for the per-option test-id discriminator, applied to both
3690
+ * a suggestion row and the chip it becomes. Defaults to the option's `label`,
3691
+ * the text actually on screen — provide this to key off a locale-independent
3692
+ * field instead, or where two options share a display name and the derived ids
3693
+ * would otherwise collide. Free-text chips have no option to extract from and
3694
+ * stay named by their own value.
3695
+ *
3696
+ * @example
3697
+ * ```html
3698
+ * <tn-chip-input testId="users" [optionTestIdKey]="(o) => o.value.id" ... />
3699
+ * ```
3700
+ */
3701
+ optionTestIdKey: _angular_core.InputSignal<((option: TnChipInputOption<T>) => string | number | null | undefined) | undefined>;
2866
3702
  /** Emits the committed value whenever a chip is added. */
2867
3703
  chipAdded: _angular_core.OutputEmitterRef<T>;
2868
3704
  /** Emits the removed value whenever a chip is removed. */
@@ -2924,19 +3760,27 @@ declare class TnChipInputComponent<T = string> implements ControlValueAccessor,
2924
3760
  /**
2925
3761
  * Scopes a per-chip test id beneath the component's base.
2926
3762
  *
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}.
3763
+ * A chip backed by an option goes through the same {@link optionTestId}
3764
+ * derivation as the suggestion row that created it, so the two carry the same
3765
+ * discriminator by construction rather than one naming the label and the other
3766
+ * the value — including whatever fallback that shared rule settles on, and any
3767
+ * `optionTestIdKey` override.
3768
+ *
3769
+ * A value with no matching option is named by itself when it is a primitive:
3770
+ * either a free-text chip, which is its own text, or an option-backed value
3771
+ * whose options have not arrived yet — an async `[options]` load moves such a
3772
+ * chip's id from the value to the resolved label once it does. An object value
3773
+ * with no match cannot stand in for itself, because `String(value)` on an
3774
+ * object is `[object Object]` and would stamp an identical id on every chip.
3775
+ * Duplicates are worse than absence for automation, so that chip stays
3776
+ * attribute-free rather than colliding. A primitive that normalizes away is
3777
+ * dropped for the same reason — see {@link discriminatedTestId}.
2934
3778
  */
2935
3779
  protected chipTestId(value: T): TnTestIdValue;
2936
3780
  /**
2937
3781
  * Scopes a per-suggestion test id beneath the component's base, via the shared
2938
3782
  * dropdown-option derivation. Unlike `tn-select` / `tn-autocomplete`, which
2939
- * emit an unscoped `option-<value>` when they have no base, an unidentified
3783
+ * emit an unscoped `option-<label>` when they have no base, an unidentified
2940
3784
  * chip-input stays attribute-free — its rows carry no page-unique id, so
2941
3785
  * emitting one would invite collisions between inputs.
2942
3786
  */
@@ -2958,6 +3802,24 @@ declare class TnChipInputComponent<T = string> implements ControlValueAccessor,
2958
3802
  private commitText;
2959
3803
  /** Commits a resolved value, honouring duplicate and cap rules. */
2960
3804
  private commitValue;
3805
+ /**
3806
+ * The option a committed value came from, or `undefined` for a free-text chip.
3807
+ *
3808
+ * Both the chip's label and its test id need this lookup, and both are called
3809
+ * from the template — once per chip per change-detection cycle — so a linear
3810
+ * scan of the options would be quadratic in (chips × options) on every cycle.
3811
+ * With the default identity comparator, {@link optionIndex} answers in constant
3812
+ * time; a custom `compareWith` can't be indexed (only it knows what equality
3813
+ * means for the value), so that path keeps the scan.
3814
+ */
3815
+ private optionFor;
3816
+ /**
3817
+ * Value → option, rebuilt only when the option list changes. First entry wins,
3818
+ * matching the `find` it replaces where a value is repeated across options.
3819
+ * Keys compare by `Map` identity, which agrees with the `===` this stands in
3820
+ * for on every value a form control can hold.
3821
+ */
3822
+ private optionIndex;
2961
3823
  private valuesIncludes;
2962
3824
  private valueMatches;
2963
3825
  private clearInput;
@@ -2974,7 +3836,7 @@ declare class TnChipInputComponent<T = string> implements ControlValueAccessor,
2974
3836
  private attachOverlay;
2975
3837
  private detachOverlay;
2976
3838
  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>;
3839
+ 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
3840
  }
2979
3841
 
2980
3842
  /**
@@ -3266,6 +4128,28 @@ declare class TnCardComponent {
3266
4128
  */
3267
4129
  titleTooltipAriaLabel: _angular_core.InputSignal<string | undefined>;
3268
4130
  protected readonly resolvedTitleTooltipAriaLabel: _angular_core.Signal<string>;
4131
+ /**
4132
+ * Whether a tooltip message holding a link may be pinned open by clicking its host (see
4133
+ * `tnTooltipSticky`). On by default, like the directive, and it reaches the title help button
4134
+ * and the footer actions.
4135
+ *
4136
+ * The help button has no other use for the click, so there pinning is all the click does. A
4137
+ * footer action does have one: pinning is additive there, exactly as `tnTooltipSticky` describes
4138
+ * it — one click on a `TnCardAction` whose `tooltip` holds a link both runs `handler()` and pins
4139
+ * the panel. That is fine for a handler that leaves the card in place, and worth a second look
4140
+ * for one that navigates away or closes it, since the panel it pinned goes with the card. Set
4141
+ * this to false for such a card.
4142
+ *
4143
+ * The kebab-menu trigger is deliberately out of scope. Its click already opens the menu, so a
4144
+ * pinnable tooltip there would put a panel over the menu from that same click and take the hint
4145
+ * off hover to do it; `headerMenuTriggerTooltip` therefore always hovers, and this flag does not
4146
+ * reach it.
4147
+ *
4148
+ * It does not make plain tooltips pinnable — card help and action hints are nearly always plain
4149
+ * text, and those keep hovering. Set it to false only to force a message that does hold a link
4150
+ * back to hover behaviour, accepting that the link is then unreachable.
4151
+ */
4152
+ tooltipSticky: _angular_core.InputSignal<boolean>;
3269
4153
  elevation: _angular_core.InputSignal<"none" | "low" | "medium" | "high">;
3270
4154
  padding: _angular_core.InputSignal<"small" | "large" | "medium">;
3271
4155
  padContent: _angular_core.InputSignal<boolean>;
@@ -3301,6 +4185,10 @@ declare class TnCardComponent {
3301
4185
  /**
3302
4186
  * Hover tooltip for the kebab-menu trigger. Defaults to `headerMenuTriggerAriaLabel`, so setting
3303
4187
  * a single translated string covers both the accessible name and the visible hint.
4188
+ *
4189
+ * Hover is all it ever is: unlike the card's other tooltips this one is never pinnable, because
4190
+ * the trigger's click belongs to the menu — see `tooltipSticky`. A message holding a link works
4191
+ * here, but the link stays out of reach.
3304
4192
  */
3305
4193
  headerMenuTriggerTooltip: _angular_core.InputSignal<string | undefined>;
3306
4194
  protected readonly resolvedHeaderMenuAriaLabel: _angular_core.Signal<string>;
@@ -3329,7 +4217,7 @@ declare class TnCardComponent {
3329
4217
  onHeaderMenuItemClick(_item: TnMenuItem): void;
3330
4218
  getStatusClass(type?: string): string;
3331
4219
  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>;
4220
+ 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
4221
  }
3334
4222
 
3335
4223
  /**
@@ -4265,7 +5153,6 @@ declare class TnSlideToggleComponent implements AfterViewInit, OnDestroy, Contro
4265
5153
  registerOnTouched(fn: () => void): void;
4266
5154
  setDisabledState(isDisabled: boolean): void;
4267
5155
  onToggleChange(event: Event): void;
4268
- onLabelClick(): void;
4269
5156
  classes: _angular_core.Signal<string[]>;
4270
5157
  effectiveAriaLabel: _angular_core.Signal<string | undefined>;
4271
5158
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnSlideToggleComponent, never>;
@@ -4361,6 +5248,20 @@ declare class TnTabComponent implements AfterContentInit {
4361
5248
  selected: _angular_core.OutputEmitterRef<void>;
4362
5249
  iconContent: _angular_core.Signal<TemplateRef<unknown> | undefined>;
4363
5250
  index: _angular_core.WritableSignal<number>;
5251
+ /**
5252
+ * Id namespace, set by the parent `tn-tabs` so that both ends of the tab↔panel wiring
5253
+ * agree. The default is unique per instance rather than a constant, so a `tn-tab`
5254
+ * rendered outside a `tn-tabs` still renders an `id` that collides with nothing.
5255
+ */
5256
+ groupId: _angular_core.WritableSignal<string>;
5257
+ /**
5258
+ * Whether the parent has a panel at this tab's index, which is what decides whether
5259
+ * `aria-controls` is rendered at all. `tn-tabs` walks its tabs and its panels
5260
+ * independently, so a group given more tabs than panels — or a `tn-tab` used outside a
5261
+ * `tn-tabs`, which is why this starts false — would otherwise point `aria-controls` at
5262
+ * an id no element in the document carries.
5263
+ */
5264
+ hasPanel: _angular_core.WritableSignal<boolean>;
4364
5265
  isActive: _angular_core.WritableSignal<boolean>;
4365
5266
  tabsComponent?: {
4366
5267
  onKeydown: (event: KeyboardEvent, index: number) => void;
@@ -4373,20 +5274,92 @@ declare class TnTabComponent implements AfterContentInit {
4373
5274
  onKeydown(event: KeyboardEvent): void;
4374
5275
  classes: _angular_core.Signal<string>;
4375
5276
  tabIndex: _angular_core.Signal<-1 | 0>;
5277
+ /** This tab's own id, which its panel points back at with `aria-labelledby`. */
5278
+ tabId: _angular_core.Signal<string>;
5279
+ /** The id of the panel this tab controls, for `aria-controls`. */
5280
+ panelId: _angular_core.Signal<string>;
4376
5281
  hasIcon: _angular_core.Signal<boolean>;
4377
5282
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnTabComponent, never>;
4378
5283
  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
5284
  }
4380
5285
 
5286
+ /**
5287
+ * The name the scrolling content region takes when it becomes focusable and the
5288
+ * panel has no `label` to name it from (#270).
5289
+ *
5290
+ * A focusable element with no accessible name is announced as a bare "group".
5291
+ * A panel rendered outside a `tn-tabs`, or one whose caller left `label` empty,
5292
+ * has no better name available — so this is the last resort rather than the
5293
+ * usual case, and `contentLabel` prefers the label every time there is one.
5294
+ *
5295
+ * Exported so specs assert against it by name rather than by a copied literal.
5296
+ */
5297
+ declare const TN_TAB_PANEL_CONTENT_LABEL = "Tab panel content";
4381
5298
  declare class TnTabPanelComponent {
4382
5299
  label: _angular_core.InputSignal<string>;
4383
5300
  lazyLoad: _angular_core.InputSignal<boolean>;
4384
5301
  testId: _angular_core.InputSignal<TnTestIdValue>;
4385
5302
  content: _angular_core.Signal<TemplateRef<unknown>>;
4386
- index: _angular_core.WritableSignal<number>;
4387
- isActive: _angular_core.WritableSignal<boolean>;
5303
+ /**
5304
+ * The scrolling content region, which is `.tn-tab-panel__content` — the
5305
+ * element the stylesheet gives `overflow: auto`, not the `role="tabpanel"`
5306
+ * wrapper around it. Optional because it lives inside `@if (shouldRender())`,
5307
+ * so a lazy panel that has never been active does not render it.
5308
+ *
5309
+ * The template ref is `#contentRegion` and not `#content`, which is taken:
5310
+ * the `content` query above asks for a `TemplateRef` under that name, and
5311
+ * naming a real element `#content` would hand it an `ElementRef` instead.
5312
+ */
5313
+ private contentRef;
5314
+ /**
5315
+ * Whether the content region carries the tab stop, its role and its name —
5316
+ * and, conversely, whether the `role="tabpanel"` wrapper gives its own up.
5317
+ *
5318
+ * `.tn-tab-panel__content` is what scrolls, so it is what a keyboard user
5319
+ * has to be able to stand on to read past the fold (#270). The measurement,
5320
+ * the observers behind it and the rule that holds the answer true while the
5321
+ * region has focus are `tnScrollableRegion`'s; the template says why the two
5322
+ * elements trade one tab stop rather than carrying two.
5323
+ */
5324
+ protected contentKeyboardReachable: _angular_core.Signal<boolean>;
5325
+ index: _angular_core.WritableSignal<number>;
5326
+ /**
5327
+ * Id namespace, set by the parent `tn-tabs` so that both ends of the tab↔panel wiring
5328
+ * agree. Unique per instance by default, for the same reason as on `tn-tab`: a panel
5329
+ * rendered outside a `tn-tabs` still renders an `id`, and one colliding with another
5330
+ * group's panel would be worse than one nothing points at.
5331
+ */
5332
+ groupId: _angular_core.WritableSignal<string>;
5333
+ /**
5334
+ * Whether the parent has a tab at this panel's index, which is what decides whether
5335
+ * `aria-labelledby` is rendered. The mirror of `hasPanel` on `tn-tab`, and for the same
5336
+ * reason: more panels than tabs, or a panel outside a `tn-tabs`, would otherwise leave
5337
+ * this pointing at an id nothing carries — which is what `aria-labelledby="tab-0"` did
5338
+ * unconditionally before #232.
5339
+ */
5340
+ hasTab: _angular_core.WritableSignal<boolean>;
5341
+ isActive: _angular_core.WritableSignal<boolean>;
4388
5342
  hasBeenActive: _angular_core.WritableSignal<boolean>;
4389
5343
  elementRef: ElementRef<any>;
5344
+ /** This panel's own id, which its tab points at with `aria-controls`. */
5345
+ panelId: _angular_core.Signal<string>;
5346
+ /** The id of the tab that labels this panel, for `aria-labelledby`. */
5347
+ tabId: _angular_core.Signal<string>;
5348
+ /**
5349
+ * What the scrolling content region is named while it carries the tab stop.
5350
+ *
5351
+ * The panel's own `label` where there is one, because that is what its tab
5352
+ * says and a listener arriving from the tab hears the same words for the
5353
+ * region it opened. `aria-labelledby` pointing at the tab would be the other
5354
+ * way to say it and is not used here: the tab lives in a sibling component,
5355
+ * so the IDREF resolves only inside a `tn-tabs`, and a panel rendered on its
5356
+ * own would be left unnamed by exactly the route that was supposed to name
5357
+ * it — the `--tn-error-text` hazard in another shape.
5358
+ *
5359
+ * Trimmed, because a whitespace-only label names the region with nothing,
5360
+ * which is the state `TN_TAB_PANEL_CONTENT_LABEL` exists to prevent.
5361
+ */
5362
+ protected contentLabel: _angular_core.Signal<string>;
4390
5363
  classes: _angular_core.Signal<string>;
4391
5364
  shouldRender: _angular_core.Signal<boolean>;
4392
5365
  onActivate(): void;
@@ -4407,10 +5380,17 @@ declare class TnTabsComponent implements AfterContentInit, AfterViewInit, OnDest
4407
5380
  orientation: _angular_core.InputSignal<"horizontal" | "vertical">;
4408
5381
  highlightPosition: _angular_core.InputSignal<"bottom" | "top" | "left" | "right">;
4409
5382
  /**
4410
- * Test-id applied to the tablist root element. Rendered under whichever attribute name
4411
- * is configured via `TN_TEST_ATTR` (default `data-testid`).
5383
+ * Test-id applied to the root element — the wrapper around the tablist and the panels,
5384
+ * not the tablist itself, which is the header inside it. Rendered under whichever
5385
+ * attribute name is configured via `TN_TEST_ATTR` (default `data-testid`).
4412
5386
  */
4413
5387
  testId: _angular_core.InputSignal<TnTestIdValue>;
5388
+ /**
5389
+ * Namespace for the ids this group hands to its tabs and panels, so that two `tn-tabs`
5390
+ * on one page cannot both mint `tab-0` and cross-wire each other's `aria-controls` and
5391
+ * `aria-labelledby`. See `tab-ids.ts`.
5392
+ */
5393
+ private readonly groupId;
4414
5394
  selectedIndexChange: _angular_core.OutputEmitterRef<number>;
4415
5395
  tabChange: _angular_core.OutputEmitterRef<TabChangeEvent>;
4416
5396
  private internalSelectedIndex;
@@ -5045,6 +6025,18 @@ declare class TnKeyboardShortcutComponent {
5045
6025
  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
6026
  }
5047
6027
 
6028
+ /** What a host binds its template and its dismiss handler to. @internal */
6029
+ interface DismissibleErrorState {
6030
+ /** The host's own list when it has one, otherwise the app-wide default. */
6031
+ resolvedDismissibleErrors: Signal<readonly string[]>;
6032
+ /** Whether the shown message renders a close button beside it. */
6033
+ showDismiss: Signal<boolean>;
6034
+ /** Accessible name for that button. */
6035
+ resolvedDismissAriaLabel: Signal<string>;
6036
+ /** Hover hint for that button. */
6037
+ resolvedDismissTooltip: Signal<string>;
6038
+ }
6039
+
5048
6040
  /**
5049
6041
  * Contract between `tn-form-field` and its projected form control, published
5050
6042
  * over DI so the control can wire its ARIA attributes to the field's chrome
@@ -5143,6 +6135,21 @@ declare class TnFormFieldComponent implements AfterContentInit, TnFormFieldConte
5143
6135
  tooltip: _angular_core.InputSignal<string>;
5144
6136
  /** Placement of the tooltip relative to its help icon. */
5145
6137
  tooltipPosition: _angular_core.InputSignal<TooltipPosition>;
6138
+ /**
6139
+ * Whether a tooltip message holding a link may be pinned open by clicking the help button (see
6140
+ * `tnTooltipSticky`). On by default, like the directive.
6141
+ *
6142
+ * It does not make plain tooltips pinnable — field help is nearly always plain text, and that
6143
+ * keeps hovering. Set it to false only to force a message that does hold a link back to hover
6144
+ * behaviour, accepting that the link is then unreachable.
6145
+ */
6146
+ tooltipSticky: _angular_core.InputSignal<boolean>;
6147
+ /**
6148
+ * Accessible name for the help button, which is icon-only and so has nothing else to be named
6149
+ * by. The message is what the button is for, but it may hold markup — a link is the whole point
6150
+ * of `tooltipSticky` — and `aria-label` takes plain text, so the tags come off first.
6151
+ */
6152
+ protected readonly tooltipAriaLabel: _angular_core.Signal<string>;
5146
6153
  /**
5147
6154
  * Per-field overrides for validation messages, keyed by error key. Values may
5148
6155
  * be a string or a function that receives the error's detail value. Takes
@@ -5150,7 +6157,56 @@ declare class TnFormFieldComponent implements AfterContentInit, TnFormFieldConte
5150
6157
  * built-in defaults.
5151
6158
  */
5152
6159
  errorMessages: _angular_core.InputSignal<Partial<Record<string, _truenas_ui_components.TnFormFieldErrorMessage>>>;
6160
+ /**
6161
+ * Error keys whose message renders with a dismiss button beside it — in
6162
+ * practice a failure the user cannot fix by editing the value, so the message
6163
+ * would otherwise stick until the control changes: a server-side rejection an
6164
+ * error handler attached to the control, or an async validator that judged the
6165
+ * value the user already picked.
6166
+ *
6167
+ * Only the error actually being shown gets the button. A control carrying both
6168
+ * a dismissible key and `required` shows the `required` message, undismissable,
6169
+ * because that is the message on screen.
6170
+ *
6171
+ * Dismissing deletes these keys from the control's errors — listing them here
6172
+ * is what grants that, since a message the user can close but that does not go
6173
+ * away would be worse than no button. Every listed key the control carries goes
6174
+ * at once, not just the one behind the message: an app that spreads one failure
6175
+ * across sibling keys (a flag, its message, a legacy alias) would otherwise see
6176
+ * the message reappear from a sibling. Unlisted errors are left alone, and
6177
+ * {@link dismiss} reports which message went.
6178
+ *
6179
+ * Left unset, the app-wide {@link TN_FORM_FIELD_DISMISSIBLE_ERRORS} default
6180
+ * applies; pass `[]` to opt this field out of it.
6181
+ */
6182
+ dismissibleErrors: _angular_core.InputSignal<readonly string[] | undefined>;
6183
+ /**
6184
+ * Accessible name for the dismiss button, which is icon-only and so has
6185
+ * nothing else to be named by. The library cannot translate its own
6186
+ * `'Dismiss this error'` default, so a consumer with an i18n layer passes an
6187
+ * already-translated string here.
6188
+ */
6189
+ dismissAriaLabel: _angular_core.InputSignal<string | undefined>;
6190
+ /**
6191
+ * Hover tooltip for the dismiss button. Defaults to the resolved
6192
+ * `dismissAriaLabel`, so one translated string covers both the accessible name
6193
+ * and the visible hint.
6194
+ */
6195
+ dismissTooltip: _angular_core.InputSignal<string | undefined>;
6196
+ /**
6197
+ * Emits the error key the user dismissed, after it has been removed — the key
6198
+ * of the message that was on screen, so a consumer listing several dismissible
6199
+ * keys knows which one went.
6200
+ */
6201
+ dismiss: _angular_core.OutputEmitterRef<string>;
5153
6202
  control: _angular_core.Signal<NgControl | undefined>;
6203
+ private host;
6204
+ /**
6205
+ * `read: ElementRef` because the ref sits on `tn-icon-button`, and a component
6206
+ * ref resolves to the instance by default — the element is what the focus
6207
+ * check needs.
6208
+ */
6209
+ private dismissButton;
5154
6210
  private destroyRef;
5155
6211
  /**
5156
6212
  * App-wide message resolver, captured once at construction. Unlike the
@@ -5187,6 +6243,28 @@ declare class TnFormFieldComponent implements AfterContentInit, TnFormFieldConte
5187
6243
  protected showInlineExtras: _angular_core.Signal<boolean>;
5188
6244
  protected hasError: _angular_core.Signal<boolean>;
5189
6245
  protected errorMessage: _angular_core.Signal<string>;
6246
+ /**
6247
+ * The error key the shown message came from. Same pick `resolveErrorMessage`
6248
+ * makes, so the dismiss button can never belong to an error other than the one
6249
+ * being read.
6250
+ */
6251
+ protected activeError: _angular_core.Signal<string | null>;
6252
+ /**
6253
+ * Which list applies, whether the button shows, and what it is called — shared
6254
+ * with `tn-form-errors` so a field message and a group message beside it can
6255
+ * never decide dismissibility by different rules.
6256
+ */
6257
+ protected dismissible: DismissibleErrorState;
6258
+ /**
6259
+ * Drops the dismissed error, then puts focus back on the control rather than
6260
+ * letting it fall to `<body>` with the button — dismissing a server-side error
6261
+ * means "let me try again", and the control is where trying again happens.
6262
+ *
6263
+ * Focus only moves if it was on the button to begin with: a dismiss triggered
6264
+ * from anywhere else (a Safari mouse click, which leaves the button unfocused)
6265
+ * has no focus to lose and should not steal any.
6266
+ */
6267
+ protected dismissError(): void;
5190
6268
  ngAfterContentInit(): void;
5191
6269
  private syncControlState;
5192
6270
  /**
@@ -5194,14 +6272,12 @@ declare class TnFormFieldComponent implements AfterContentInit, TnFormFieldConte
5194
6272
  * `errorMessages` input (and the injected resolver), so it is reactive: the
5195
6273
  * displayed message updates when either the control errors or the overrides
5196
6274
  * change — e.g. a runtime locale switch.
6275
+ *
6276
+ * The ladder itself lives in `./form-field.errors`, shared with
6277
+ * `tn-form-errors` so a group-level message reads exactly like the
6278
+ * field-level one it sits beside.
5197
6279
  */
5198
6280
  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
6281
  showError: _angular_core.Signal<boolean>;
5206
6282
  showHint: _angular_core.Signal<boolean>;
5207
6283
  protected showSubscript: _angular_core.Signal<boolean>;
@@ -5218,7 +6294,7 @@ declare class TnFormFieldComponent implements AfterContentInit, TnFormFieldConte
5218
6294
  /** Forced or validator-inferred required state, for `aria-required`. */
5219
6295
  requiredState: _angular_core.Signal<boolean>;
5220
6296
  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>;
6297
+ 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
6298
  }
5223
6299
 
5224
6300
  /**
@@ -5275,6 +6351,59 @@ type TnFormFieldErrorResolver = (errorKey: string, errorValue: unknown, control:
5275
6351
  * ```
5276
6352
  */
5277
6353
  declare const TN_FORM_FIELD_ERRORS: InjectionToken<TnFormFieldErrorResolver>;
6354
+ /**
6355
+ * App-wide default for which error keys carry a dismiss button, for apps whose
6356
+ * server-side failures always land under the same keys. A field's
6357
+ * `dismissibleErrors` input overrides it — including with `[]`, to opt one field
6358
+ * out of the default.
6359
+ *
6360
+ * Listing a key here grants permission to delete it: dismissing removes it from
6361
+ * the control's errors, since a message the user can close but that will not go
6362
+ * away is worse than no button at all.
6363
+ *
6364
+ * @example
6365
+ * ```ts
6366
+ * providers: [
6367
+ * {
6368
+ * provide: TN_FORM_FIELD_DISMISSIBLE_ERRORS,
6369
+ * useValue: ['manualValidateError', 'manualValidateErrorMsg'],
6370
+ * },
6371
+ * ];
6372
+ * ```
6373
+ */
6374
+ declare const TN_FORM_FIELD_DISMISSIBLE_ERRORS: InjectionToken<readonly string[]>;
6375
+ /** What {@link resolveErrorMessage} needs to answer for one control. */
6376
+ interface ResolveErrorMessageOptions {
6377
+ /** The control's current errors — the caller has already checked it has some. */
6378
+ errors: ValidationErrors;
6379
+ /** Per-instance overrides, keyed by error key. Consulted first. */
6380
+ errorMessages?: TnFormFieldErrorMessages;
6381
+ /** The app-wide resolver from {@link TN_FORM_FIELD_ERRORS}, if one is provided. */
6382
+ resolver?: TnFormFieldErrorResolver | null;
6383
+ /** The failing control, passed through to the resolver. */
6384
+ control?: AbstractControl | null;
6385
+ /**
6386
+ * Who is asking, used only to attribute the dev-mode `console.error` this
6387
+ * logs when a caller-supplied message provider throws. Optional: the library's
6388
+ * own components pass their selector so the log names the element on screen,
6389
+ * and an outside caller has nothing useful to put here.
6390
+ */
6391
+ selector?: string;
6392
+ }
6393
+ /**
6394
+ * Resolves a user-facing message for a control's active error, in the order
6395
+ * `tn-form-field` has always used:
6396
+ *
6397
+ * 1. a per-instance `errorMessages` override (string or factory),
6398
+ * 2. the app-wide {@link TN_FORM_FIELD_ERRORS} resolver,
6399
+ * 3. {@link defaultErrorMessage} for Angular's standard validators,
6400
+ * 4. the error value itself, when a custom validator returned its own string,
6401
+ * 5. the raw error key.
6402
+ *
6403
+ * Shared with `tn-form-errors` so a group-level message reads exactly like the
6404
+ * field-level one it sits beside.
6405
+ */
6406
+ declare function resolveErrorMessage(options: ResolveErrorMessageOptions): string;
5278
6407
 
5279
6408
  /**
5280
6409
  * Harness for interacting with `tn-form-field` in tests.
@@ -5308,6 +6437,7 @@ declare class TnFormFieldHarness extends ComponentHarness {
5308
6437
  private _error;
5309
6438
  private _hint;
5310
6439
  private _tooltip;
6440
+ private _dismiss;
5311
6441
  /**
5312
6442
  * Gets a `HarnessPredicate` that can be used to search for a form field
5313
6443
  * with specific attributes.
@@ -5364,6 +6494,37 @@ declare class TnFormFieldHarness extends ComponentHarness {
5364
6494
  * ```
5365
6495
  */
5366
6496
  hasError(): Promise<boolean>;
6497
+ /**
6498
+ * Checks whether the shown error carries a dismiss button — true only when the
6499
+ * active error key is one of the field's `dismissibleErrors`.
6500
+ *
6501
+ * @returns Promise resolving to true if the dismiss button is present.
6502
+ *
6503
+ * @example
6504
+ * ```typescript
6505
+ * const field = await loader.getHarness(TnFormFieldHarness.with({ label: 'Image' }));
6506
+ * expect(await field.isErrorDismissible()).toBe(true);
6507
+ * ```
6508
+ */
6509
+ isErrorDismissible(): Promise<boolean>;
6510
+ /**
6511
+ * Clicks the dismiss button, as a user clearing a server-side error would.
6512
+ *
6513
+ * The field clears the error itself — every key in its `dismissibleErrors`
6514
+ * that the control carries — puts focus back on the control, and then emits
6515
+ * `dismiss` with the key that was on screen, so the message is gone by the
6516
+ * time this resolves.
6517
+ *
6518
+ * @throws If the shown error is not dismissible.
6519
+ *
6520
+ * @example
6521
+ * ```typescript
6522
+ * const field = await loader.getHarness(TnFormFieldHarness.with({ label: 'Image' }));
6523
+ * await field.dismissError();
6524
+ * expect(await field.hasError()).toBe(false);
6525
+ * ```
6526
+ */
6527
+ dismissError(): Promise<void>;
5367
6528
  /**
5368
6529
  * Gets the hint text, if visible.
5369
6530
  *
@@ -5391,6 +6552,9 @@ declare class TnFormFieldHarness extends ComponentHarness {
5391
6552
  /**
5392
6553
  * Gets the tooltip message (read from the trigger's accessible label).
5393
6554
  *
6555
+ * A message holding markup — a link, say — comes back as the text a screen reader hears, since
6556
+ * that is what the label carries; assert against the message's text, not its tags.
6557
+ *
5394
6558
  * @returns Promise resolving to the tooltip text, or null if no tooltip.
5395
6559
  *
5396
6560
  * @example
@@ -5472,6 +6636,455 @@ interface FormFieldHarnessFilters extends BaseHarnessFilters {
5472
6636
  testId?: string;
5473
6637
  }
5474
6638
 
6639
+ /**
6640
+ * Renders the validation message for a control that is NOT projected into a
6641
+ * `tn-form-field` — in practice a `FormGroup` or `FormArray`, whose errors
6642
+ * belong to the group as a whole and so have no single field to sit under.
6643
+ *
6644
+ * `tn-form-field` covers the ordinary case and should still be preferred: it
6645
+ * owns the label, the `aria-describedby` wiring and the subscript slot. Reach
6646
+ * for this component only where there is no field to own the message —
6647
+ * a cross-field validator on a group, a `minArrayLength` on a form array, a
6648
+ * server-side error attached to a group by an error handler.
6649
+ *
6650
+ * The message comes from the same ladder `tn-form-field` uses (per-instance
6651
+ * `errorMessages`, then the app-wide {@link TN_FORM_FIELD_ERRORS} resolver,
6652
+ * then the built-in defaults), so a group message reads exactly like the field
6653
+ * messages around it. Like `tn-form-field`, it shows ONE message — the active
6654
+ * error, chosen by the same priority — rather than every error at once.
6655
+ *
6656
+ * @example
6657
+ * ```html
6658
+ * <tn-form-errors [control]="form.controls.schedule" />
6659
+ * ```
6660
+ */
6661
+ declare class TnFormErrorsComponent {
6662
+ /** The control whose errors are rendered. Usually a group or an array. */
6663
+ control: _angular_core.InputSignal<AbstractControl<any, any, any>>;
6664
+ /**
6665
+ * Per-instance overrides, keyed by error key. Take precedence over the
6666
+ * app-wide resolver, exactly as on `tn-form-field`.
6667
+ */
6668
+ errorMessages: _angular_core.InputSignal<Partial<Record<string, _truenas_ui_components.TnFormFieldErrorMessage>>>;
6669
+ /**
6670
+ * Show the message before the user has touched or dirtied the control.
6671
+ *
6672
+ * Off by default, so a freshly opened form does not greet the user with
6673
+ * errors. Turn it on where the invalid value did not come from the user —
6674
+ * an edit form populated from an API, or a group an error handler has just
6675
+ * attached a server-side failure to.
6676
+ */
6677
+ showWhenUntouched: _angular_core.InputSignal<boolean>;
6678
+ /**
6679
+ * Test-id base for the message element (`error-` prefixed). There is no
6680
+ * fallback: an `AbstractControl` does not know its own name, so a message
6681
+ * that needs to be addressable in a test has to be named here.
6682
+ */
6683
+ testId: _angular_core.InputSignal<TnTestIdValue>;
6684
+ /**
6685
+ * Error keys whose message renders with a dismiss button beside it — in
6686
+ * practice a failure the user cannot fix by editing a field, so the message
6687
+ * would otherwise stick: a server-side rejection an error handler attached to
6688
+ * the group.
6689
+ *
6690
+ * Only the error actually being shown gets the button, since that is the
6691
+ * message the button belongs to.
6692
+ *
6693
+ * Dismissing deletes these keys from the group's errors — listing them here is
6694
+ * what grants that. Every listed key the group carries goes at once, not just
6695
+ * the one behind the message, so a failure spread across sibling keys cannot
6696
+ * reappear from a sibling. Unlike `tn-form-field` there is no control to hand
6697
+ * focus back to once the button goes away, so a consumer who cares where focus
6698
+ * lands should move it in the {@link dismiss} handler.
6699
+ *
6700
+ * Left unset, the app-wide {@link TN_FORM_FIELD_DISMISSIBLE_ERRORS} default
6701
+ * applies; pass `[]` to opt this message out of it.
6702
+ */
6703
+ dismissibleErrors: _angular_core.InputSignal<readonly string[] | undefined>;
6704
+ /**
6705
+ * Accessible name for the dismiss button, which is icon-only and so has
6706
+ * nothing else to be named by. The library cannot translate its own
6707
+ * `'Dismiss this error'` default, so a consumer with an i18n layer passes an
6708
+ * already-translated string here.
6709
+ */
6710
+ dismissAriaLabel: _angular_core.InputSignal<string | undefined>;
6711
+ /**
6712
+ * Hover tooltip for the dismiss button. Defaults to the resolved
6713
+ * `dismissAriaLabel`, so one translated string covers both the accessible name
6714
+ * and the visible hint.
6715
+ */
6716
+ dismissTooltip: _angular_core.InputSignal<string | undefined>;
6717
+ /** Emits the error key whose message the user dismissed, after it is removed. */
6718
+ dismiss: _angular_core.OutputEmitterRef<string>;
6719
+ /**
6720
+ * Id of the message element, so a caller can point a control's
6721
+ * `aria-describedby` at a group message it is covered by.
6722
+ */
6723
+ readonly errorId: string;
6724
+ private errorResolver;
6725
+ private state;
6726
+ protected errorMessage: _angular_core.Signal<string>;
6727
+ /**
6728
+ * Whether to render. A control can be invalid with no message to show — a
6729
+ * group whose only error resolves to blank — so the message is part of the
6730
+ * condition rather than something the template renders empty.
6731
+ */
6732
+ protected show: _angular_core.Signal<boolean>;
6733
+ /**
6734
+ * The id when there is a message to point at, and null when there is not — so
6735
+ * a caller can bind this straight to `aria-describedby` without ever naming
6736
+ * an element that is not on screen.
6737
+ */
6738
+ readonly describedBy: _angular_core.Signal<string | null>;
6739
+ /**
6740
+ * The error key the shown message came from. Same pick `resolveErrorMessage`
6741
+ * makes, so the dismiss button can never belong to an error other than the one
6742
+ * being read.
6743
+ */
6744
+ protected activeError: _angular_core.Signal<string | null>;
6745
+ /**
6746
+ * Which list applies, whether the button shows, and what it is called — shared
6747
+ * with `tn-form-field` so a group message and the field messages beside it can
6748
+ * never decide dismissibility by different rules.
6749
+ */
6750
+ protected dismissible: DismissibleErrorState;
6751
+ protected dismissError(): void;
6752
+ constructor();
6753
+ private sync;
6754
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnFormErrorsComponent, never>;
6755
+ 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>;
6756
+ }
6757
+
6758
+ /**
6759
+ * A set of criteria that can be used to filter a list of
6760
+ * `TnFormErrorsHarness` instances.
6761
+ */
6762
+ interface TnFormErrorsHarnessFilters extends BaseHarnessFilters {
6763
+ /** Filters by the rendered message. Supports string or regex matching. */
6764
+ textContains?: string | RegExp;
6765
+ }
6766
+ /**
6767
+ * Harness for `tn-form-errors`.
6768
+ *
6769
+ * The host renders nothing while the control is valid, untouched, or has no
6770
+ * message to show, so `hasMessage()` — not the presence of the harness — is
6771
+ * what tells you whether an error is visible.
6772
+ *
6773
+ * @example
6774
+ * ```typescript
6775
+ * const errors = await loader.getHarness(TnFormErrorsHarness);
6776
+ * expect(await errors.getMessage()).toBe('Select at least one day');
6777
+ * ```
6778
+ */
6779
+ declare class TnFormErrorsHarness extends ComponentHarness {
6780
+ static hostSelector: string;
6781
+ /**
6782
+ * Gets a `HarnessPredicate` for finding a `tn-form-errors` by its message.
6783
+ *
6784
+ * @param options Options for filtering which instances are considered a match.
6785
+ */
6786
+ static with(options?: TnFormErrorsHarnessFilters): HarnessPredicate<TnFormErrorsHarness>;
6787
+ private message;
6788
+ private dismissButton;
6789
+ /** Whether a message is currently rendered. */
6790
+ hasMessage(): Promise<boolean>;
6791
+ /** The rendered message, or `''` when none is shown. */
6792
+ getMessage(): Promise<string>;
6793
+ /**
6794
+ * Whether the shown message carries a dismiss button — true only when the
6795
+ * active error key is one of the host's `dismissibleErrors`.
6796
+ */
6797
+ isDismissible(): Promise<boolean>;
6798
+ /**
6799
+ * Clicks the dismiss button, as a user clearing a server-side error would.
6800
+ *
6801
+ * The component clears the error itself — every key in its `dismissibleErrors`
6802
+ * that the control carries — and then emits `dismiss` with the key that was on
6803
+ * screen, so the message is gone by the time this resolves. The handler is for
6804
+ * what the app does next, such as moving focus.
6805
+ *
6806
+ * @throws If the shown message is not dismissible.
6807
+ */
6808
+ dismiss(): Promise<void>;
6809
+ }
6810
+
6811
+ /**
6812
+ * What a `tn-form-list-item` needs to know about the list it was projected
6813
+ * into. Published over DI rather than passed as an input, so that locking a
6814
+ * list is one binding on the list and not one on every entry the consumer
6815
+ * writes inside its `@for`.
6816
+ *
6817
+ * A projected entry's element injector chains through the `tn-form-list` it is
6818
+ * declared inside, the same way a control projected into `tn-form-field` reaches
6819
+ * {@link TN_FORM_FIELD_CONTEXT}. An entry used on its own injects nothing and
6820
+ * falls back to its own defaults.
6821
+ *
6822
+ * Its own file, and not `form-list.component.ts`, because the list already
6823
+ * imports the item to count its entries — a token declared beside either
6824
+ * component would make that import cycle.
6825
+ */
6826
+ interface TnFormListContext {
6827
+ /** Whether the enclosing list is locked, so an entry can disable its remove button. */
6828
+ disabled: Signal<boolean>;
6829
+ }
6830
+ /** DI token under which `tn-form-list` exposes its {@link TnFormListContext}. */
6831
+ declare const TN_FORM_LIST_CONTEXT: InjectionToken<TnFormListContext>;
6832
+
6833
+ /**
6834
+ * The editor for a repeating group of fields — a `FormArray` the user grows
6835
+ * and shrinks, rendered as a labelled group of {@link TnFormListItemComponent}
6836
+ * cards with an Add control in the header.
6837
+ *
6838
+ * Not to be confused with `tn-list`, which DISPLAYS a list of items. This one
6839
+ * edits one, and owns none of the array: the consumer holds the `FormArray`,
6840
+ * renders an item per element, and does the pushing and splicing in response
6841
+ * to `(add)` and each item's `(delete)`. That keeps the item's shape — which
6842
+ * only the consumer knows — out of the library.
6843
+ *
6844
+ * @example
6845
+ * ```html
6846
+ * <tn-form-list label="ACL entries" [control]="form.controls.entries" (add)="addEntry()">
6847
+ * @for (entry of form.controls.entries.controls; track entry; let i = $index) {
6848
+ * <tn-form-list-item label="ACL entry" (delete)="removeEntry(i)">
6849
+ * <!-- the entry's own fields -->
6850
+ * </tn-form-list-item>
6851
+ * }
6852
+ * </tn-form-list>
6853
+ * ```
6854
+ */
6855
+ declare class TnFormListComponent implements TnFormListContext {
6856
+ /**
6857
+ * The `FormArray` being edited. Optional, and used only to render an error
6858
+ * that belongs to the array as a whole — a minimum or maximum length. The
6859
+ * component neither reads the elements nor writes to it.
6860
+ */
6861
+ control: _angular_core.InputSignal<AbstractControl<any, any, any> | undefined>;
6862
+ /**
6863
+ * What the list is called, in the plural ('ACL entries'). Names the group,
6864
+ * so a screen reader announces which list a field is inside. Supports the
6865
+ * same lightweight markup as `tn-form-field` labels.
6866
+ */
6867
+ label: _angular_core.InputSignal<string>;
6868
+ /**
6869
+ * Optional help tooltip, shown via an icon in the header — next to the label
6870
+ * where there is one, and on its own where there is not.
6871
+ */
6872
+ tooltip: _angular_core.InputSignal<string>;
6873
+ /** Placement of the tooltip relative to its help icon. */
6874
+ tooltipPosition: _angular_core.InputSignal<TooltipPosition>;
6875
+ /** Marks the list as required — at least one entry. Renders the asterisk. */
6876
+ required: _angular_core.InputSignal<boolean>;
6877
+ /** Whether the Add control renders. Turn it off at a maximum length. */
6878
+ canAdd: _angular_core.InputSignal<boolean>;
6879
+ /**
6880
+ * Locks the list, for one the user may not edit yet: the group reports itself
6881
+ * `aria-disabled`, the entries are dimmed and stop taking pointer events, and
6882
+ * Add and every remove button are disabled.
6883
+ *
6884
+ * It does NOT disable the fields inside the entries — those are projected
6885
+ * content the consumer owns, so locking them is `entries.disable()` on the
6886
+ * `FormArray`, which is also what keeps their values out of `form.value`. This
6887
+ * input deliberately does not reach them by going `inert` instead: the entries
6888
+ * stay on screen, and `inert` would drop what a sighted user can still read out
6889
+ * of the accessibility tree entirely.
6890
+ */
6891
+ disabled: _angular_core.InputSignal<boolean>;
6892
+ /** Text of the Add control. English by default — pass a translated string. */
6893
+ addLabel: _angular_core.InputSignal<string>;
6894
+ /** Shown in place of the entries while there are none. */
6895
+ emptyMessage: _angular_core.InputSignal<string>;
6896
+ /**
6897
+ * Overrides the derived empty state. Set it to `false` while the entries are
6898
+ * still being fetched, so a list that is merely not loaded yet does not
6899
+ * announce itself as empty and then fill in.
6900
+ */
6901
+ empty: _angular_core.InputSignal<boolean | undefined>;
6902
+ /** Per-error overrides for the array-level message, as on `tn-form-field`. */
6903
+ errorMessages: _angular_core.InputSignal<Partial<Record<string, _truenas_ui_components.TnFormFieldErrorMessage>>>;
6904
+ /**
6905
+ * Show the array-level message before the user has touched the array — for a
6906
+ * list populated from an API, or one an error handler has just attached a
6907
+ * server-side failure to. Passed straight to `tn-form-errors`.
6908
+ */
6909
+ showErrorWhenUntouched: _angular_core.InputSignal<boolean>;
6910
+ /**
6911
+ * Error keys whose array-level message renders with a close button, and which
6912
+ * dismissing deletes. Unset takes the app-wide
6913
+ * `TN_FORM_FIELD_DISMISSIBLE_ERRORS` default; `[]` opts this list out. Passed
6914
+ * straight to `tn-form-errors`, which is where the reasoning lives.
6915
+ */
6916
+ dismissibleErrors: _angular_core.InputSignal<readonly string[] | undefined>;
6917
+ /** Accessible name for that close button. Pass it already translated. */
6918
+ dismissAriaLabel: _angular_core.InputSignal<string | undefined>;
6919
+ /** Hover hint for that close button. Defaults to `dismissAriaLabel`. */
6920
+ dismissTooltip: _angular_core.InputSignal<string | undefined>;
6921
+ /**
6922
+ * Test-id base for the group (`form-list-` prefixed). Also names the
6923
+ * array-level message, which gets it `error-` prefixed.
6924
+ */
6925
+ testId: _angular_core.InputSignal<TnTestIdValue>;
6926
+ /** Emitted when Add is pressed. Appending the element is the consumer's. */
6927
+ add: _angular_core.OutputEmitterRef<void>;
6928
+ /**
6929
+ * Emitted with the error key when the user closes the array-level message,
6930
+ * after it has been removed. `tn-form-errors` has no control to hand focus
6931
+ * back to, so a consumer who cares where focus lands moves it here.
6932
+ */
6933
+ dismiss: _angular_core.OutputEmitterRef<string>;
6934
+ /**
6935
+ * The projected entries. Counted rather than derived from the `FormArray`,
6936
+ * so the empty state follows what is actually on screen — a consumer may
6937
+ * filter or page the elements it renders, and `control` is optional anyway.
6938
+ */
6939
+ private items;
6940
+ protected isEmpty: _angular_core.Signal<boolean>;
6941
+ /**
6942
+ * The array-level message, queried rather than reached through a template
6943
+ * reference: it renders inside an `@if`, and a reference declared in a block
6944
+ * is scoped to that block.
6945
+ */
6946
+ private errors;
6947
+ /**
6948
+ * Points the group at its own message. `role="alert"` covers the moment the
6949
+ * error appears; it says nothing to a screen reader that tabs into the list
6950
+ * afterwards, which would otherwise hear the label and never learn the array
6951
+ * is in error. Null while there is no message, so this never names an element
6952
+ * that is not on screen.
6953
+ */
6954
+ protected describedBy: _angular_core.Signal<string | null>;
6955
+ /**
6956
+ * Accessible name for the Add control, which reads as a bare 'Add' beside
6957
+ * every other list on a long form. Names the list it adds to.
6958
+ */
6959
+ protected addAriaLabel: _angular_core.Signal<string>;
6960
+ protected tooltipAriaLabel: _angular_core.Signal<string>;
6961
+ /**
6962
+ * Stable id naming the group from the label text alone. Without it the
6963
+ * group's accessible name would also absorb the tooltip button and the Add
6964
+ * control, announcing both on every field inside the list.
6965
+ */
6966
+ protected readonly labelId: string;
6967
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnFormListComponent, never>;
6968
+ 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>;
6969
+ }
6970
+
6971
+ /**
6972
+ * One entry of a {@link TnFormListComponent} — a bordered card holding the
6973
+ * controls for a single element of the form array, with the control that
6974
+ * removes it.
6975
+ *
6976
+ * The item owns only the frame and the remove button; the controls inside are
6977
+ * the consumer's, bound to that element's `FormGroup`.
6978
+ */
6979
+ declare class TnFormListItemComponent {
6980
+ /**
6981
+ * Whether this entry can be removed. Turn it off for an entry the form
6982
+ * requires — the button disappears rather than being disabled, since a
6983
+ * permanently disabled control tells the user nothing about why.
6984
+ */
6985
+ canDelete: _angular_core.InputSignal<boolean>;
6986
+ /**
6987
+ * What one entry is called, in the singular ('ACL entry', 'Portal'). Used to
6988
+ * name the remove button, which is icon-only and has nothing else to be
6989
+ * named by. Pass it already translated.
6990
+ */
6991
+ label: _angular_core.InputSignal<string>;
6992
+ /**
6993
+ * Accessible name for the remove button. Defaults to `Remove <label>`, or
6994
+ * plain `Remove` when there is no label. Set it to translate the wording —
6995
+ * the library ships English only.
6996
+ */
6997
+ removeAriaLabel: _angular_core.InputSignal<string>;
6998
+ /**
6999
+ * Disables the remove button — the entry stays readable, the control just
7000
+ * stops working, the way a native disabled control does.
7001
+ *
7002
+ * Left unset it follows the enclosing `tn-form-list`'s own `disabled`, so
7003
+ * locking a list is one binding on the list rather than one per entry. Set it
7004
+ * explicitly to lock a single entry inside an otherwise editable list.
7005
+ */
7006
+ disabled: _angular_core.InputSignal<boolean | undefined>;
7007
+ /** Test-id base for the remove button (`icon-button-` prefixed). */
7008
+ testId: _angular_core.InputSignal<TnTestIdValue>;
7009
+ /** Emitted when the remove button is pressed. Removing is the consumer's. */
7010
+ delete: _angular_core.OutputEmitterRef<void>;
7011
+ /** Absent when the entry is used outside a `tn-form-list`. */
7012
+ private list;
7013
+ protected resolvedDisabled: _angular_core.Signal<boolean>;
7014
+ protected resolvedRemoveAriaLabel: _angular_core.Signal<string>;
7015
+ protected resolvedTestId: _angular_core.Signal<string | number | (string | number | null | undefined)[]>;
7016
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnFormListItemComponent, never>;
7017
+ 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>;
7018
+ }
7019
+
7020
+ /** Criteria for filtering `TnFormListHarness` instances. */
7021
+ interface TnFormListHarnessFilters extends BaseHarnessFilters {
7022
+ /** Filters by the list's label. Supports string or regex matching. */
7023
+ label?: string | RegExp;
7024
+ }
7025
+ /** Criteria for filtering `TnFormListItemHarness` instances. */
7026
+ type TnFormListItemHarnessFilters = BaseHarnessFilters;
7027
+ /**
7028
+ * Harness for one entry of a `tn-form-list`.
7029
+ *
7030
+ * @example
7031
+ * ```typescript
7032
+ * const [first] = await list.getItems();
7033
+ * await first.remove();
7034
+ * ```
7035
+ */
7036
+ declare class TnFormListItemHarness extends ComponentHarness {
7037
+ static hostSelector: string;
7038
+ static with(options?: TnFormListItemHarnessFilters): HarnessPredicate<TnFormListItemHarness>;
7039
+ private removeButton;
7040
+ /** Whether this entry offers a remove control. */
7041
+ canRemove(): Promise<boolean>;
7042
+ /**
7043
+ * Whether the remove control is disabled — true for an entry inside a
7044
+ * `disabled` list, or one given its own `disabled`. `false` when the entry
7045
+ * offers no remove control at all; ask `canRemove()` to tell the two apart.
7046
+ */
7047
+ isRemoveDisabled(): Promise<boolean>;
7048
+ /** Presses the remove control. Throws when the entry has none. */
7049
+ remove(): Promise<void>;
7050
+ }
7051
+ /**
7052
+ * Harness for `tn-form-list`, the editor for a repeating group of fields.
7053
+ *
7054
+ * @example
7055
+ * ```typescript
7056
+ * const list = await loader.getHarness(TnFormListHarness.with({ label: 'ACL entries' }));
7057
+ * expect(await list.getItemCount()).toBe(0);
7058
+ * expect(await list.isEmpty()).toBe(true);
7059
+ *
7060
+ * await list.add();
7061
+ * expect(await list.getItemCount()).toBe(1);
7062
+ * ```
7063
+ */
7064
+ declare class TnFormListHarness extends ComponentHarness {
7065
+ static hostSelector: string;
7066
+ static with(options?: TnFormListHarnessFilters): HarnessPredicate<TnFormListHarness>;
7067
+ private labelEl;
7068
+ private addButton;
7069
+ private emptyEl;
7070
+ /** The list's label, or `''` when it has none. */
7071
+ getLabel(): Promise<string>;
7072
+ /** Whether the Add control renders. */
7073
+ canAdd(): Promise<boolean>;
7074
+ /** Whether the Add control is disabled. */
7075
+ isAddDisabled(): Promise<boolean>;
7076
+ /** Presses Add. Throws when the list offers no Add control. */
7077
+ add(): Promise<void>;
7078
+ /** The entries currently rendered. */
7079
+ getItems(): Promise<TnFormListItemHarness[]>;
7080
+ /** How many entries are rendered. */
7081
+ getItemCount(): Promise<number>;
7082
+ /** Whether the empty message is showing. */
7083
+ isEmpty(): Promise<boolean>;
7084
+ /** The empty message, or `''` when the list has entries. */
7085
+ getEmptyMessage(): Promise<string>;
7086
+ }
7087
+
5475
7088
  /**
5476
7089
  * Semantic grouping for a related set of form fields. Renders a native
5477
7090
  * `<fieldset>` with an optional `<legend>` heading and help tooltip, and
@@ -5488,6 +7101,21 @@ declare class TnFormSectionComponent {
5488
7101
  tooltip: _angular_core.InputSignal<string>;
5489
7102
  /** Placement of the tooltip relative to its help icon. */
5490
7103
  tooltipPosition: _angular_core.InputSignal<TooltipPosition>;
7104
+ /**
7105
+ * Whether a tooltip message holding a link may be pinned open by clicking the help button (see
7106
+ * `tnTooltipSticky`). On by default, like the directive.
7107
+ *
7108
+ * It does not make plain tooltips pinnable — section help is nearly always plain text, and that
7109
+ * keeps hovering. Set it to false only to force a message that does hold a link back to hover
7110
+ * behaviour, accepting that the link is then unreachable.
7111
+ */
7112
+ tooltipSticky: _angular_core.InputSignal<boolean>;
7113
+ /**
7114
+ * Accessible name for the help button, which is icon-only and so has nothing else to be named
7115
+ * by. The message is what the button is for, but it may hold markup — a link is the whole point
7116
+ * of `tooltipSticky` — and `aria-label` takes plain text, so the tags come off first.
7117
+ */
7118
+ protected readonly tooltipAriaLabel: _angular_core.Signal<string>;
5491
7119
  /** Test id applied to the host for harness/e2e selection. */
5492
7120
  testId: _angular_core.InputSignal<TnTestIdValue>;
5493
7121
  /**
@@ -5498,7 +7126,7 @@ declare class TnFormSectionComponent {
5498
7126
  */
5499
7127
  protected readonly headingId: string;
5500
7128
  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>;
7129
+ 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
7130
  }
5503
7131
 
5504
7132
  /**
@@ -6233,8 +7861,78 @@ declare class TnListItemComponent {
6233
7861
  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
7862
  }
6235
7863
 
6236
- declare class TnListSubheaderComponent {
7864
+ /**
7865
+ * A section heading inside a list.
7866
+ *
7867
+ * A container role can forbid its children's roles, so a heading between two
7868
+ * rows invalidates the container — axe's `aria-required-children`, and the
7869
+ * defect fixed in #237 and #259. The heading is not dropped for that; it is
7870
+ * moved, and where the HOST goes depends on what owns it. See `ariaOwnerRole`
7871
+ * for what "owns" means here.
7872
+ *
7873
+ * | Owner | Host | The element around the text |
7874
+ * |---|---|---|
7875
+ * | `role="list"` | `listitem` | `heading`, level 3 |
7876
+ * | `role="listbox"` | `group`, named by the text below | an ordinary span |
7877
+ * | anything else | `heading`, level 3 | an ordinary span |
7878
+ *
7879
+ * Inside a LIST the host becomes the `listitem` the list requires and the
7880
+ * heading goes one level in — `<li><h3>Pools</h3></li>` in plain HTML, which
7881
+ * keeps the section heading in the accessibility tree at the level it always
7882
+ * had. The cost is that the list counts one more item per section, which is the
7883
+ * same count a browser reports for that HTML.
7884
+ *
7885
+ * Inside a LISTBOX neither of those is available. `listitem` is not an allowed
7886
+ * child of a listbox either, so it trades one `aria-required-children` violation
7887
+ * for another; and the heading cannot move one level in the way it does for a
7888
+ * list, because axe reads THROUGH a `group` when it collects what a listbox
7889
+ * owns — measured, a `group` wrapping a `role="heading"` reports the same
7890
+ * violation, now naming the heading. So the section survives as a `group` with
7891
+ * the subheader's own text as its accessible name, via `aria-labelledby` to the
7892
+ * unmarked span: the text is still in the accessibility tree and still
7893
+ * announced, as a named section rather than as a heading.
7894
+ *
7895
+ * That group holds the text and NOT the rows that follow it: a subheader is
7896
+ * projected content and is a sibling of the rows it introduces, so it can name
7897
+ * a section without enclosing one. Genuinely nesting the options is markup for
7898
+ * the consumer to write.
7899
+ *
7900
+ * Outside either — or nested inside a row of one, where a heading is already
7901
+ * legal — the host carries the heading itself and the inner element is an
7902
+ * ordinary span.
7903
+ */
7904
+ declare class TnListSubheaderComponent implements DoCheck {
6237
7905
  inset: _angular_core.InputSignal<boolean>;
7906
+ private readonly owner;
7907
+ /**
7908
+ * Id of the inner span, so that the `group` form can be named by the text.
7909
+ *
7910
+ * Allocated per instance rather than per render, and emitted only when the
7911
+ * group needs it — see the template.
7912
+ */
7913
+ protected readonly textId: string;
7914
+ /**
7915
+ * Which of the three containers in the class docblock this is sitting in.
7916
+ *
7917
+ * Only the two that prescribe their children are named; every other owner,
7918
+ * `null` included, leaves the heading on the host where it has always been.
7919
+ */
7920
+ private readonly ownerKind;
7921
+ /** The role the host carries. See the table in the class docblock. */
7922
+ protected readonly hostRole: _angular_core.Signal<"heading" | "listitem" | "group">;
7923
+ /** Whether the heading is on the HOST, which is where `aria-level` follows it. */
7924
+ protected readonly headingOnHost: _angular_core.Signal<boolean>;
7925
+ /**
7926
+ * Whether the heading has moved one level in, onto the span.
7927
+ *
7928
+ * `list` alone, and never both this and {@link headingOnHost}: one section is
7929
+ * one heading, and a host that is both a `listitem` and a heading is what
7930
+ * #237 removed.
7931
+ */
7932
+ protected readonly headingInside: _angular_core.Signal<boolean>;
7933
+ /** Whether the host is a `group` that the span below has to name. */
7934
+ protected readonly namesGroup: _angular_core.Signal<boolean>;
7935
+ ngDoCheck(): void;
6238
7936
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnListSubheaderComponent, never>;
6239
7937
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnListSubheaderComponent, "tn-list-subheader", never, { "inset": { "alias": "inset"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
6240
7938
  }
@@ -6303,14 +8001,53 @@ declare class TnListItemTrailingDirective {
6303
8001
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnListItemTrailingDirective, never>;
6304
8002
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnListItemTrailingDirective, "[tnListItemTrailing]", never, {}, {}, never, never, true, never>;
6305
8003
  }
6306
- declare class TnDividerDirective {
8004
+ /**
8005
+ * Makes an element that is already something else read as a divider.
8006
+ *
8007
+ * Same ARIA as `TnDividerComponent` and for the same reason (#237):
8008
+ * `role="separator"`, unless a `role="list"` is what owns it, where a separator
8009
+ * is not an allowed child and invalidates the list.
8010
+ *
8011
+ * **It no longer matches `tn-divider` as well, which is a breaking change**:
8012
+ * code that writes `<tn-divider>` while importing only this directive now has
8013
+ * no match for that element and stops compiling. Import `TnDividerComponent`
8014
+ * for the element form — it is what declares the element, and what every use of
8015
+ * `<tn-divider>` in this repository already imports.
8016
+ *
8017
+ * On `tn-divider` this only ever restated what the component declares, and once
8018
+ * the role varies by context, a second source for it is a second answer waiting
8019
+ * to disagree — two instances of the tracking that decides it, on one element,
8020
+ * writing one attribute.
8021
+ */
8022
+ declare class TnDividerDirective implements DoCheck {
8023
+ private readonly owner;
8024
+ protected readonly role: _angular_core.Signal<"presentation" | "separator">;
8025
+ ngDoCheck(): void;
6307
8026
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnDividerDirective, never>;
6308
- static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnDividerDirective, "tn-divider, [tnDivider]", never, {}, {}, never, never, true, never>;
8027
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnDividerDirective, "[tnDivider]", never, {}, {}, never, never, true, never>;
6309
8028
  }
6310
8029
 
6311
- declare class TnDividerComponent {
8030
+ /**
8031
+ * A rule between things.
8032
+ *
8033
+ * `role="separator"`, except where the element that owns it is a container
8034
+ * whose children are prescribed — a `role="list"` owns only `listitem`, so a
8035
+ * separator between two rows invalidates the list it sits in (#237). Owned by a
8036
+ * ROW of that list — a divider inside a `tn-list-item` — it is a separator like
8037
+ * anywhere else. See `ariaOwnerRole` for what "owns" means and why the DOM
8038
+ * decides it rather than DI.
8039
+ */
8040
+ declare class TnDividerComponent implements DoCheck {
6312
8041
  vertical: _angular_core.InputSignal<boolean>;
6313
8042
  inset: _angular_core.InputSignal<boolean>;
8043
+ private readonly owner;
8044
+ /**
8045
+ * `presentation` rather than no role at all: both are invisible to assistive
8046
+ * technology and satisfy the container, and this one says in the DOM that the
8047
+ * rule is decoration on purpose.
8048
+ */
8049
+ protected readonly role: _angular_core.Signal<"presentation" | "separator">;
8050
+ ngDoCheck(): void;
6314
8051
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnDividerComponent, never>;
6315
8052
  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
8053
  }
@@ -6330,14 +8067,95 @@ declare class TnListOptionComponent implements AfterContentInit {
6330
8067
  internalDisabled: _angular_core.WritableSignal<boolean | null>;
6331
8068
  internalColor: _angular_core.WritableSignal<"primary" | "warn" | "accent" | null>;
6332
8069
  effectiveSelected: _angular_core.Signal<boolean>;
8070
+ /**
8071
+ * Whether the parent `tn-selection-list` is disabled as a whole, pushed down
8072
+ * by the listbox (#221). `false` when there is no parent.
8073
+ *
8074
+ * Kept apart from `internalDisabled` rather than written into it, because the
8075
+ * two answer different questions and a single slot cannot hold both. The list
8076
+ * disabling itself must not erase an option the consumer disabled on its own,
8077
+ * and re-enabling the list is exactly where a shared slot loses that: the
8078
+ * parent has no record of what the option's own state was before it wrote
8079
+ * over it, so `[disabled]="true"` on the option never comes back. That is not
8080
+ * hypothetical — it is what `setDisabledState()` did to a per-option
8081
+ * `[disabled]` on every `control.enable()` before this signal existed.
8082
+ *
8083
+ * A plain boolean rather than the nullable this sits beside: absent and
8084
+ * not-disabled are the same thing for a parent's opinion, whereas
8085
+ * `internalDisabled` needs `null` to mean "the input still speaks".
8086
+ */
8087
+ listDisabled: _angular_core.WritableSignal<boolean>;
8088
+ /**
8089
+ * Disabled if EITHER source says so — the list as a whole, or this option.
8090
+ *
8091
+ * Not a precedence: a disabled list cannot be overridden by an enabled
8092
+ * option, and a list that is enabled has nothing to say about an option that
8093
+ * disabled itself. Only the second half is a fallback chain, and it is the
8094
+ * one that already existed: `internalDisabled` outranks the input when it has
8095
+ * been written. Nothing in the library writes it any more — `setDisabledState()`
8096
+ * was its only writer and now stops at `listDisabled` — so it is kept for the
8097
+ * public surface it has always been part of, alongside `internalSelected` and
8098
+ * `internalColor`, rather than because a path in here still uses it.
8099
+ */
6333
8100
  effectiveDisabled: _angular_core.Signal<boolean>;
6334
8101
  effectiveColor: _angular_core.Signal<"primary" | "warn" | "accent">;
8102
+ /**
8103
+ * The tabindex a parent `tn-selection-list` has assigned, or `null` when this
8104
+ * option is standalone.
8105
+ *
8106
+ * Set by the listbox's roving tabindex (#216): 0 on the one option that is
8107
+ * the list's single tab stop, -1 on the rest. `null` is not "no tab stop" but
8108
+ * "no parent" — it is what keeps a `tn-list-option` used on its own behaving
8109
+ * as #213 left it, and it is why this is a nullable number rather than a
8110
+ * number defaulting to -1.
8111
+ */
8112
+ rovingTabindex: _angular_core.WritableSignal<number | null>;
8113
+ /**
8114
+ * -1 rather than a removed attribute for a disabled option under a roving
8115
+ * tabindex, which is the one place these two paths disagree. An element with
8116
+ * no `tabindex` cannot be focused programmatically either, so dropping the
8117
+ * attribute would leave the listbox's arrow keys with nothing to move focus
8118
+ * to — and the listbox deliberately visits disabled options, so that they can
8119
+ * be perceived rather than silently skipped. Standalone, where every stop
8120
+ * costs a Tab press, the disabled option is still left out entirely.
8121
+ */
8122
+ effectiveTabindex: _angular_core.Signal<number | null>;
6335
8123
  protected hasLeadingContent: _angular_core.WritableSignal<boolean>;
6336
8124
  protected hasSecondaryTextContent: _angular_core.WritableSignal<boolean>;
6337
8125
  protected hasPrimaryTextDirective: _angular_core.WritableSignal<boolean>;
6338
8126
  ngAfterContentInit(): void;
6339
8127
  private checkContentProjection;
8128
+ /**
8129
+ * Move real DOM focus to this option.
8130
+ *
8131
+ * Called by the parent listbox as the arrow keys move (#216). Focus lands on
8132
+ * the host, which is both the element carrying the roving tabindex and the
8133
+ * element `:host(:focus-visible)` draws the #215 focus ring on — the two
8134
+ * reasons the listbox moves focus rather than pointing at the option with
8135
+ * `aria-activedescendant`.
8136
+ */
8137
+ focus(): void;
6340
8138
  onClick(_event: Event): void;
8139
+ /**
8140
+ * The key is swallowed BEFORE the disabled guard, not after it.
8141
+ *
8142
+ * Declining to toggle is not the same as declining the key. Space scrolls the
8143
+ * page on any element that is neither a form control nor a scroller, and the
8144
+ * option host is neither — so bailing out first hands the browser a Space
8145
+ * with its default action intact, on an element the user deliberately focused
8146
+ * and got nothing from. Nothing upstream saves it either: the listbox's own
8147
+ * handler falls through for Space on purpose, so that the key reaches here.
8148
+ *
8149
+ * Only reachable under a parent `tn-selection-list` (#216), whose roving
8150
+ * tabindex is what makes a disabled option focusable — standalone it carries
8151
+ * no `tabindex`, so it cannot hold focus and this never fires on it.
8152
+ *
8153
+ * Which is safe only because the key has to have been pressed on the host
8154
+ * itself. A host listener hears whatever bubbles out of projected content, so
8155
+ * a consumer's `<input>` inside an option would otherwise have its Space
8156
+ * swallowed by the `preventDefault()` above — and, on an enabled option,
8157
+ * toggle the option as well as typing.
8158
+ */
6341
8159
  onKeydown(event: Event): void;
6342
8160
  toggle(): void;
6343
8161
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnListOptionComponent, never>;
@@ -6353,21 +8171,238 @@ declare class TnSelectionListComponent implements ControlValueAccessor {
6353
8171
  disabled: _angular_core.InputSignal<boolean>;
6354
8172
  multiple: _angular_core.InputSignal<boolean>;
6355
8173
  color: _angular_core.InputSignal<"primary" | "warn" | "accent">;
8174
+ /**
8175
+ * Explicit accessible name for the listbox. Inside a `tn-form-field` with a
8176
+ * label this is unnecessary — the field names the list automatically — but a
8177
+ * list with neither is announced as an unlabelled listbox.
8178
+ *
8179
+ * ALIASED to the attribute name, unlike the plain `ariaLabel` most controls in
8180
+ * this library take, and the divergence is forced rather than chosen: this
8181
+ * component's `role="listbox"` is on the HOST, so `<tn-selection-list
8182
+ * aria-label="Mailboxes">` is valid markup that named the list before this
8183
+ * input existed. The host binding below rewrites that attribute on every
8184
+ * change-detection pass, so an unaliased input would silently STRIP a name
8185
+ * that used to work. The alias makes the same markup set the input instead,
8186
+ * and the binding writes it straight back.
8187
+ */
8188
+ ariaLabel: _angular_core.InputSignal<string | undefined>;
8189
+ /** Id of visible text naming the listbox. Wins over `ariaLabel` where it resolves. */
8190
+ ariaLabelledby: _angular_core.InputSignal<string | undefined>;
8191
+ /**
8192
+ * ARIA wiring from an enclosing `tn-form-field`, read for `labelledby` alone:
8193
+ * #235 is about the listbox having no accessible name, and the field's
8194
+ * `describedby`/`invalid`/`required` are a separate question this list has
8195
+ * never answered either way.
8196
+ *
8197
+ * Called with NO argument, so it reports the field's label id unconditioned.
8198
+ * Handing it the `ariaLabel` input would have it suppress the field itself —
8199
+ * on truthiness, so a whitespace-only label would cancel the field while being
8200
+ * dropped as no name, leaving nothing — and it would only do so while this
8201
+ * field is initialised after the one it reads, since a signal captured before
8202
+ * its own initialiser runs arrives as `undefined` and is swallowed by an
8203
+ * optional call. The suppression is applied below instead, where the rest of
8204
+ * the precedence is.
8205
+ */
8206
+ private readonly fieldAria;
8207
+ /**
8208
+ * The `aria-label` this component supplies, or `null` when it supplies none.
8209
+ *
8210
+ * Blank is not a name: `aria-label=""` names the listbox as emptily as no
8211
+ * attribute at all, while satisfying axe's `aria-input-field-name` rule — a
8212
+ * green check on a control a screen reader announces as "listbox".
8213
+ */
8214
+ private readonly resolvedAriaLabel;
8215
+ /**
8216
+ * The explicit `ariaLabelledby` input, blank normalised away, or `null`.
8217
+ *
8218
+ * Kept apart from the field's label because the two rank differently against
8219
+ * a name the consumer wrote on the host — see {@link hostAriaLabelledby}.
8220
+ */
8221
+ private readonly explicitAriaLabelledby;
8222
+ private readonly hostElement;
8223
+ /**
8224
+ * The value this component last wrote for each naming attribute, so that it
8225
+ * can tell its own from the consumer's. See {@link claim}.
8226
+ */
8227
+ private readonly written;
6356
8228
  selectionChange: _angular_core.OutputEmitterRef<TnSelectionChange>;
6357
8229
  options: _angular_core.Signal<readonly TnListOptionComponent[]>;
6358
8230
  private formDisabled;
6359
8231
  isDisabled: _angular_core.Signal<boolean>;
8232
+ /**
8233
+ * The option the user has moved to, and the slot it occupied at the time —
8234
+ * or `null` while they have not moved yet.
8235
+ *
8236
+ * Kept separate from `activeIndex` rather than seeded with a starting value,
8237
+ * because "where the user last was" and "where a user who has not arrived yet
8238
+ * would land" are different questions and only the second one should follow
8239
+ * the selection around. See `activeIndex`.
8240
+ *
8241
+ * The OPTION and not merely its index, because the options are content
8242
+ * children and the caller can add or remove them ABOVE the one the user is
8243
+ * standing on. An index survives that edit while quietly changing meaning —
8244
+ * it names whichever option shifted into the slot — so the tab stop and the
8245
+ * arrow keys' starting point both come away from the option holding focus,
8246
+ * and one ArrowDown lands two options from where the user is. A reference
8247
+ * cannot drift that way. The index rides along only as the fallback for the
8248
+ * one case a reference cannot answer: the option itself being removed.
8249
+ */
8250
+ private visited;
8251
+ /**
8252
+ * Which option carries the listbox's single tab stop.
8253
+ *
8254
+ * Before the user has touched the list this tracks the first selected option,
8255
+ * which is what APG asks for — tabbing into a list that already has a
8256
+ * selection should land where the user left off rather than at the top. Once
8257
+ * they have moved, `visited` pins it and the selection no longer drags the
8258
+ * tab stop around underneath them.
8259
+ *
8260
+ * Resolved against the current options on every read rather than stored, so
8261
+ * that a list edited underneath the user still points at the option they were
8262
+ * on. Only once that option has left the list is there nothing to resolve,
8263
+ * and the remembered slot is the best answer left — clamped, because an index
8264
+ * held across a removal otherwise reads past the end of the array.
8265
+ */
8266
+ private activeIndex;
8267
+ /**
8268
+ * The option host that currently holds DOM focus, or `null`.
8269
+ *
8270
+ * Held as an element rather than an index, because the whole point of it is
8271
+ * to outlive the option: an index still resolves after a removal, to whatever
8272
+ * option moved into that slot.
8273
+ */
8274
+ private focusedOptionElement;
6360
8275
  private onChange;
6361
8276
  private onTouched;
6362
8277
  constructor();
8278
+ /**
8279
+ * ArrowUp / ArrowDown / Home / End — unmodified, and pressed on an option
8280
+ * host — and deliberately nothing else.
8281
+ *
8282
+ * Space and Enter are absent because `tn-list-option` has handled them since
8283
+ * before this component had any keyboard handling at all, and its keydown
8284
+ * bubbles up to here. Toggling from both places would toggle twice — select
8285
+ * then immediately deselect — which reads as the key doing nothing rather
8286
+ * than as a bug, so it is worth stating why it is missing rather than leaving
8287
+ * a later reader to add it.
8288
+ *
8289
+ * Every other key is left alone, Tab included: `preventDefault()` on an
8290
+ * unrecognised key is how a widget traps a keyboard user inside it.
8291
+ *
8292
+ * Navigation is not gated on `isDisabled()`, because moving focus selects
8293
+ * nothing: a disabled list a user can still read through is the same
8294
+ * reasoning that has the arrow keys visit disabled options at all.
8295
+ *
8296
+ * That is a statement about NAVIGATION only, and deliberately not the wider
8297
+ * claim that a disabled list can be toggled. It cannot: `isDisabled()` is
8298
+ * pushed onto every option's `listDisabled` by the effect in the constructor
8299
+ * (#221), so the option's own guard refuses the toggle whichever route asked
8300
+ * for it — mouse, Space, Enter or a reactive form. What a disabled list still
8301
+ * allows is moving through it, which selects nothing.
8302
+ *
8303
+ * Only keys pressed ON an option host are the listbox's. The handler is on
8304
+ * the host and hears everything that bubbles through it, so without that test
8305
+ * a consumer who projects a focusable control into an option — a text input,
8306
+ * a slider — would have Home and End taken off its caret and the arrows taken
8307
+ * off its value, and get a `preventDefault()` for it. Nothing in this library
8308
+ * projects such a control today, which is why this is a target check rather
8309
+ * than a redesign.
8310
+ */
8311
+ onKeydown(event: KeyboardEvent): void;
8312
+ /**
8313
+ * Keep the tab stop under whichever option actually holds focus.
8314
+ *
8315
+ * Covers the routes into the list that are not the arrow keys — a click, and
8316
+ * a Tab that lands here — so that leaving the list and coming back returns to
8317
+ * the option the user was last on, rather than to the one the arrow keys
8318
+ * happened to leave the index at.
8319
+ *
8320
+ * `contains` rather than an identity check on the target, because focus can
8321
+ * land on something projected into the option rather than on the option host.
8322
+ */
8323
+ onFocusIn(event: FocusEvent): void;
8324
+ /**
8325
+ * Forget the focused option once focus leaves it under its own steam.
8326
+ *
8327
+ * Guarded on the option still being in the document, because a `focusout`
8328
+ * fired BY a removal — Firefox fires one, Chrome does not — is exactly the
8329
+ * case the restore in the constructor exists for, and clearing on it would
8330
+ * defeat that restore in one browser and not the other.
8331
+ */
8332
+ onFocusOut(event: FocusEvent): void;
8333
+ /**
8334
+ * The `aria-label` the host should carry.
8335
+ *
8336
+ * A METHOD rather than a signal, and that is the load-bearing part of this
8337
+ * whole arrangement: a host binding re-evaluates on every change-detection
8338
+ * pass, while a `computed` re-runs only when a signal it read has changed.
8339
+ * These two answers depend on the host's CURRENT attributes, which are not
8340
+ * signals — a consumer's `[attr.aria-label]` bound in the parent template
8341
+ * never reaches an input and never notifies anything. Asked once per pass,
8342
+ * the answer stays true; asked from a `computed`, it goes stale the moment
8343
+ * the consumer's binding changes, and the list is left announcing the wrong
8344
+ * name or no name at all.
8345
+ */
8346
+ protected hostAriaLabel(): string | null;
8347
+ /**
8348
+ * The `aria-labelledby` the host should carry — and the one place the naming
8349
+ * precedence is decided.
8350
+ *
8351
+ * Most specific first: the `ariaLabelledby` input, then any name the consumer
8352
+ * put on the host by another route, then the enclosing `tn-form-field`'s
8353
+ * label. The field comes last because it is chrome the consumer did not write
8354
+ * on this element, and it is WITHHELD rather than rendered alongside: ARIA
8355
+ * prefers `aria-labelledby` wherever it resolves, so a field reference emitted
8356
+ * beside a consumer's `aria-label` does not merely coexist with it — it
8357
+ * outranks it, and the list announces the field's label instead.
8358
+ *
8359
+ * With none of the three, a list stays unnamed — deliberately: a generic
8360
+ * fallback ("List") would satisfy axe while announcing nothing the user can
8361
+ * act on, and only the consumer knows what this list holds.
8362
+ */
8363
+ protected hostAriaLabelledby(): string | null;
8364
+ /** Whether the host already carries a non-blank name this component did not write. */
8365
+ private namedByConsumer;
8366
+ /**
8367
+ * What to bind `attribute` to: this component's own `value` where it has one,
8368
+ * and otherwise whatever is already on the host — so that a name the consumer
8369
+ * set is preserved rather than overwritten by the `null` that means "nothing
8370
+ * to say".
8371
+ *
8372
+ * That distinction is the reason this exists. `[attr.x]="null"` REMOVES the
8373
+ * attribute, and the role is on the HOST here, so `<tn-selection-list
8374
+ * aria-label="…">` and `[attr.aria-labelledby]="…"` are valid markup that
8375
+ * named this list before it had any naming input at all. Measured on Angular
8376
+ * 21, a plain host binding of these attributes ran after the parent's and left
8377
+ * the element with NEITHER — named to unnamed, silently, with the parent's
8378
+ * value reappearing only on a later pass and only for the one whose bound
8379
+ * value had changed.
8380
+ *
8381
+ * Ownership is tracked by value rather than by a flag, so it also lapses
8382
+ * correctly: an attribute this component wrote and something else has since
8383
+ * changed is no longer this component's to rewrite or take away.
8384
+ */
8385
+ private claim;
6363
8386
  writeValue(value: unknown[]): void;
6364
8387
  registerOnChange(fn: (value: unknown[]) => void): void;
6365
8388
  registerOnTouched(fn: () => void): void;
8389
+ /**
8390
+ * Records the form's state and stops there — the effect in the constructor is
8391
+ * what reaches the options, via `isDisabled()`.
8392
+ *
8393
+ * It used to also write each `option.internalDisabled` directly, which is the
8394
+ * only reason this route enforced anything while `[disabled]` enforced
8395
+ * nothing. Two problems, both fixed by routing it through the same signal the
8396
+ * input uses: the two paths could disagree, and `internalDisabled` is the
8397
+ * slot an option's own `[disabled]` input falls back to — so `control.enable()`
8398
+ * wrote `false` over an option the consumer had disabled independently, and
8399
+ * never gave it back.
8400
+ */
6366
8401
  setDisabledState(isDisabled: boolean): void;
6367
8402
  onOptionSelectionChange(): void;
6368
8403
  get selectedOptions(): TnListOptionComponent[];
6369
8404
  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: {}; }]>;
8405
+ 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
8406
  }
6372
8407
 
6373
8408
  declare class TnHeaderCellDefDirective {
@@ -6392,6 +8427,26 @@ declare class TnTableColumnDirective {
6392
8427
  * overrides the card field label. Falls back to the column `name` when unset.
6393
8428
  */
6394
8429
  label: _angular_core.InputSignal<string | undefined>;
8430
+ /**
8431
+ * Renders the header label for screen readers only, leaving the header cell
8432
+ * visually blank. For a column whose purpose is obvious from its contents and
8433
+ * whose heading would only add noise — a row-actions or icon column.
8434
+ *
8435
+ * A blank header is not the same thing as an unlabelled one: `<th>` with no
8436
+ * text at all fails axe's `empty-table-header`, and a screen-reader user
8437
+ * moving across the header row hears nothing where a column exists (#246). So
8438
+ * the label is still rendered, inside the table's own `.cdk-visually-hidden`
8439
+ * — the same treatment the built-in `__expand` and row-actions headers get.
8440
+ *
8441
+ * The text is `label`, falling back to the column `name`. A `tnHeaderCellDef`
8442
+ * template is NOT rendered when this is set: the point is that nothing shows,
8443
+ * and the class that hides it is scoped to the table's own view, so a
8444
+ * consumer's projected markup could not use it anyway.
8445
+ *
8446
+ * Header-only. Card mode has no header row; use `cardHidden` to keep a column
8447
+ * off the card.
8448
+ */
8449
+ hideLabel: _angular_core.InputSignal<boolean>;
6395
8450
  /**
6396
8451
  * Relative importance of this column in card mode (see `mobileLayout` on
6397
8452
  * `tn-table`). Higher numbers render first; fields ranked beyond
@@ -6428,7 +8483,7 @@ declare class TnTableColumnDirective {
6428
8483
  headerTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
6429
8484
  cellTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
6430
8485
  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>;
8486
+ 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
8487
  }
6433
8488
  /**
6434
8489
  * Directive to define the expandable detail row template.
@@ -6475,6 +8530,25 @@ declare class TnRowActionsDefDirective {
6475
8530
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnRowActionsDefDirective, "[tnRowActionsDef]", never, {}, {}, never, never, true, never>;
6476
8531
  }
6477
8532
 
8533
+ /**
8534
+ * The name the table's scroll region falls back to when the consumer has not
8535
+ * said what the table holds (#270).
8536
+ *
8537
+ * A focusable element with no accessible name is announced as a bare "group",
8538
+ * which tells a listener that something has been reached and nothing about what
8539
+ * it is. "Table" is a poor name and is deliberately the fallback rather than
8540
+ * the expectation — `scrollRegionAriaLabel` is how a consumer says something
8541
+ * useful, and it is the one input on this component whose default is worth
8542
+ * overriding on sight.
8543
+ *
8544
+ * No dev-mode warning goes with it, unlike `tnAccessibleName`'s fallbacks: this
8545
+ * name is rendered only on a table that is WIDER THAN ITS CONTAINER, which
8546
+ * depends on the consumer's layout rather than on their markup, so the warning
8547
+ * would fire on a viewport rather than on a mistake.
8548
+ *
8549
+ * Exported so specs assert against it by name rather than by a copied literal.
8550
+ */
8551
+ declare const TN_TABLE_SCROLL_REGION_LABEL = "Table";
6478
8552
  interface TnTableDataSource<T = unknown> {
6479
8553
  data?: T[];
6480
8554
  connect?(): T[];
@@ -6511,7 +8585,46 @@ interface TnSortEvent {
6511
8585
  * Above the breakpoint both modes render the regular table.
6512
8586
  */
6513
8587
  type TnTableMobileLayout = 'cards' | 'scroll';
8588
+ /**
8589
+ * Chrome copy `tn-table` renders itself. These were baked into the template as English literals,
8590
+ * which a consumer could not translate at all — there was no input to bind. Provide
8591
+ * {@link TN_TABLE_LABELS} at the app root to wire them to an i18n service.
8592
+ */
8593
+ interface TnTableLabels {
8594
+ /** Accessible name for the card-mode sort control. */
8595
+ sortBy: string;
8596
+ /** Card-mode sort option that clears the sort. */
8597
+ unsorted: string;
8598
+ /** Toggle that reveals the columns card mode collapsed. */
8599
+ moreFields: string;
8600
+ /** Card-mode button that opens a row's detail panel. */
8601
+ details: string;
8602
+ /**
8603
+ * Card-mode direction toggle while unsorted or sorted descending — the label names the
8604
+ * ACTION, and `toggleSortDirection()` starts at ascending from both of those states.
8605
+ */
8606
+ sortAscending: string;
8607
+ /** Card-mode direction toggle while sorted ascending. */
8608
+ sortDescending: string;
8609
+ /** Visually-hidden name for the row-expand COLUMN's header cell, not the per-row control. */
8610
+ expand: string;
8611
+ /** Per-row expand control while its detail row is collapsed. */
8612
+ expandRow: string;
8613
+ /** Per-row expand control while its detail row is open. */
8614
+ collapseRow: string;
8615
+ /** Visually-hidden name for the row actions column. */
8616
+ actions: string;
8617
+ }
8618
+ /** English defaults used when no `TN_TABLE_LABELS` provider is registered. */
8619
+ declare const TN_TABLE_DEFAULT_LABELS: TnTableLabels;
8620
+ /**
8621
+ * DI token for app-wide table chrome labels. Provide either a static object or a
8622
+ * `Signal<TnTableLabels>` — the latter lets every table react to language changes when the
8623
+ * consumer wires it up to an i18n service.
8624
+ */
8625
+ declare const TN_TABLE_LABELS: InjectionToken<TnTableLabels | Signal<TnTableLabels>>;
6514
8626
  declare class TnTableComponent<T = unknown> implements OnInit {
8627
+ protected readonly labels: Signal<TnTableLabels>;
6515
8628
  private destroyRef;
6516
8629
  private elementRef;
6517
8630
  private injector;
@@ -6523,6 +8636,39 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6523
8636
  * loads — was dropped to `<body>` and had to tab back from the top of the document.
6524
8637
  */
6525
8638
  private focusBeforeLoading;
8639
+ /**
8640
+ * Whether the host carries a tab stop, `role="group"` and a name because it
8641
+ * is currently scrolling (#270).
8642
+ *
8643
+ * `:host` is `overflow-x: auto` — the stylesheet says why the scrollport is
8644
+ * the host and not `.tn-table__table` — so a table wider than its container
8645
+ * scrolls HERE, and axe's `scrollable-region-focusable` reports a scroll
8646
+ * container that is neither in the tab order nor holds anything that is.
8647
+ *
8648
+ * Which this table is depends entirely on how it was configured: sortable
8649
+ * headers and clickable rows are `tabindex="0"`, so a table with either
8650
+ * satisfies the rule through its content, and a plain read-only one — the
8651
+ * default of every input on this component — satisfies nothing and leaves its
8652
+ * trailing columns unreachable from a keyboard.
8653
+ *
8654
+ * Gated on the measurement rather than on `isScrollMode()`: that class says
8655
+ * which LAYOUT the container's width selected, and this asks whether the
8656
+ * content actually exceeds the box, which is a different question and the one
8657
+ * axe asks. The measurement, the observers behind it and the rule that holds
8658
+ * the answer true while the host has focus are `tnScrollableRegion`'s.
8659
+ *
8660
+ * A field initializer rather than the constructor, because it registers an
8661
+ * `effect` and so needs an injection context.
8662
+ */
8663
+ protected scrollKeyboardReachable: Signal<boolean>;
8664
+ /**
8665
+ * The scroll region's name, falling back when a consumer passes whitespace.
8666
+ *
8667
+ * Blank is not a name: an `aria-label=" "` names the group as emptily as no
8668
+ * label at all, and axe agrees — the same rule `tnAccessibleName` applies to
8669
+ * every other name in this library.
8670
+ */
8671
+ protected resolvedScrollRegionLabel: Signal<string>;
6526
8672
  dataSource: _angular_core.InputSignal<TnTableDataSource<T> | T[]>;
6527
8673
  displayedColumns: _angular_core.InputSignal<string[]>;
6528
8674
  trackBy: _angular_core.InputSignal<((index: number, item: T) => unknown) | undefined>;
@@ -6534,6 +8680,17 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6534
8680
  */
6535
8681
  emptyDescription: _angular_core.InputSignal<string>;
6536
8682
  emptyIcon: _angular_core.InputSignal<string>;
8683
+ /**
8684
+ * Accessible name for the table's own scroll region, used only while the host
8685
+ * actually scrolls — see `scrollKeyboardReachable` and
8686
+ * `TN_TABLE_SCROLL_REGION_LABEL`.
8687
+ *
8688
+ * Set it to say what the table holds ("Storage pools"). It names the SCROLL
8689
+ * REGION rather than the table: a table's own structure is announced from its
8690
+ * rows and headers, and this is the box around it that a keyboard user stands
8691
+ * on to scroll sideways.
8692
+ */
8693
+ scrollRegionAriaLabel: _angular_core.InputSignal<string>;
6537
8694
  selectable: _angular_core.InputSignal<boolean>;
6538
8695
  expandable: _angular_core.InputSignal<boolean>;
6539
8696
  bordered: _angular_core.InputSignal<boolean>;
@@ -6620,6 +8777,10 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6620
8777
  * rows the `isRowExpandable` predicate rejects are unaffected, since `toggleRowExpansion` gates
6621
8778
  * on it. `rowClick` still emits, so a consumer can both expand and react to the click.
6622
8779
  *
8780
+ * In table mode the expanded state is announced by the chevron, not by the row: `aria-expanded`
8781
+ * is a `treegrid`-row attribute and a plain `table` row cannot hold it (#246). The row points at
8782
+ * the open panel with `aria-controls`, which every role permits.
8783
+ *
6623
8784
  * Applies in card mode too, where activating the card toggles its detail section. Both controls
6624
8785
  * report the state: the card carries `aria-expanded` while it is the trigger (`listitem` does
6625
8786
  * permit it, unlike `aria-selected`), and the "Details" button carries it unconditionally,
@@ -6670,7 +8831,7 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6670
8831
  */
6671
8832
  minWidth: _angular_core.InputSignal<string>;
6672
8833
  /** The floor actually applied to the table — explicit if given, else derived, else none. */
6673
- protected readonly resolvedMinWidth: _angular_core.Signal<string | null>;
8834
+ protected readonly resolvedMinWidth: Signal<string | null>;
6674
8835
  /**
6675
8836
  * How the table adapts when its container is narrower than `cardBreakpoint`.
6676
8837
  * See {@link TnTableMobileLayout}. Defaults to `scroll`, which preserves the
@@ -6708,9 +8869,9 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6708
8869
  * button inside the row, as the file picker does for entering directories.
6709
8870
  */
6710
8871
  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>;
8872
+ columnDefs: Signal<readonly TnTableColumnDirective[]>;
8873
+ detailRowDef: Signal<TnDetailRowDefDirective | undefined>;
8874
+ rowActionsDef: Signal<TnRowActionsDefDirective | undefined>;
6714
8875
  /** Observed host width in px; drives the switch into card mode. */
6715
8876
  private containerWidth;
6716
8877
  private resizeObserver?;
@@ -6748,21 +8909,50 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6748
8909
  private measureContainer;
6749
8910
  ngOnInit(): void;
6750
8911
  /** True when the layout should collapse rows into cards. */
6751
- isCardMode: _angular_core.Signal<boolean>;
8912
+ isCardMode: Signal<boolean>;
6752
8913
  /** 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[]>;
8914
+ isScrollMode: Signal<boolean>;
8915
+ data: Signal<T[]>;
8916
+ effectiveDisplayedColumns: Signal<string[]>;
6756
8917
  /**
6757
8918
  * Every column the table actually renders, including the trailing actions column, which
6758
8919
  * `effectiveDisplayedColumns` does not track because it comes from a content template rather
6759
8920
  * than `displayedColumns`. Used for the detail row's `colspan`, so a detail row spans the full
6760
8921
  * width no matter which structural columns are on.
6761
8922
  */
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>;
8923
+ totalColumnCount: Signal<number>;
8924
+ /**
8925
+ * How many rows a select-all can actually select.
8926
+ *
8927
+ * `SelectionModel` stores selections in a `Set`, so `selection.selected.length`
8928
+ * counts DISTINCT rows while `data().length` counts array entries. A `dataSource`
8929
+ * holding the same row reference twice makes the two disagree, and every comparison
8930
+ * of a selection count against a row count has to use this one to stay honest —
8931
+ * see {@link isAllSelected}.
8932
+ */
8933
+ private distinctRowCount;
8934
+ isAllSelected: Signal<boolean>;
8935
+ isIndeterminate: Signal<boolean>;
8936
+ /**
8937
+ * Whether the select-all control has anything to act on.
8938
+ *
8939
+ * Disabling it on an empty table is a correctness guard, not a nicety. Since #236
8940
+ * the checkbox is a real control the user can click, and its `checked` binding is
8941
+ * one-way: the DOM follows `isAllSelected()`, and Angular only writes the attribute
8942
+ * back when that value CHANGES. With no rows, `isAllSelected()` is pinned false —
8943
+ * selecting nothing leaves the count at zero — so a click would flip the input in
8944
+ * the DOM, change no bound value, and leave a checked-looking box over an empty
8945
+ * selection until something else re-rendered it.
8946
+ *
8947
+ * The hit area around the checkbox stands down for the same reason, so the two
8948
+ * cannot disagree about whether the control is live.
8949
+ *
8950
+ * An empty table is the only case that has to be disabled rather than fixed: a
8951
+ * repeated row reference produces the same DOM-versus-model divergence, and
8952
+ * {@link distinctRowCount} resolves that one by making the control work.
8953
+ */
8954
+ canSelectAll: Signal<boolean>;
8955
+ trackByFn: Signal<(index: number, item: T) => unknown>;
6766
8956
  onSortClick(column: string): void;
6767
8957
  isSorted(column: string): boolean;
6768
8958
  /**
@@ -6776,11 +8966,17 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6776
8966
  toggleRowExpansion(row: T): void;
6777
8967
  isRowExpanded(row: T): boolean;
6778
8968
  /**
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.
8969
+ * Whether the row element itself acts as the expand/collapse control. Requires `clickable` —
8970
+ * without it the row isn't activatable and {@link onRowClick} returns before toggling anything.
8971
+ *
8972
+ * A table row does NOT get `aria-expanded` from this. That attribute is only supported on a
8973
+ * `treegrid` row; on a plain `table` it is ignored, so the state it seemed to publish reached
8974
+ * nobody (#246). The chevron in the `__expand` cell carries it instead, on a `button`, where it
8975
+ * is valid and where a screen-reader user lands by tabbing. What the row does take from this is
8976
+ * `aria-controls`, which is global rather than role-conditional.
8977
+ *
8978
+ * Card mode is the case where the trigger element CAN hold the state — see
8979
+ * {@link isCardExpandTrigger}.
6784
8980
  */
6785
8981
  isRowExpandTrigger(row: T): boolean;
6786
8982
  /**
@@ -6788,8 +8984,8 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6788
8984
  * template. Card mode renders no expansion affordance at all without one, so `aria-expanded`
6789
8985
  * on the card would advertise a state change that produces nothing.
6790
8986
  *
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.
8987
+ * `listitem` permits `aria-expanded`, which is what lets the card hold the state its table-row
8988
+ * equivalent cannot.
6793
8989
  */
6794
8990
  isCardExpandTrigger(row: T): boolean;
6795
8991
  /** DOM id for a row's detail panel, so the expand trigger can point `aria-controls` at it. */
@@ -6855,14 +9051,55 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6855
9051
  */
6856
9052
  onSortHeaderClick(column: string, event: Event): void;
6857
9053
  /**
6858
- * Keyboard counterpart of {@link onSortHeaderClick}.
9054
+ * Keyboard counterpart of {@link onSortHeaderClick}.
9055
+ *
9056
+ * Takes `Event` rather than `KeyboardEvent` because Angular types `$event` as `Event` for
9057
+ * the `keydown.enter` / `keydown.space` pseudo-events; narrowing happens here.
9058
+ */
9059
+ onSortHeaderKeydown(column: string, event: Event): void;
9060
+ /** Handles the card-mode sort `<select>` change. */
9061
+ onSortSelectChange(event: Event): void;
9062
+ /**
9063
+ * Whether an event started inside a selection checkbox rather than on the hit area
9064
+ * around it.
9065
+ *
9066
+ * Matched on the component's host class rather than through
9067
+ * {@link isControlTarget}, because the element clicked is usually neither the input
9068
+ * nor the host: `tn-checkbox` renders a `<label>` wrapping the input and its
9069
+ * checkmark, and a click on the checkmark activates the input as the label's default
9070
+ * action. `closest('input')` says no to that click, and the toggle would then happen
9071
+ * twice — once here, once from the label's own activation.
9072
+ *
9073
+ * @param event The DOM event; its `currentTarget` is the hit area.
9074
+ */
9075
+ private isSelectionCheckboxTarget;
9076
+ /**
9077
+ * Click on the hit area around a select-all checkbox, in either layout.
9078
+ *
9079
+ * @param event The originating click.
9080
+ */
9081
+ onSelectAllHitAreaClick(event: Event): void;
9082
+ /**
9083
+ * Enter on the select-all checkbox.
9084
+ *
9085
+ * A native checkbox answers to Space and not to Enter, and the `<th>` this replaced
9086
+ * handled both. Bound on the header's checkbox only, which is where that behaviour
9087
+ * existed — card mode's select-all never had it.
9088
+ *
9089
+ * @param event The originating keydown; typed as `Event` because Angular types
9090
+ * `$event` that way for the `keydown.enter` pseudo-event.
9091
+ */
9092
+ onSelectAllEnter(event: Event): void;
9093
+ /**
9094
+ * Click on the hit area around a row's selection checkbox, in either layout.
6859
9095
  *
6860
- * Takes `Event` rather than `KeyboardEvent` because Angular types `$event` as `Event` for
6861
- * the `keydown.enter` / `keydown.space` pseudo-events; narrowing happens here.
9096
+ * Propagation stops whichever path activates the checkbox: a row is clickable and a
9097
+ * card is activatable, and selecting is not activating.
9098
+ *
9099
+ * @param event The originating click.
9100
+ * @param row The row the cell or card belongs to.
6862
9101
  */
6863
- onSortHeaderKeydown(column: string, event: Event): void;
6864
- /** Handles the card-mode sort `<select>` change. */
6865
- onSortSelectChange(event: Event): void;
9102
+ onRowSelectHitAreaClick(event: Event, row: T): void;
6866
9103
  toggleSelectAll(): void;
6867
9104
  toggleRowSelection(row: T): void;
6868
9105
  isRowSelected(row: T): boolean;
@@ -6873,17 +9110,17 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6873
9110
  * most prominent slot. Falls back to the first displayed column only when every
6874
9111
  * column is hidden, since a card still needs a title.
6875
9112
  */
6876
- cardTitleColumn: _angular_core.Signal<string>;
9113
+ cardTitleColumn: Signal<string>;
6877
9114
  /**
6878
9115
  * Columns rendered as label/value fields in a card, ordered by descending
6879
9116
  * `cardPriority` (ties keep `displayedColumns` order). Excludes the title column
6880
9117
  * and any `cardHidden` columns.
6881
9118
  */
6882
- cardFieldColumns: _angular_core.Signal<string[]>;
9119
+ cardFieldColumns: Signal<string[]>;
6883
9120
  /** Fields shown directly on the card (up to `cardPrimaryCount`). */
6884
- cardPrimaryColumns: _angular_core.Signal<string[]>;
9121
+ cardPrimaryColumns: Signal<string[]>;
6885
9122
  /** Fields tucked behind the "More fields" disclosure. */
6886
- cardSecondaryColumns: _angular_core.Signal<string[]>;
9123
+ cardSecondaryColumns: Signal<string[]>;
6887
9124
  /**
6888
9125
  * Displayed columns that are sortable — populates the card-mode sort menu.
6889
9126
  *
@@ -6897,7 +9134,7 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6897
9134
  * The title column stays eligible: it is excluded from the *fields*, but it is
6898
9135
  * displayed, so sorting by it is meaningful.
6899
9136
  */
6900
- sortableColumns: _angular_core.Signal<string[]>;
9137
+ sortableColumns: Signal<string[]>;
6901
9138
  /**
6902
9139
  * Whether the active sort column is one the table can actually sort by.
6903
9140
  *
@@ -6907,7 +9144,7 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6907
9144
  * non-sortable header does nothing, so the direction toggle is hidden — and
6908
9145
  * {@link toggleSortDirection} refuses — for the same column.
6909
9146
  */
6910
- protected readonly canSortActiveColumn: _angular_core.Signal<boolean>;
9147
+ protected readonly canSortActiveColumn: Signal<boolean>;
6911
9148
  /**
6912
9149
  * Sets (or clears, when passed `''`) the active sort column for card mode.
6913
9150
  * Switching columns resets to ascending, and clearing emits an empty `column` —
@@ -6931,7 +9168,7 @@ declare class TnTableComponent<T = unknown> implements OnInit {
6931
9168
  getCardLabel(column: string): string;
6932
9169
  getCellValue(row: T, column: string): unknown;
6933
9170
  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: {}; }]>;
9171
+ 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
9172
  }
6936
9173
 
6937
9174
  /**
@@ -7471,11 +9708,6 @@ declare class TnTablePagerComponent {
7471
9708
  * `TnTestIdValue` testId.
7472
9709
  */
7473
9710
  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
9711
  private readonly defaultLabels;
7480
9712
  /** 1-based index of the currently displayed page. */
7481
9713
  currentPage: _angular_core.ModelSignal<number>;
@@ -7509,7 +9741,27 @@ declare class TnTablePagerComponent {
7509
9741
  previousPageLabel: _angular_core.InputSignal<string | undefined>;
7510
9742
  nextPageLabel: _angular_core.InputSignal<string | undefined>;
7511
9743
  lastPageLabel: _angular_core.InputSignal<string | undefined>;
9744
+ /**
9745
+ * Accessible name for the pager's `navigation` landmark — what a screen
9746
+ * reader user picking this pager out of a landmark list hears.
9747
+ *
9748
+ * **Name every pager on a page that has more than one.** Two pagers both
9749
+ * announcing the DI default are one landmark repeated as far as that list is
9750
+ * concerned, which is what #249 was: `landmark-unique` fails and the user has
9751
+ * nothing to choose between them.
9752
+ */
7512
9753
  tablePaginationLabel: _angular_core.InputSignal<string | undefined>;
9754
+ /**
9755
+ * IDREF naming the pager from text already on the page — the heading over the
9756
+ * table it pages, typically, which is the name the user can see.
9757
+ *
9758
+ * Wins over the `aria-label` in the ARIA name computation while it resolves,
9759
+ * so pass one or the other rather than both. The `aria-label` is rendered
9760
+ * beside it either way and is what the pager falls back to if the IDREF is
9761
+ * typo'd or names an element that has not rendered yet — see
9762
+ * `resolvedTablePaginationLabel`.
9763
+ */
9764
+ ariaLabelledby: _angular_core.InputSignal<string | undefined>;
7513
9765
  /** Resolved labels: explicit input takes precedence over the DI default. */
7514
9766
  protected resolvedItemsPerPageLabel: Signal<string>;
7515
9767
  protected resolvedOfLabel: Signal<string>;
@@ -7517,7 +9769,57 @@ declare class TnTablePagerComponent {
7517
9769
  protected resolvedPreviousPageLabel: Signal<string>;
7518
9770
  protected resolvedNextPageLabel: Signal<string>;
7519
9771
  protected resolvedLastPageLabel: Signal<string>;
7520
- protected resolvedTablePaginationLabel: Signal<string>;
9772
+ /**
9773
+ * The landmark name a pager falls back to when the consumer sets no
9774
+ * `tablePaginationLabel` — the DI default, scoped by `testId` when there is
9775
+ * one.
9776
+ *
9777
+ * The scoping is what stops the DEFAULT from being the same string on every
9778
+ * pager, which is the shape #249 reported: two pagers, both announcing "Table
9779
+ * pagination", indistinguishable in a landmark list. `testId` is the only
9780
+ * per-instance identity the pager already has, and a page with two pagers
9781
+ * needs distinct ones anyway — without them the pagers' own child controls
9782
+ * collide on `select-page-size` / `button-first-page` (see the **Multiple
9783
+ * Pagers** story), so the multi-pager case that trips this rule is exactly the
9784
+ * case that already sets it.
9785
+ *
9786
+ * **It is a fallback and not the good answer.** A test id is a developer-facing
9787
+ * token: it is not translated, and `storage` reads as the word rather than as
9788
+ * "Storage pools". A pager whose name matters names itself with
9789
+ * `tablePaginationLabel`, or points at the table's visible heading with
9790
+ * `ariaLabelledby`; this only keeps the unnamed case from being ambiguous as
9791
+ * well as generic.
9792
+ *
9793
+ * Appended in parentheses rather than woven into the sentence, because the
9794
+ * base string comes from `TN_TABLE_PAGER_LABELS` and may be in any language —
9795
+ * there is no word order here to get right, only a translated name and a
9796
+ * scope after it.
9797
+ */
9798
+ private defaultTablePaginationLabel;
9799
+ /**
9800
+ * The name rendered as `aria-label`, always — the explicit
9801
+ * `tablePaginationLabel` when there is one, the scoped default otherwise.
9802
+ *
9803
+ * **It is emitted beside an `ariaLabelledby` rather than withheld under it**,
9804
+ * which is where this differs from `../a11y/accessible-name`, the shared rule
9805
+ * the progressbars and the dialogs take. That rule drops the generic fallback
9806
+ * when the caller supplies an IDREF, so a dangling IDREF surfaces as an
9807
+ * unnamed element instead of being masked by a name that says nothing. The
9808
+ * trade is only worth making where "unnamed" is itself caught: a progressbar
9809
+ * fails `aria-progressbar-name`, an `over` drawer fails `aria-dialog-name`,
9810
+ * and a `side` drawer — a landmark, like this — rests on that module's
9811
+ * dev-mode warning instead. This pager has neither. `landmark-unique` compares
9812
+ * landmarks against each other, so one unnamed pager violates nothing at all;
9813
+ * and the warning is one it cannot take, because the fallback here is a name
9814
+ * the consumer configures through `TN_TABLE_PAGER_LABELS` rather than one
9815
+ * someone forgot, so it would fire on the ordinary case.
9816
+ *
9817
+ * Withholding here would therefore turn a typo in `ariaLabelledby` into a
9818
+ * pager that announces nothing and reports nothing — a worse version of the
9819
+ * defect #249 opened on. So the pager keeps a name it can always fall back to,
9820
+ * and accepts that a dangling IDREF is masked by a generic one.
9821
+ */
9822
+ protected resolvedTablePaginationLabel: Signal<string | undefined>;
7521
9823
  /** Emits the new 1-based page number whenever the user navigates. */
7522
9824
  pageChange: _angular_core.OutputEmitterRef<number>;
7523
9825
  /** Emits the new page-size value when the dropdown changes. */
@@ -7562,7 +9864,7 @@ declare class TnTablePagerComponent {
7562
9864
  /** Forwards the current page/size to the data provider, if one is bound. */
7563
9865
  private pushToProvider;
7564
9866
  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>;
9867
+ 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
9868
  }
7567
9869
 
7568
9870
  /**
@@ -8561,6 +10863,26 @@ declare const QuickShortcuts: {
8561
10863
  alt(key: string, platform?: PlatformType): string;
8562
10864
  };
8563
10865
 
10866
+ /**
10867
+ * Reads a label token and hands back a Signal, whichever of the two shapes the app provided.
10868
+ *
10869
+ * Every `TN_*_LABELS` token in this library is declared as `T | Signal<T>` so a consumer can
10870
+ * pass a plain object (the common case — one static bundle for a single-language app) or a
10871
+ * Signal derived from an i18n service, in which case a language switch re-renders the chrome
10872
+ * live instead of needing a reload. Components only ever want the Signal, so normalizing at
10873
+ * the injection point keeps that union from leaking into every read site.
10874
+ *
10875
+ * @param token The label token to inject.
10876
+ * @returns A Signal of the provided labels; a plain object is wrapped in a constant Signal.
10877
+ *
10878
+ * @example
10879
+ * ```typescript
10880
+ * private readonly labels = injectTnLabels(TN_TABLE_LABELS);
10881
+ * // template: {{ labels().sortBy }}
10882
+ * ```
10883
+ */
10884
+ declare function injectTnLabels<T>(token: InjectionToken<T | Signal<T>>): Signal<T>;
10885
+
8564
10886
  declare class FileSizePipe implements PipeTransform {
8565
10887
  transform(value: number): string;
8566
10888
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<FileSizePipe, never>;
@@ -8618,6 +10940,20 @@ declare function labelMarkupToHtml(value: string): string;
8618
10940
  declare function labelMarkupToText(value: string): string;
8619
10941
 
8620
10942
  type SpinnerMode = 'determinate' | 'indeterminate';
10943
+ /**
10944
+ * The accessible name a spinner falls back to when the caller names neither
10945
+ * `ariaLabel` nor `ariaLabelledby` (#202). Same reasoning as
10946
+ * `TN_PROGRESS_BAR_DEFAULT_LABEL`, and the case is sharper here: the spinner
10947
+ * defaults to indeterminate mode, so its unnamed default rendering reached
10948
+ * assistive technology as a progressbar with neither a name nor a value.
10949
+ *
10950
+ * `branded-spinner.component.ts` in this folder already fell back this way —
10951
+ * `ariaLabel() || "Loading..."` inline — so a fallback is the shape this
10952
+ * library had already settled on; what it lacked was the warning. It has both
10953
+ * since #206, through the same `tnAccessibleName` this component uses, which is
10954
+ * why the two constants sit side by side and differ.
10955
+ */
10956
+ declare const TN_SPINNER_DEFAULT_LABEL = "Loading";
8621
10957
  declare class TnSpinnerComponent {
8622
10958
  mode: _angular_core.InputSignal<SpinnerMode>;
8623
10959
  value: _angular_core.InputSignal<number>;
@@ -8625,6 +10961,18 @@ declare class TnSpinnerComponent {
8625
10961
  strokeWidth: _angular_core.InputSignal<number>;
8626
10962
  ariaLabel: _angular_core.InputSignal<string | null>;
8627
10963
  ariaLabelledby: _angular_core.InputSignal<string | null>;
10964
+ /**
10965
+ * The name to render, or `null` to render no `aria-label` attribute — and the
10966
+ * dev-mode warning when the caller named neither input.
10967
+ *
10968
+ * Both halves live in `../a11y/accessible-name`, shared with `tn-progress-bar`
10969
+ * and `tn-branded-spinner` (#206), where the reasoning for each is set out:
10970
+ * an explicit `ariaLabel` always survives, because `aria-labelledby` only wins
10971
+ * the name calculation while its IDREF resolves; the generic fallback is
10972
+ * withheld beside one, because there it would mask a dangling IDREF with a
10973
+ * name that says nothing.
10974
+ */
10975
+ resolvedAriaLabel: _angular_core.Signal<string | null>;
8628
10976
  radius: _angular_core.Signal<number>;
8629
10977
  circumference: _angular_core.Signal<number>;
8630
10978
  strokeDasharray: _angular_core.Signal<string>;
@@ -8634,8 +10982,35 @@ declare class TnSpinnerComponent {
8634
10982
  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
10983
  }
8636
10984
 
10985
+ /**
10986
+ * The name this spinner falls back to when the caller names neither `ariaLabel`
10987
+ * nor `ariaLabelledby`. Same reasoning as `TN_SPINNER_DEFAULT_LABEL` and
10988
+ * `TN_PROGRESS_BAR_DEFAULT_LABEL`.
10989
+ *
10990
+ * It differs from `TN_SPINNER_DEFAULT_LABEL` — `"Loading..."` against
10991
+ * `"Loading"` — only because this is the string the component already rendered,
10992
+ * inline in its host binding, before #206 gave it the shared resolver. Aligning
10993
+ * the two would change what a screen reader announces for every unnamed branded
10994
+ * spinner already in the wild, which is a louder change than the consistency
10995
+ * fix it would be part of. Exported so specs assert against it by name rather
10996
+ * than by a copied string literal.
10997
+ */
10998
+ declare const TN_BRANDED_SPINNER_DEFAULT_LABEL = "Loading...";
8637
10999
  declare class TnBrandedSpinnerComponent implements OnInit, OnDestroy, AfterViewInit {
8638
11000
  ariaLabel: _angular_core.InputSignal<string | null>;
11001
+ ariaLabelledby: _angular_core.InputSignal<string | null>;
11002
+ /**
11003
+ * The name to render, or `null` to render no `aria-label` attribute — and the
11004
+ * dev-mode warning when the caller named neither input.
11005
+ *
11006
+ * This component is why `../a11y/accessible-name` exists (#206). It carried a
11007
+ * fallback inline in its host binding, so it never failed
11008
+ * `aria-progressbar-name` and the two fixes that gave the other progressbars
11009
+ * an `ariaLabelledby` input and a warning both passed it by. Routing it
11010
+ * through the shared resolver is what makes it named by the same rule rather
11011
+ * than by a third one nobody chose.
11012
+ */
11013
+ resolvedAriaLabel: _angular_core.Signal<string | null>;
8639
11014
  private paths;
8640
11015
  private animationId;
8641
11016
  private isAnimating;
@@ -8651,16 +11026,48 @@ declare class TnBrandedSpinnerComponent implements OnInit, OnDestroy, AfterViewI
8651
11026
  private animateSequence;
8652
11027
  private tween;
8653
11028
  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>;
11029
+ 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
11030
  }
8656
11031
 
8657
11032
  type ProgressBarMode = 'determinate' | 'indeterminate' | 'buffer';
11033
+ /**
11034
+ * The accessible name a bar falls back to when the caller names neither
11035
+ * `ariaLabel` nor `ariaLabelledby` (#202).
11036
+ *
11037
+ * Deliberately generic, and deliberately not silent: the host carries
11038
+ * `role="progressbar"` unconditionally, so without a fallback the default
11039
+ * rendering is a progressbar assistive technology announces with no name at all
11040
+ * — "progress bar, 40%", with nothing to say what is progressing. The
11041
+ * alternative fix, withholding the role until there is a name for it, trades
11042
+ * that for no announcement whatever, which is worse: a screen reader would not
11043
+ * learn that anything is in progress, and on a determinate bar it would lose
11044
+ * the value too.
11045
+ *
11046
+ * A generic name is still a poor one, so it is paired with the dev-mode warning
11047
+ * `tnAccessibleName` raises. Exported so specs assert against it by name rather
11048
+ * than by a copied string literal.
11049
+ */
11050
+ declare const TN_PROGRESS_BAR_DEFAULT_LABEL = "Progress";
8658
11051
  declare class TnProgressBarComponent {
8659
11052
  mode: _angular_core.InputSignal<ProgressBarMode>;
8660
11053
  value: _angular_core.InputSignal<number>;
8661
11054
  bufferValue: _angular_core.InputSignal<number>;
8662
11055
  ariaLabel: _angular_core.InputSignal<string | null>;
8663
11056
  ariaLabelledby: _angular_core.InputSignal<string | null>;
11057
+ /**
11058
+ * The name to render, or `null` to render no `aria-label` attribute — and the
11059
+ * dev-mode warning when the caller named neither input.
11060
+ *
11061
+ * Both halves live in `../a11y/accessible-name`, shared with `tn-spinner` and
11062
+ * `tn-branded-spinner` (#206), where the reasoning for each is set out: why an
11063
+ * explicit `ariaLabel` always survives, and why the generic fallback is
11064
+ * withheld beside an `ariaLabelledby`.
11065
+ *
11066
+ * A field initializer rather than the constructor, because it registers an
11067
+ * `effect` and so needs an injection context; this is one, and it keeps the
11068
+ * signal beside the inputs it reads.
11069
+ */
11070
+ resolvedAriaLabel: _angular_core.Signal<string | null>;
8664
11071
  /**
8665
11072
  * Gets the transform value for the primary progress bar
8666
11073
  */
@@ -8680,13 +11087,113 @@ declare class TnProgressBarComponent {
8680
11087
  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
11088
  }
8682
11089
 
11090
+ /**
11091
+ * The accessible name this bar falls back to when the caller names neither
11092
+ * `ariaLabel` nor `ariaLabelledby` (#209).
11093
+ *
11094
+ * The same string and the same reasoning as `TN_PROGRESS_BAR_DEFAULT_LABEL`
11095
+ * next door, and deliberately a separate constant rather than an import of it:
11096
+ * `tnAccessibleName` takes a fallback PER COMPONENT because the least-bad
11097
+ * generic name differs by what the component is ("Progress" for a bar,
11098
+ * "Loading" for a spinner), and sharing the binding would make a future
11099
+ * divergence in one a silent change to the other. Exported so specs assert
11100
+ * against it by name rather than by a copied string literal.
11101
+ */
11102
+ declare const TN_PARTICLE_PROGRESS_BAR_DEFAULT_LABEL = "Progress";
11103
+ /**
11104
+ * THE DECISION #209 ASKED FOR: THIS IS A PROGRESSBAR, NOT DECORATION
11105
+ * ------------------------------------------------------------------
11106
+ * Both readings were open. Before the change the host carried a class and
11107
+ * nothing else — no role, no name, no value — and because it never claimed
11108
+ * `role="progressbar"` it could not fail `aria-progressbar-name` either, so
11109
+ * every axe-based check in this library was silent on it by construction.
11110
+ * Measured on the unchanged component under jsdom, sweeping `svg-img-alt` and
11111
+ * the four rules `particle-progress-bar-a11y.spec.ts` keeps: 0 violations, 0
11112
+ * passes, 0 incomplete, on every one of them. Not clean — unexamined. That
11113
+ * spec keeps the measurement as its first test.
11114
+ *
11115
+ * It is a progressbar because it is a WHOLE indicator rather than an overlay on
11116
+ * someone else's. The SVG draws its own background track and its own fill rect,
11117
+ * and `fill` sizes the second against the first; that is a determinate progress
11118
+ * value, whatever the particles on top of it are doing. The alternative reading
11119
+ * — an ambient flourish shown BESIDE a real indicator, where a role would be
11120
+ * the redundant second announcement #203 was about — needs the real indicator
11121
+ * to exist, and nothing here provides one.
11122
+ *
11123
+ * The usage evidence points the same way, weakly but only in one direction. Its
11124
+ * only consumer in this repository is its own Storybook story: it has never
11125
+ * been placed next to a `tn-progress-bar`, so the redundancy risk is
11126
+ * hypothetical. It IS exported from `public-api.ts`, so consumers outside this
11127
+ * repository may already be showing it as the only progress on a screen — and
11128
+ * that asymmetry is what settles it. Choosing `aria-hidden` would assert a
11129
+ * usage constraint the library cannot enforce, and when that assertion is wrong
11130
+ * a screen-reader user gets nothing at all where a sighted user sees a bar
11131
+ * filling — strictly worse than the unnamed progressbars #202/#205/#206 fixed.
11132
+ * Choosing the role when the component really is ambient costs a redundant
11133
+ * announcement a consumer can silence with `aria-hidden` on its own wrapper.
11134
+ * The two errors are not the same size.
11135
+ *
11136
+ * The canvas IS decoration, and that is a separate question from what the host
11137
+ * is. It draws particles and carries no information the fill rect does not, so
11138
+ * the whole drawing is hidden and the ARIA value is what conveys the progress —
11139
+ * see the `aria-hidden` on the `<svg>` in the template.
11140
+ *
11141
+ * WHY THE VALUE IS A PERCENTAGE AND `fill` IS NOT
11142
+ * ----------------------------------------------
11143
+ * `fill` is a px length along the track, not a percentage — the story's control
11144
+ * runs it 0–600 while `width` runs 200–800, so the same `fill` means different
11145
+ * progress at different widths. `aria-valuenow` is reported on 0–100 instead,
11146
+ * matching `tn-progress-bar`, so that two bars from one library do not announce
11147
+ * on two different scales and no layout px reaches the accessibility tree.
11148
+ * Assistive technology derives the percentage from the range either way; what
11149
+ * this fixes is which range a consumer reads in the DOM.
11150
+ */
8683
11151
  declare class TnParticleProgressBarComponent implements AfterViewInit, OnDestroy {
8684
11152
  speed: _angular_core.InputSignal<"medium" | "slow" | "fast" | "ludicrous">;
8685
11153
  color: _angular_core.InputSignal<string>;
8686
11154
  height: _angular_core.InputSignal<number>;
8687
11155
  width: _angular_core.InputSignal<number>;
8688
11156
  fill: _angular_core.InputSignal<number>;
11157
+ ariaLabel: _angular_core.InputSignal<string | null>;
11158
+ ariaLabelledby: _angular_core.InputSignal<string | null>;
8689
11159
  canvasRef: _angular_core.Signal<ElementRef<HTMLCanvasElement>>;
11160
+ /** Exposed to the template so the rects and the ARIA value share one inset. */
11161
+ readonly trackInset = 50;
11162
+ /**
11163
+ * The name to render, or `null` to render no `aria-label` at all — and the
11164
+ * dev-mode warning when the caller named neither input.
11165
+ *
11166
+ * Both halves live in `../a11y/accessible-name`, shared with the other three
11167
+ * progressbars in this library (#206), where the reasoning for each is set
11168
+ * out: why an explicit `ariaLabel` always survives, and why the generic
11169
+ * fallback is withheld beside an `ariaLabelledby`. Routed through that helper
11170
+ * rather than given a rule of its own, which is what #209 asked for — a
11171
+ * fourth naming rule is how `tn-branded-spinner` ended up divergent.
11172
+ *
11173
+ * A field initializer rather than the constructor, because it registers an
11174
+ * `effect` and so needs an injection context; this is one, and it keeps the
11175
+ * signal beside the inputs it reads.
11176
+ */
11177
+ resolvedAriaLabel: _angular_core.Signal<string | null>;
11178
+ /** The drawable length of the track: the SVG width less the inset at each end. */
11179
+ trackLength: _angular_core.Signal<number>;
11180
+ /**
11181
+ * `fill` as a percentage of the track, or `null` when there is no track to
11182
+ * measure against.
11183
+ *
11184
+ * Clamped, because `fill` is not: the story alone can drive it to 600 against
11185
+ * a 500px track, where the fill rect simply overflows. `aria-valuenow` may not
11186
+ * exceed `aria-valuemax`, and 100 is also what such a bar visually reads as —
11187
+ * a track filled end to end. The clamp is on the announcement only; nothing
11188
+ * here changes what is drawn.
11189
+ *
11190
+ * `null` on a non-positive track — `width` at or below twice the inset — is
11191
+ * what makes the host announce as an INDETERMINATE progressbar rather than
11192
+ * carrying a value derived from a division by zero or a negative range. The
11193
+ * role stays either way, because "something is in progress" is true even when
11194
+ * how far is not answerable.
11195
+ */
11196
+ valuePercent: _angular_core.Signal<number | null>;
8690
11197
  private ctx;
8691
11198
  private particles;
8692
11199
  private shades;
@@ -8714,7 +11221,7 @@ declare class TnParticleProgressBarComponent implements AfterViewInit, OnDestroy
8714
11221
  */
8715
11222
  private generateDarkerShades;
8716
11223
  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>;
11224
+ 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
11225
  }
8719
11226
 
8720
11227
  interface DateRange {
@@ -9769,8 +12276,9 @@ declare class TnSliderThumbDirective implements ControlValueAccessor, OnInit, On
9769
12276
  value: () => number;
9770
12277
  labelPrefix: () => string;
9771
12278
  labelSuffix: () => string;
9772
- ariaLabel: () => string | undefined;
9773
- ariaLabelledby: () => string | undefined;
12279
+ resolvedAriaLabel: () => string | null;
12280
+ explicitAriaLabelledby: () => string | null;
12281
+ fieldAriaLabelledby: () => string | null;
9774
12282
  updateValue: (value: number) => void;
9775
12283
  markTouched: () => void;
9776
12284
  getSliderRect: () => DOMRect;
@@ -9817,12 +12325,29 @@ declare class TnSliderThumbDirective implements ControlValueAccessor, OnInit, On
9817
12325
  private onGlobalTouchEnd;
9818
12326
  private updateValueFromPosition;
9819
12327
  /**
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.
12328
+ * The `aria-label` for the range input: the parent slider's input when set,
12329
+ * otherwise a value placed directly on the `<input tnSliderThumb>`. Returning
12330
+ * the fallback keeps the host binding from wiping a directly-set label. Null
12331
+ * removes the attribute.
9824
12332
  */
9825
12333
  ariaLabel(): string | null;
12334
+ /**
12335
+ * The `aria-labelledby` for the range input — and the one place the precedence
12336
+ * between an explicit name and an inherited one is decided.
12337
+ *
12338
+ * An explicit reference comes first, from the slider's input or from this
12339
+ * input itself. **An enclosing `tn-form-field`'s label comes last, and only
12340
+ * when nothing has named the control directly.** That ordering is the point:
12341
+ * `aria-labelledby` beats `aria-label` in the ARIA name calculation whenever
12342
+ * it resolves, so emitting the field's label beside an `aria-label` written on
12343
+ * this input would render both attributes and announce the FIELD's label —
12344
+ * silently replacing the name the consumer wrote on the control (#235).
12345
+ *
12346
+ * The slider's own `ariaLabel` input needs no such check here, because
12347
+ * `injectTnFormFieldAria` already withholds the field's label whenever it is
12348
+ * set; this covers the label that is written past the slider, onto the
12349
+ * projected input, which the slider's inputs never see.
12350
+ */
9826
12351
  ariaLabelledby(): string | null;
9827
12352
  /**
9828
12353
  * Commit a value that originated outside the native input (a thumb drag).
@@ -9860,13 +12385,63 @@ declare class TnSliderComponent implements ControlValueAccessor, OnDestroy, Afte
9860
12385
  /**
9861
12386
  * Accessible name forwarded to the inner range input — the focusable element
9862
12387
  * 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
12388
+ * slider isn't already inside a labelled `tn-form-field`, otherwise a
9864
12389
  * 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.
12390
+ * "slider".
12391
+ *
12392
+ * Precedence is per attribute: each of `aria-label` and `aria-labelledby` is
12393
+ * taken from the matching input here, else from one written directly on the
12394
+ * `input[tnSliderThumb]`. Between the two attributes, ARIA's own rule decides
12395
+ * — `aria-labelledby` wins wherever it resolves.
12396
+ *
12397
+ * An enclosing `tn-form-field`'s label comes after all of those, and only when
12398
+ * nothing above has named the control: it is chrome the consumer did not write
12399
+ * on the control. See `TnSliderThumbDirective.ariaLabelledby`, where the
12400
+ * ordering is applied.
9867
12401
  */
9868
12402
  ariaLabel: _angular_core.InputSignal<string | undefined>;
9869
12403
  ariaLabelledby: _angular_core.InputSignal<string | undefined>;
12404
+ /**
12405
+ * ARIA wiring from an enclosing `tn-form-field`, read for `labelledby` alone:
12406
+ * #235 is about the range input having no accessible name, and the field's
12407
+ * `describedby`/`invalid`/`required` are a separate question this slider has
12408
+ * never answered either way.
12409
+ *
12410
+ * Called with NO argument, so it reports the field's label id unconditioned.
12411
+ * Handing it the `ariaLabel` input would have it suppress the field itself —
12412
+ * on truthiness, so a whitespace-only label would cancel the field while being
12413
+ * dropped as no name, leaving nothing — and it would only do so while this
12414
+ * field is initialised after the one it reads, since a signal captured before
12415
+ * its own initialiser runs arrives as `undefined` and is swallowed by an
12416
+ * optional call. The suppression is the thumb's, where the rest of the
12417
+ * precedence already lives and where it is visible.
12418
+ */
12419
+ private readonly fieldAria;
12420
+ /**
12421
+ * The `aria-label` the thumb should render, or `null` for none.
12422
+ *
12423
+ * Blank is not a name: `aria-label=""` names the input as emptily as no
12424
+ * attribute at all, while satisfying axe's `label` rule — a green check on a
12425
+ * control a screen reader announces as "slider" (#235). Same reasoning as
12426
+ * `a11y/accessible-name.ts`, which the three progressbars share.
12427
+ */
12428
+ readonly resolvedAriaLabel: _angular_core.Signal<string | null>;
12429
+ /**
12430
+ * The `ariaLabelledby` INPUT, blank normalised away, or `null` when unset.
12431
+ *
12432
+ * Kept separate from {@link fieldAriaLabelledby} rather than folded into one
12433
+ * resolved value, because the two rank differently against a label written on
12434
+ * the projected input — see `TnSliderThumbDirective.ariaLabelledby`.
12435
+ */
12436
+ readonly explicitAriaLabelledby: _angular_core.Signal<string | null>;
12437
+ /**
12438
+ * The enclosing `tn-form-field`'s label id, or `null` outside one.
12439
+ *
12440
+ * With no field and no input, a slider stays unnamed — deliberately: a
12441
+ * generic fallback ("Slider") would satisfy axe while announcing nothing the
12442
+ * user can act on, and only the consumer knows what this slider controls.
12443
+ */
12444
+ readonly fieldAriaLabelledby: _angular_core.Signal<string | null>;
9870
12445
  thumbDirective: _angular_core.Signal<TnSliderThumbDirective>;
9871
12446
  sliderContainer: _angular_core.Signal<ElementRef<HTMLDivElement>>;
9872
12447
  private onChange;
@@ -10175,11 +12750,243 @@ interface ButtonToggleHarnessFilters extends BaseHarnessFilters {
10175
12750
  label?: string | RegExp;
10176
12751
  }
10177
12752
 
12753
+ /**
12754
+ * The visual half of `tnTooltip`, and — while it is only shown on hover — only the visual half
12755
+ * (#203).
12756
+ *
12757
+ * WHY THE HOVER PANEL IS `aria-hidden` AND CARRIES NO ROLE
12758
+ * --------------------------------------------------------
12759
+ * The accessible text is `TnTooltipDirective`'s job, and it routes it through
12760
+ * CDK's `AriaDescriber` — a persistent visually-hidden element referenced by
12761
+ * `aria-describedby` on the interactive control, so the reference never dangles
12762
+ * while this overlay comes and goes. That is one tooltip, described once.
12763
+ *
12764
+ * This node used to ALSO claim `role="tooltip"`, which put a second tooltip in
12765
+ * the accessibility tree for the same message: the describer's (named, and
12766
+ * referenced by the control) and this one (named by nothing when the message is
12767
+ * empty or markup-only). axe scores that `aria-tooltip-name`, WCAG 4.1.2.
12768
+ * Of the two models the ticket set out — make the overlay the accessible
12769
+ * tooltip, or make it decorative — decorative is the one the rest of the
12770
+ * directive is already built for, and it is what `pointer-events: none` on
12771
+ * `:host` already says: this element cannot be hovered, clicked or interacted
12772
+ * with. Hiding it removes the duplicate rather than moving the name onto it.
12773
+ *
12774
+ * `aria-hidden="true"` is bound to `sticky` rather than to a visibility signal.
12775
+ * The node is exposed in no state a hover tooltip can be in, so there is no
12776
+ * visibility for it to reflect — the old hard-coded `aria-hidden="false"` was
12777
+ * the bug, because that value DID depend on state it was not tracking.
12778
+ *
12779
+ * AND WHY A PINNED ONE IS THE OPPOSITE
12780
+ * ------------------------------------
12781
+ * `sticky` is the one state where the panel is more than decoration: it takes
12782
+ * pointer events, holds focusable content, and renders a dismiss button, so
12783
+ * hiding it would put focusable nodes inside an `aria-hidden` subtree and make
12784
+ * the very content pinning exists to reach unreachable. A pinned panel is
12785
+ * therefore an exposed, named `dialog` (see `sticky`), and the describer's
12786
+ * description on the host stands alongside it rather than in place of it.
12787
+ */
10178
12788
  declare class TnTooltipComponent {
10179
12789
  message: _angular_core.InputSignal<string>;
12790
+ /**
12791
+ * Optional DOM id for the rendered element. Omitted entirely when empty
12792
+ * rather than rendered as `id=""`, which no `aria-describedby` or selector can
12793
+ * reference — a silently dangling hook.
12794
+ */
10180
12795
  id: _angular_core.InputSignal<string>;
12796
+ /**
12797
+ * Pinned ("sticky") mode. The tooltip stops being click-through so its content can be
12798
+ * interacted with, and a dismiss button is rendered next to the message.
12799
+ *
12800
+ * It also changes what the panel *is* for assistive tech: ARIA's `tooltip` role is specified as
12801
+ * non-focusable, non-interactive content that something else is described by, so a screen
12802
+ * reader may flatten it to a text description and never expose the link or the dismiss button -
12803
+ * exactly what pinning exists to make reachable. A pinned panel is therefore a `dialog`, named
12804
+ * by `panelAriaLabel`.
12805
+ *
12806
+ * It is a *non-modal* dialog and deliberately traps nothing: Tab past the dismiss button leaves
12807
+ * the panel while it stays open. Where focus lands next is wherever the panel sits in the tab
12808
+ * order, which is the end of it - CDK appends its overlay container as the last child of
12809
+ * `<body>` - so in practice Tab leaves the document for the browser's own chrome rather than
12810
+ * continuing after the host. Not trapping is the right shape for a popup the user can also
12811
+ * leave by Escape or by clicking outside, and it keeps a tooltip from holding the keyboard
12812
+ * hostage.
12813
+ */
12814
+ sticky: _angular_core.InputSignal<boolean>;
12815
+ /** Accessible name for the dismiss button, so consumers can localize it. */
12816
+ closeAriaLabel: _angular_core.InputSignal<string>;
12817
+ /**
12818
+ * Accessible name for the pinned panel (see `sticky`), so consumers can localize it.
12819
+ *
12820
+ * A short static name rather than the message: a screen reader reads a dialog's name on entry
12821
+ * and then its content, so naming it after the message would announce that message twice —
12822
+ * three times counting the host's own description.
12823
+ */
12824
+ panelAriaLabel: _angular_core.InputSignal<string>;
12825
+ /** Emitted when the user activates the dismiss button. */
12826
+ onDismiss: _angular_core.OutputEmitterRef<void>;
12827
+ private panel;
12828
+ /**
12829
+ * Moves focus onto the tooltip panel. Used when sticky mode is entered from the keyboard, so
12830
+ * the tooltip's content is reachable without a pointer: from the panel, Tab walks the message
12831
+ * (links included) and then the dismiss button. The panel is only focusable in sticky mode,
12832
+ * so this is a no-op otherwise.
12833
+ */
12834
+ focusPanel(): void;
10181
12835
  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>;
12836
+ 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>;
12837
+ }
12838
+
12839
+ /**
12840
+ * Harness for interacting with a tooltip rendered by `tnTooltip` in tests.
12841
+ *
12842
+ * Tooltips render in a CDK overlay **outside** the component tree, so the regular
12843
+ * `TestbedHarnessEnvironment.loader(fixture)` won't find them. Use
12844
+ * `TnTooltipTesting.rootLoader(fixture)` instead — it searches the whole document.
12845
+ *
12846
+ * @example
12847
+ * ```typescript
12848
+ * import { TnTooltipHarness, TnTooltipTesting } from '@truenas/ui-components';
12849
+ *
12850
+ * const rootLoader = TnTooltipTesting.rootLoader(fixture);
12851
+ *
12852
+ * // Hover tooltip: show it, then read it
12853
+ * host.dispatchEvent(new MouseEvent('mouseenter'));
12854
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
12855
+ * expect(await tooltip.getText()).toBe('Pool is online');
12856
+ *
12857
+ * // Sticky tooltip: click the host to pin it, then dismiss it
12858
+ * host.click();
12859
+ * const pinned = await rootLoader.getHarness(TnTooltipHarness);
12860
+ * expect(await pinned.isSticky()).toBe(true);
12861
+ * await pinned.dismiss();
12862
+ * ```
12863
+ */
12864
+ declare class TnTooltipHarness extends ComponentHarness {
12865
+ /**
12866
+ * The selector for the host element of a `TnTooltipComponent` instance.
12867
+ */
12868
+ static hostSelector: string;
12869
+ private _panel;
12870
+ private _message;
12871
+ private _closeButton;
12872
+ /**
12873
+ * Gets a `HarnessPredicate` that can be used to search for a tooltip with specific
12874
+ * attributes. Useful when more than one tooltip is on screen — a pinned one plus a hover
12875
+ * one, for instance.
12876
+ *
12877
+ * @param options Options for filtering which tooltip instances are considered a match.
12878
+ * @returns A `HarnessPredicate` configured with the given options.
12879
+ *
12880
+ * @example
12881
+ * ```typescript
12882
+ * // Find a tooltip by its text
12883
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness.with({ text: 'Pool is online' }));
12884
+ *
12885
+ * // Find only the pinned one
12886
+ * const pinned = await rootLoader.getHarness(TnTooltipHarness.with({ sticky: true }));
12887
+ * ```
12888
+ */
12889
+ static with(options?: TooltipHarnessFilters): HarnessPredicate<TnTooltipHarness>;
12890
+ /**
12891
+ * Gets the tooltip's text content, with any markup in the message stripped.
12892
+ *
12893
+ * @returns Promise resolving to the tooltip's text.
12894
+ *
12895
+ * @example
12896
+ * ```typescript
12897
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
12898
+ * expect(await tooltip.getText()).toBe('Pool is online');
12899
+ * ```
12900
+ */
12901
+ getText(): Promise<string>;
12902
+ /**
12903
+ * Checks whether the tooltip is pinned open (sticky mode), i.e. interactive and dismissible
12904
+ * rather than tied to the pointer.
12905
+ *
12906
+ * @returns Promise resolving to true if the tooltip is pinned.
12907
+ *
12908
+ * @example
12909
+ * ```typescript
12910
+ * host.click();
12911
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
12912
+ * expect(await tooltip.isSticky()).toBe(true);
12913
+ * ```
12914
+ */
12915
+ isSticky(): Promise<boolean>;
12916
+ /**
12917
+ * Gets the accessible name of the dismiss button, or null when the tooltip isn't pinned.
12918
+ *
12919
+ * @returns Promise resolving to the dismiss button's aria-label.
12920
+ *
12921
+ * @example
12922
+ * ```typescript
12923
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
12924
+ * expect(await tooltip.getDismissLabel()).toBe('Close tooltip');
12925
+ * ```
12926
+ */
12927
+ getDismissLabel(): Promise<string | null>;
12928
+ /**
12929
+ * Clicks the tooltip's dismiss button. Throws if the tooltip isn't pinned, since the button
12930
+ * only exists in sticky mode.
12931
+ *
12932
+ * @returns Promise that resolves when the tooltip has been dismissed.
12933
+ *
12934
+ * @example
12935
+ * ```typescript
12936
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
12937
+ * await tooltip.dismiss();
12938
+ * ```
12939
+ */
12940
+ dismiss(): Promise<void>;
12941
+ /**
12942
+ * Clicks an element inside the tooltip's message — a link, typically. Only meaningful in
12943
+ * sticky mode, where the tooltip stops being click-through.
12944
+ *
12945
+ * @param selector CSS selector of the element to click, relative to the message.
12946
+ * @returns Promise that resolves when the click action is complete.
12947
+ *
12948
+ * @example
12949
+ * ```typescript
12950
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
12951
+ * await tooltip.clickContent('a');
12952
+ * ```
12953
+ */
12954
+ clickContent(selector: string): Promise<void>;
12955
+ }
12956
+ /**
12957
+ * A set of criteria that can be used to filter a list of `TnTooltipHarness` instances.
12958
+ */
12959
+ interface TooltipHarnessFilters extends BaseHarnessFilters {
12960
+ /** Filters by the tooltip's text. Supports string or regex matching. */
12961
+ text?: string | RegExp;
12962
+ /** Filters by whether the tooltip is pinned open (sticky mode). */
12963
+ sticky?: boolean;
12964
+ }
12965
+
12966
+ /**
12967
+ * Test utilities for working with `TnTooltipHarness`.
12968
+ *
12969
+ * Tooltips are portaled into the CDK overlay outside the component tree, so a regular
12970
+ * `TestbedHarnessEnvironment.loader()` won't find them. Use `TnTooltipTesting.rootLoader()`
12971
+ * to get a loader that can.
12972
+ *
12973
+ * @example
12974
+ * ```typescript
12975
+ * import { TnTooltipTesting, TnTooltipHarness } from '@truenas/ui-components';
12976
+ *
12977
+ * const rootLoader = TnTooltipTesting.rootLoader(fixture);
12978
+ * const tooltip = await rootLoader.getHarness(TnTooltipHarness);
12979
+ * ```
12980
+ */
12981
+ declare class TnTooltipTesting {
12982
+ /**
12983
+ * Creates a `HarnessLoader` that searches the entire document, including the CDK overlays
12984
+ * that tooltips are rendered into.
12985
+ *
12986
+ * @param fixture The component fixture for the test.
12987
+ * @returns A `HarnessLoader` capable of finding tooltip harnesses.
12988
+ */
12989
+ static rootLoader(fixture: ComponentFixture<unknown>): HarnessLoader;
10183
12990
  }
10184
12991
 
10185
12992
  interface TnConfirmDialogData {
@@ -10234,7 +13041,56 @@ declare class TnDialog {
10234
13041
  static ɵprov: _angular_core.ɵɵInjectableDeclaration<TnDialog>;
10235
13042
  }
10236
13043
 
13044
+ /**
13045
+ * Chrome copy `tn-dialog-shell` renders itself. These were baked into the template as English
13046
+ * literals, which a consumer could not translate at all — there was no input to bind. Provide
13047
+ * {@link TN_DIALOG_CHROME_LABELS} at the app root to wire them to an i18n service.
13048
+ *
13049
+ * "Chrome" rather than plain `TnDialogLabels` to keep this bundle a clear letter apart from
13050
+ * {@link TN_DIALOG_SHELL_DEFAULT_LABEL}, which is a different thing entirely — the fallback
13051
+ * accessible name for the dialog surface itself, not copy the header renders.
13052
+ */
13053
+ interface TnDialogChromeLabels {
13054
+ /** Accessible name for the header close (X) button. */
13055
+ close: string;
13056
+ /** Accessible name for the fullscreen toggle while the dialog is windowed. */
13057
+ enterFullscreen: string;
13058
+ /** Accessible name for the fullscreen toggle while the dialog is fullscreen. */
13059
+ exitFullscreen: string;
13060
+ }
13061
+ /** English defaults used when no `TN_DIALOG_CHROME_LABELS` provider is registered. */
13062
+ declare const TN_DIALOG_DEFAULT_CHROME_LABELS: TnDialogChromeLabels;
13063
+ /**
13064
+ * DI token for app-wide dialog chrome labels. Provide either a static object or a
13065
+ * `Signal<TnDialogChromeLabels>` — the latter lets every dialog react to language changes when
13066
+ * the consumer wires it up to an i18n service.
13067
+ */
13068
+ declare const TN_DIALOG_CHROME_LABELS: InjectionToken<TnDialogChromeLabels | Signal<TnDialogChromeLabels>>;
13069
+ /**
13070
+ * The accessible name a dialog falls back to when it renders no `title` and the
13071
+ * caller named it through neither this component nor the `DialogConfig` (#219).
13072
+ *
13073
+ * `title` defaults to `''`, so the DEFAULT rendering of this component put an
13074
+ * empty `<h2>` in the header and left the CDK container with no naming
13075
+ * attribute at all — measured as `empty-heading` on the heading and
13076
+ * `aria-dialog-name` on the dialog. A dialog with no name is announced as
13077
+ * "dialog" and nothing else, which is the whole of what a screen-reader user is
13078
+ * told about a surface that just took over the page and trapped their focus.
13079
+ *
13080
+ * "Dialog" is a poor name, and says almost exactly what the role already says.
13081
+ * It is still better than the two alternatives: leaving the surface unnamed, or
13082
+ * withholding `role="dialog"` until there is a name — the latter would move a
13083
+ * listener into a focus trap with no announcement that anything had opened. So
13084
+ * it is paired with the dev-mode warning `tnAccessibleName` raises, which is
13085
+ * what keeps the fallback from becoming a quiet way to ship a nameless dialog.
13086
+ *
13087
+ * Exported so specs assert against it by name rather than by a copied literal.
13088
+ */
13089
+ declare const TN_DIALOG_SHELL_DEFAULT_LABEL = "Dialog";
10237
13090
  declare class TnDialogShellComponent implements OnInit {
13091
+ protected readonly labels: Signal<TnDialogChromeLabels>;
13092
+ /** Accessible name for the fullscreen toggle, following its current state. */
13093
+ protected readonly fullscreenLabel: Signal<string>;
10238
13094
  title: _angular_core.InputSignal<string>;
10239
13095
  showFullscreenButton: _angular_core.InputSignal<boolean>;
10240
13096
  /**
@@ -10260,6 +13116,22 @@ declare class TnDialogShellComponent implements OnInit {
10260
13116
  * provided (useful when more than one dialog can be open).
10261
13117
  */
10262
13118
  testId: _angular_core.InputSignal<TnTestIdValue>;
13119
+ /**
13120
+ * Accessible name for the dialog itself, for a dialog that renders no `title`.
13121
+ *
13122
+ * A `title` outranks it: the heading is what the user can see, and
13123
+ * `aria-labelledby` wins the ARIA name calculation while it resolves. The
13124
+ * attribute is still rendered beside the heading rather than suppressed — see
13125
+ * `tnAccessibleName`, which owns that rule for every component in this
13126
+ * library, and the reason it is safer than the alternative.
13127
+ */
13128
+ ariaLabel: _angular_core.InputSignal<string | null>;
13129
+ /**
13130
+ * IDREF naming the dialog from text elsewhere on the page, for a dialog that
13131
+ * renders no `title`. Same precedence: a `title` wins, because it is the
13132
+ * visible heading.
13133
+ */
13134
+ ariaLabelledby: _angular_core.InputSignal<string | null>;
10263
13135
  /**
10264
13136
  * The `testId` base normalized to a flat segment array. Nothing is dropped
10265
13137
  * here — `composeTestId` (via the `[tnTestId]` directive) filters falsy
@@ -10276,9 +13148,9 @@ declare class TnDialogShellComponent implements OnInit {
10276
13148
  * close-button ids and lets automation target "all close buttons" with one
10277
13149
  * selector.
10278
13150
  */
10279
- protected closeTestId: _angular_core.Signal<(string | number | null | undefined)[]>;
13151
+ protected closeTestId: Signal<(string | number | null | undefined)[]>;
10280
13152
  /** Role-first test-id segments for the fullscreen button: `button-fullscreen[-<base>]`. */
10281
- protected fullscreenTestId: _angular_core.Signal<(string | number | null | undefined)[]>;
13153
+ protected fullscreenTestId: Signal<(string | number | null | undefined)[]>;
10282
13154
  /** Stable id for the title heading, referenced by the dialog's aria-labelledby. */
10283
13155
  readonly titleId: string;
10284
13156
  isFullscreen: _angular_core.WritableSignal<boolean>;
@@ -10287,14 +13159,52 @@ declare class TnDialogShellComponent implements OnInit {
10287
13159
  private document;
10288
13160
  private host;
10289
13161
  private data;
13162
+ /**
13163
+ * Whether there is a heading to render, and to name the dialog from.
13164
+ *
13165
+ * Trimmed, because a whitespace-only title renders a heading that looks empty
13166
+ * to a sighted user and names the dialog with nothing — which is the state
13167
+ * this ticket fixed, arriving by a second route.
13168
+ */
13169
+ protected hasTitle: Signal<boolean>;
13170
+ /**
13171
+ * What the dialog is named by, in the order ARIA resolves: the visible
13172
+ * heading when there is one, then the caller's IDREF, then one passed to
13173
+ * `TnDialog.open` in the `DialogConfig`.
13174
+ *
13175
+ * The config is consulted so that the fallback below cannot overwrite a name
13176
+ * the opener supplied through CDK's own route. Reading it back through
13177
+ * `ref.config` rather than leaving CDK's binding to render it, because this
13178
+ * component writes both attributes onto that element and would otherwise
13179
+ * clear one it did not set.
13180
+ *
13181
+ * Guarded, even though `DialogRef.config` is non-optional: a shell rendered
13182
+ * directly in a consumer's test gets its `DialogRef` from a mock provider,
13183
+ * which supplies the methods but no config. Naming then falls back to the
13184
+ * component's own inputs, which is what such a test is exercising anyway.
13185
+ */
13186
+ private resolvedAriaLabelledby;
13187
+ /** An explicit label, from this component's input or from the `DialogConfig`. */
13188
+ private explicitAriaLabel;
13189
+ /**
13190
+ * The name to render as `aria-label`, or `null` to render none — and the
13191
+ * dev-mode warning when the dialog has no name from any route.
13192
+ *
13193
+ * Both halves live in `../a11y/accessible-name`, shared with `tn-side-panel`,
13194
+ * `tn-drawer` and the three progressbars, where the reasoning for each branch
13195
+ * is set out. `title` reaches it as the `ariaLabelledby` above, so a titled
13196
+ * dialog is named, takes no fallback and raises no warning.
13197
+ */
13198
+ private resolvedAriaLabel;
10290
13199
  constructor();
13200
+ private applyName;
10291
13201
  ngOnInit(): void;
10292
13202
  close(result?: unknown): void;
10293
13203
  toggleFullscreen(): void;
10294
13204
  private enterFullscreen;
10295
13205
  private exitFullscreen;
10296
13206
  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>;
13207
+ 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
13208
  }
10299
13209
 
10300
13210
  /**
@@ -10346,7 +13256,9 @@ declare class TnDialogHarness extends ComponentHarness {
10346
13256
  */
10347
13257
  static with(options?: DialogHarnessFilters): HarnessPredicate<TnDialogHarness>;
10348
13258
  /**
10349
- * Gets the dialog's title text.
13259
+ * Gets the dialog's title text, or `''` for a dialog opened with no title —
13260
+ * which renders no heading at all (#219), and is named by `ariaLabel` or
13261
+ * `ariaLabelledby` instead.
10350
13262
  *
10351
13263
  * @returns Promise resolving to the dialog title.
10352
13264
  *
@@ -10436,8 +13348,11 @@ declare class TnDialogHarness extends ComponentHarness {
10436
13348
  */
10437
13349
  toggleFullscreen(): Promise<void>;
10438
13350
  /**
10439
- * Whether the dialog is currently in fullscreen mode.
10440
- * Checks the aria-label of the fullscreen button.
13351
+ * Whether the dialog is currently in fullscreen mode, read from the fullscreen toggle's
13352
+ * `data-fullscreen`. Deliberately not derived from the button's `aria-label`: that label is
13353
+ * app-configurable through `TN_DIALOG_CHROME_LABELS`, so matching display text would report
13354
+ * `false` forever the moment a consumer translated the chrome — silently, in exactly the apps
13355
+ * the token exists for. Same call `TnTableHarness.getCardSortDirection` makes.
10441
13356
  *
10442
13357
  * @returns Promise resolving to true if fullscreen, false if not or if no fullscreen button.
10443
13358
  */
@@ -10488,6 +13403,53 @@ declare class TnDialogTesting {
10488
13403
  static rootLoader(fixture: ComponentFixture<unknown>): HarnessLoader;
10489
13404
  }
10490
13405
 
13406
+ /**
13407
+ * The accessible name an open panel falls back to when it has no `title` and the
13408
+ * caller named neither `ariaLabel` nor `ariaLabelledby` (#214).
13409
+ *
13410
+ * `title` defaults to `''`, so the DEFAULT rendering of this component was a
13411
+ * `role="dialog"` with `aria-labelledby` pointing at an empty `<h2>` — measured
13412
+ * as an `aria-dialog-name` violation, alongside `empty-heading`. A dialog with no
13413
+ * name is announced as "dialog" and nothing else, which is the whole of what a
13414
+ * screen-reader user gets told about a surface that just covered the page.
13415
+ *
13416
+ * Withholding `role="dialog"` until there is a name would be the other way to
13417
+ * clear the rule, and it is worse: the panel traps focus either way, so a
13418
+ * listener would be moved into a region with no announcement that anything had
13419
+ * opened. A generic name is still a poor one, so it is paired with the dev-mode
13420
+ * warning `tnAccessibleName` raises.
13421
+ *
13422
+ * Exported so specs assert against it by name rather than by a copied literal.
13423
+ */
13424
+ declare const TN_SIDE_PANEL_DEFAULT_LABEL = "Side panel";
13425
+ /**
13426
+ * The name given to the scrolling content region once it becomes focusable
13427
+ * (#248).
13428
+ *
13429
+ * A focusable element with no accessible name is announced as a bare "group",
13430
+ * which tells a listener that something has been reached and nothing about what
13431
+ * it is. It names the region rather than repeating the panel's own title: the
13432
+ * dialog announces that on entry, so a second copy of it here would say the
13433
+ * same words twice and still not distinguish the part that scrolls.
13434
+ *
13435
+ * Overridable through `contentAriaLabel`, on the same reasoning as
13436
+ * `closeButtonAriaLabel` — a string this library renders into a consumer's UI
13437
+ * has to be translatable. Exported so specs assert against it by name rather
13438
+ * than by a copied literal.
13439
+ */
13440
+ declare const TN_SIDE_PANEL_CONTENT_LABEL = "Panel content";
13441
+ /**
13442
+ * How far the content has to exceed the region before the region counts as
13443
+ * scrolling (#248).
13444
+ *
13445
+ * **This is now `TN_SCROLLABLE_REGION_TOLERANCE_PX`**, which is axe's own 13px
13446
+ * buffer and lives with the measurement it belongs to (#270). The alias stays
13447
+ * because it is what `side-panel-scrollable-content.spec.ts` pins against axe
13448
+ * from both sides, and that spec is a guard on this component rather than on
13449
+ * the helper — see `../a11y/scrollable-region.ts` for what the number is and
13450
+ * why it is copied from the rule at all.
13451
+ */
13452
+ declare const TN_SIDE_PANEL_OVERFLOW_TOLERANCE_PX = 13;
10491
13453
  /**
10492
13454
  * Directive to mark an element as a side-panel footer action.
10493
13455
  *
@@ -10517,12 +13479,49 @@ declare class TnSidePanelHeaderActionDirective {
10517
13479
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnSidePanelHeaderActionDirective, never>;
10518
13480
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnSidePanelHeaderActionDirective, "[tnSidePanelHeaderAction]", never, {}, {}, never, never, true, never>;
10519
13481
  }
13482
+ /**
13483
+ * A modal side panel: `role="dialog"` with `aria-modal="true"`, focus trapped
13484
+ * while it is open and restored to the opener when it closes.
13485
+ *
13486
+ * FOCUS ON OPEN
13487
+ * -------------
13488
+ * Opening moves focus to the panel container, whatever you projected into it,
13489
+ * so that a screen reader announces the dialog it has just entered before any
13490
+ * control in it. The first Tab then reaches the close button.
13491
+ *
13492
+ * **`[cdkFocusInitial]` is not honoured** (#227). It used to be, through the
13493
+ * CDK auto-capture this replaced, and `cdkTrapFocus` is still on the panel — so
13494
+ * the marker looks like it should work and does not. To focus a control of your
13495
+ * own, focus it yourself once the panel is open; the component leaves focus
13496
+ * alone as soon as it is inside the panel. `lib/a11y/initial-focus.ts` holds
13497
+ * the reasoning for capturing the container rather than a control.
13498
+ */
10520
13499
  declare class TnSidePanelComponent implements OnDestroy {
10521
13500
  private iconRegistry;
10522
13501
  private document;
10523
13502
  private destroyRef;
10524
13503
  private overlayRef;
13504
+ private panelRef;
13505
+ private contentRef;
10525
13506
  protected initialized: _angular_core.WritableSignal<boolean>;
13507
+ /**
13508
+ * Whether the content region carries the tab stop, its role and its name
13509
+ * (#248) — which is NOT the same question as whether it currently overflows.
13510
+ *
13511
+ * The measurement, the two observers that keep it current and the focus rule
13512
+ * that decides when the attributes may be taken off again are all
13513
+ * `tnScrollableRegion`'s (#270); this component decides only what to put on
13514
+ * the element, which is the part that differs between the five regions in
13515
+ * this library that scroll. `../a11y/scrollable-region.ts` sets out why the
13516
+ * answer is held true while the region has focus, and why `role` and
13517
+ * `aria-label` are gated on the same signal as `tabindex` rather than left on.
13518
+ *
13519
+ * Read in `afterNextRender`, which is where the helper takes its first
13520
+ * measurement — before the overlay is portaled to `<body>` below, which does
13521
+ * not affect it: `.tn-side-panel__overlay` is `position: fixed; inset: 0`, so
13522
+ * its size comes from the viewport rather than from its parent.
13523
+ */
13524
+ protected contentKeyboardReachable: _angular_core.Signal<boolean>;
10526
13525
  open: _angular_core.ModelSignal<boolean>;
10527
13526
  title: _angular_core.InputSignal<string>;
10528
13527
  width: _angular_core.InputSignal<string>;
@@ -10552,13 +13551,83 @@ declare class TnSidePanelComponent implements OnDestroy {
10552
13551
  * of context otherwise hears only "Dismiss".
10553
13552
  */
10554
13553
  closeButtonAriaLabel: _angular_core.InputSignal<string>;
13554
+ /**
13555
+ * Accessible name for the scrolling content region, which is named only while
13556
+ * it is focusable — see `TN_SIDE_PANEL_CONTENT_LABEL`. Override it to
13557
+ * translate it, or to say what the region holds ("Dataset properties").
13558
+ */
13559
+ contentAriaLabel: _angular_core.InputSignal<string>;
13560
+ /**
13561
+ * Accessible name for the panel itself, for a panel that renders no `title`.
13562
+ *
13563
+ * A `title` outranks it: the heading is what the user can see, and
13564
+ * `aria-labelledby` wins the ARIA name calculation while it resolves. The
13565
+ * attribute is still rendered beside the heading rather than suppressed — see
13566
+ * `tnAccessibleName`, which owns that rule for every component in this
13567
+ * library, and the reason it is safer than the alternative.
13568
+ */
13569
+ ariaLabel: _angular_core.InputSignal<string | null>;
13570
+ /**
13571
+ * IDREF naming the panel from text elsewhere on the page, for a panel that
13572
+ * renders no `title`. Same precedence: a `title` wins, because it is the
13573
+ * visible heading.
13574
+ */
13575
+ ariaLabelledby: _angular_core.InputSignal<string | null>;
13576
+ /**
13577
+ * Fires once the panel has finished opening.
13578
+ *
13579
+ * "Finished" means the open transition ended, OR that it was going to take
13580
+ * longer than `TN_TRANSITION_FALLBACK_MS` to say so — which is what a user
13581
+ * with `prefers-reduced-motion: reduce` gets, since this component's own
13582
+ * stylesheet zeroes the duration for them and a transition that does not run
13583
+ * fires no `transitionend` (#218). A consumer may assume the panel has
13584
+ * reached its open state and that `open()` is true; it may NOT assume the
13585
+ * animation is visually complete, because for that user there was none.
13586
+ */
10555
13587
  opened: _angular_core.OutputEmitterRef<void>;
13588
+ /**
13589
+ * Fires once the panel has finished closing. Same guarantee as `opened`, and
13590
+ * the same caveat: it reports the state, not the animation.
13591
+ *
13592
+ * Focus restoration does NOT hang off this — it happens as soon as the panel
13593
+ * closes (#214). See the effect in the constructor.
13594
+ */
10556
13595
  closed: _angular_core.OutputEmitterRef<void>;
10557
13596
  private actionContent;
10558
13597
  protected hasActions: _angular_core.Signal<boolean>;
10559
13598
  readonly panelId: string;
10560
13599
  readonly titleId: string;
13600
+ /**
13601
+ * Whether there is a heading to render, and to name the dialog from.
13602
+ *
13603
+ * Trimmed, because a whitespace-only title renders a heading that looks empty
13604
+ * to a sighted user and names the dialog with nothing — which is the state
13605
+ * that failed `aria-dialog-name` before #214, arriving by a second route.
13606
+ */
13607
+ protected hasTitle: _angular_core.Signal<boolean>;
13608
+ /**
13609
+ * What the dialog is named by, in the order ARIA resolves: the visible heading
13610
+ * when there is one, the caller's IDREF otherwise.
13611
+ */
13612
+ protected resolvedAriaLabelledby: _angular_core.Signal<string | null>;
13613
+ /**
13614
+ * The name to render as `aria-label`, or `null` to render none — and the
13615
+ * dev-mode warning when the panel has no name from any route.
13616
+ *
13617
+ * Both halves live in `../a11y/accessible-name`, shared with the three
13618
+ * progressbars, where the reasoning for each branch is set out. `title` reaches
13619
+ * it as the `ariaLabelledby` above, so a titled panel is named, takes no
13620
+ * fallback and raises no warning.
13621
+ */
13622
+ protected resolvedAriaLabel: _angular_core.Signal<string | null>;
10561
13623
  private previouslyFocusedElement;
13624
+ /**
13625
+ * Decides when an open or a close counts as finished, so that the outputs
13626
+ * above fire exactly once per change whether or not a transition ran. A field
13627
+ * initializer rather than the constructor, because it registers an `effect`
13628
+ * and so needs an injection context.
13629
+ */
13630
+ private lifecycle;
10562
13631
  constructor();
10563
13632
  ngOnDestroy(): void;
10564
13633
  protected dismiss(): void;
@@ -10568,7 +13637,7 @@ declare class TnSidePanelComponent implements OnDestroy {
10568
13637
  private restoreFocus;
10569
13638
  private registerMdiIcons;
10570
13639
  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>;
13640
+ 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
13641
  }
10573
13642
 
10574
13643
  interface SidePanelHarnessFilters extends BaseHarnessFilters {
@@ -10594,7 +13663,17 @@ declare class TnSidePanelHarness extends ComponentHarness {
10594
13663
  * Uses the data-tn-panel attribute to correlate the host with its overlay.
10595
13664
  */
10596
13665
  private getOverlay;
10597
- /** Get the panel title text. */
13666
+ /**
13667
+ * Get the panel title text, or `''` for a panel that renders no title.
13668
+ *
13669
+ * `locatorForOptional`, not `locatorFor`: since #214 a panel with no `title`
13670
+ * renders no heading element at all — an `<h2>` with nothing in it is an
13671
+ * `empty-heading` violation — and `locatorFor` throws on a selector that
13672
+ * matches nothing. That would turn `getTitle()` into an error rather than an
13673
+ * empty string, and would make `TnSidePanelHarness.with({title})` REJECT on an
13674
+ * untitled panel instead of simply not matching it, so a filter aimed at one
13675
+ * panel would fail on the presence of another.
13676
+ */
10598
13677
  getTitle(): Promise<string>;
10599
13678
  /** Whether the panel is currently open. */
10600
13679
  isOpen(): Promise<boolean>;
@@ -10618,6 +13697,17 @@ declare class TnStepComponent {
10618
13697
  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
13698
  }
10620
13699
 
13700
+ /**
13701
+ * Screen-reader text for a step whose header shows the error glyph.
13702
+ *
13703
+ * Exported so specs assert by name rather than by a copied literal, the same
13704
+ * reason `TN_SPINNER_DEFAULT_LABEL` is. It is not an override point — nothing
13705
+ * reads a token or an input in its place, so a consumer cannot change what the
13706
+ * stepper says here.
13707
+ */
13708
+ declare const TN_STEPPER_STATUS_ERROR = "Error";
13709
+ /** Screen-reader text for a step whose header shows the completed state. */
13710
+ declare const TN_STEPPER_STATUS_COMPLETED = "Completed";
10621
13711
  declare class TnStepperComponent {
10622
13712
  orientation: _angular_core.InputSignal<"auto" | "horizontal" | "vertical">;
10623
13713
  linear: _angular_core.InputSignal<boolean>;
@@ -10645,6 +13735,7 @@ declare class TnStepperComponent {
10645
13735
  isVertical: _angular_core.Signal<boolean>;
10646
13736
  readonly stepEditable: _angular_core.Signal<boolean[]>;
10647
13737
  readonly stepGated: _angular_core.Signal<boolean[]>;
13738
+ readonly stepStatusText: _angular_core.Signal<(string | null)[]>;
10648
13739
  selectStep(index: number): void;
10649
13740
  canSelectStep(index: number): boolean;
10650
13741
  next(): void;
@@ -11569,7 +14660,11 @@ declare enum TnToastPosition {
11569
14660
  Bottom = "bottom"
11570
14661
  }
11571
14662
  interface TnToastConfig {
11572
- /** Auto-dismiss duration in milliseconds. Default: 4000. Set to 0 to disable. */
14663
+ /**
14664
+ * How long the toast stays on screen, in milliseconds, counted from when it
14665
+ * appears rather than from the `open()` call. Default: 4000. Set to 0 to
14666
+ * disable auto-dismissal.
14667
+ */
11573
14668
  duration?: number;
11574
14669
  /** Visual style of the toast. Default: TnToastType.Info. */
11575
14670
  type?: TnToastType;
@@ -11591,12 +14686,46 @@ declare class TnToastComponent {
11591
14686
  position: _angular_core.WritableSignal<TnToastPosition>;
11592
14687
  visible: _angular_core.WritableSignal<boolean>;
11593
14688
  icon: _angular_core.Signal<string>;
14689
+ /**
14690
+ * The live-region role, which is also the only thing declaring how urgently the
14691
+ * toast is announced: `alert` implies `aria-live="assertive"` and `status`
14692
+ * implies `polite`.
14693
+ *
14694
+ * The template carried `role="alert"` and `aria-live="polite"` together (#190).
14695
+ * An explicit `aria-live` overrides the role's implicit one, so every toast was
14696
+ * announced politely — including `error`, the one type that needs to interrupt.
14697
+ * Deriving the role from the type and leaving `aria-live` off keeps a single
14698
+ * source: there is no second attribute left to disagree with this one.
14699
+ *
14700
+ * WHICH types get `alert` is shared with banner rather than decided here
14701
+ * (#194): #190 mapped `warning` to `status` while banner mapped it to
14702
+ * `alert`, and `../a11y/live-region.ts` is now the one place that answers it.
14703
+ */
14704
+ role: _angular_core.Signal<"alert" | "status">;
11594
14705
  onAction: () => void;
11595
14706
  onDismiss: () => void;
11596
14707
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnToastComponent, never>;
11597
14708
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnToastComponent, "tn-toast", never, {}, {}, never, never, true, never>;
11598
14709
  }
11599
14710
 
14711
+ /**
14712
+ * How long the live region is left attached and empty before the message is
14713
+ * written into it, in milliseconds.
14714
+ *
14715
+ * WHY A TIMER AND NOT THE ANIMATION FRAME THE TRANSITION RIDES
14716
+ * -----------------------------------------------------------
14717
+ * A `requestAnimationFrame` callback runs BEFORE that frame's style, layout and
14718
+ * accessibility-tree update. Attaching the region in one task and populating it
14719
+ * from the next frame's callback therefore commits both mutations in a single
14720
+ * accessibility-tree update — which is the already-populated insertion this
14721
+ * deferral exists to avoid, still there and harder to see. The region needs a
14722
+ * rendering pass of its own, which means yielding past one.
14723
+ *
14724
+ * 100ms is what `@angular/cdk`'s `LiveAnnouncer` waits before writing into its
14725
+ * own region — a dependency of this project already, and the closest thing to a
14726
+ * measured number available.
14727
+ */
14728
+ declare const TN_TOAST_ANNOUNCE_DELAY_MS = 100;
11600
14729
  declare class TnToastRef {
11601
14730
  private readonly _onAction;
11602
14731
  private readonly _afterDismissed;
@@ -11619,6 +14748,12 @@ declare class TnToastService {
11619
14748
  /**
11620
14749
  * Opens a toast notification.
11621
14750
  *
14751
+ * The toast is attached synchronously, but its message and its enter
14752
+ * transition both land `TN_TOAST_ANNOUNCE_DELAY_MS` later — the delay is what
14753
+ * makes the text a *change* to a live region a screen reader is already
14754
+ * watching. A test reading `.tn-toast__message` must let that elapse; one
14755
+ * asserting on the call rather than the DOM should use `TnToastMock`.
14756
+ *
11622
14757
  * @param message The message to display.
11623
14758
  * @param actionOrConfig Optional action button text, or config object.
11624
14759
  * @param config Optional config when action is provided as second arg.
@@ -12001,5 +15136,5 @@ declare const TN_THEME_DEFINITIONS: readonly TnThemeDefinition[];
12001
15136
  */
12002
15137
  declare const THEME_MAP: Map<TnTheme, TnThemeDefinition>;
12003
15138
 
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 };
15139
+ 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, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, getSelectableTypes, injectTnCalendarIntl, injectTnFormFieldAria, injectTnLabels, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, registerLucideIcons, resolveErrorMessage, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
15140
+ 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, 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 };