@truenas/ui-components 0.1.59 → 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truenas/ui-components",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public"
@@ -1,5 +1,5 @@
1
1
  import * as _angular_core from '@angular/core';
2
- import { ElementRef, OnDestroy, AfterViewInit, AfterContentInit, TemplateRef, Provider, ChangeDetectorRef, OnInit, InjectionToken, Signal, PipeTransform, ViewContainerRef, AfterViewChecked, ComponentRef } from '@angular/core';
2
+ import { ElementRef, OnDestroy, AfterViewInit, TemplateRef, AfterContentInit, Provider, ChangeDetectorRef, OnInit, InjectionToken, Signal, PipeTransform, ViewContainerRef, AfterViewChecked, ComponentRef } from '@angular/core';
3
3
  import { ControlValueAccessor, NgControl } from '@angular/forms';
4
4
  import { ComponentHarness, BaseHarnessFilters, HarnessPredicate, HarnessLoader } from '@angular/cdk/testing';
5
5
  import { SafeHtml, SafeResourceUrl, DomSanitizer } from '@angular/platform-browser';
@@ -524,7 +524,7 @@ interface BannerHarnessFilters extends BaseHarnessFilters {
524
524
  textContains?: string | RegExp;
525
525
  }
526
526
 
527
- declare class TnButtonComponent {
527
+ declare class TnButtonComponent implements AfterViewInit {
528
528
  size: string;
529
529
  primary: _angular_core.InputSignal<boolean>;
530
530
  color: _angular_core.InputSignal<"primary" | "secondary" | "warn" | "default">;
@@ -561,6 +561,9 @@ declare class TnButtonComponent {
561
561
  isRouterLink: _angular_core.Signal<boolean>;
562
562
  classes: _angular_core.Signal<string[]>;
563
563
  handleAnchorClick(event: MouseEvent): void;
564
+ private hostRef;
565
+ private innerRef;
566
+ ngAfterViewInit(): void;
564
567
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnButtonComponent, never>;
565
568
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnButtonComponent, "tn-button", never, { "primary": { "alias": "primary"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "backgroundColor": { "alias": "backgroundColor"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "href": { "alias": "href"; "required": false; "isSignal": true; }; "routerLink": { "alias": "routerLink"; "required": false; "isSignal": true; }; "queryParams": { "alias": "queryParams"; "required": false; "isSignal": true; }; "fragment": { "alias": "fragment"; "required": false; "isSignal": true; }; "target": { "alias": "target"; "required": false; "isSignal": true; }; "rel": { "alias": "rel"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, { "onClick": "onClick"; }, never, never, true, never>;
566
569
  }
@@ -714,7 +717,7 @@ declare class TnIconComponent {
714
717
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnIconComponent, "tn-icon", never, { "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; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "library": { "alias": "library"; "required": false; "isSignal": true; }; "fullSize": { "alias": "fullSize"; "required": false; "isSignal": true; }; "customSize": { "alias": "customSize"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
715
718
  }
716
719
 
717
- declare class TnIconButtonComponent {
720
+ declare class TnIconButtonComponent implements AfterViewInit {
718
721
  disabled: _angular_core.InputSignal<boolean>;
719
722
  ariaLabel: _angular_core.InputSignal<string | undefined>;
720
723
  /**
@@ -728,8 +731,17 @@ declare class TnIconButtonComponent {
728
731
  tooltip: _angular_core.InputSignal<string | undefined>;
729
732
  library: _angular_core.InputSignal<IconLibraryType | undefined>;
730
733
  onClick: _angular_core.OutputEmitterRef<MouseEvent>;
734
+ private hostRef;
735
+ private buttonRef;
731
736
  classes: _angular_core.Signal<string[]>;
732
737
  effectiveAriaLabel: _angular_core.Signal<string>;
738
+ /**
739
+ * Focuses the inner native `<button>`. Exposed as a public method so callers
740
+ * with a `TnIconButtonComponent` reference (e.g. `@ViewChild`) can focus it
741
+ * without reaching into the DOM themselves.
742
+ */
743
+ focus(options?: FocusOptions): void;
744
+ ngAfterViewInit(): void;
733
745
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnIconButtonComponent, never>;
734
746
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnIconButtonComponent, "tn-icon-button", never, { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "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; }; "library": { "alias": "library"; "required": false; "isSignal": true; }; }, { "onClick": "onClick"; }, never, never, true, never>;
735
747
  }
@@ -1298,6 +1310,68 @@ interface TnCardFooterLink {
1298
1310
  testId?: string;
1299
1311
  }
1300
1312
 
1313
+ /**
1314
+ * Projection-based menu item for use inside `<tn-menu>`.
1315
+ *
1316
+ * Two authoring modes:
1317
+ * 1. **Convenience** — set `label` (and optionally `icon`/`shortcut`); the
1318
+ * default icon + label + shortcut layout renders.
1319
+ * 2. **Custom content** — omit `label`; project arbitrary content via
1320
+ * `<ng-content>` (badges, two-line layouts, etc.).
1321
+ *
1322
+ * Existing `<tn-menu [items]="...">` consumers don't need this component;
1323
+ * the items-array API continues to work unchanged. Items-array entries and
1324
+ * projected `<tn-menu-item>` children render together inside one `<tn-menu>`,
1325
+ * share keyboard navigation, and look identical.
1326
+ *
1327
+ * **How it works:** the component itself renders nothing visible — it acts as
1328
+ * a configuration declaration. The parent `<tn-menu>` collects projected items
1329
+ * via `contentChildren`, re-renders them inside the CDK overlay alongside
1330
+ * items-array entries (with `cdkMenuItem` for full arrow-key navigation), and
1331
+ * routes clicks back to each item's `itemClick` output.
1332
+ *
1333
+ * **Accessibility:** because items are re-rendered inside the parent's
1334
+ * `CdkMenu`, all keyboard semantics from `@angular/cdk/menu` apply uniformly:
1335
+ * Arrow Up/Down/Home/End to move focus, Enter/Space to activate, Esc to
1336
+ * close, type-ahead search. Disabled items are skipped.
1337
+ *
1338
+ * **Limitation — custom content & keyboard nav:** the projected
1339
+ * `<ng-content>` (custom-content mode) is rendered inside a `<button
1340
+ * cdkMenuItem>` wrapper that owns Enter/Space/Arrow handling. Interactive
1341
+ * elements inside the projection (a nested `<button>`, link, toggle, etc.)
1342
+ * receive clicks but **do not** participate in CDK arrow-key navigation —
1343
+ * the wrapper button is the only keyboard-reachable target. Prefer
1344
+ * display-only content (icons, badges, two-line text); for cases that need
1345
+ * an extra interactive control, build a custom menu host with `cdkMenu`
1346
+ * directly instead of `<tn-menu>`.
1347
+ *
1348
+ * @example
1349
+ * ```html
1350
+ * <tn-menu>
1351
+ * <tn-menu-item label="JSON" [selected]="format === 'json'"
1352
+ * (itemClick)="setFormat('json')" />
1353
+ * <tn-menu-item label="CSV" [selected]="format === 'csv'"
1354
+ * (itemClick)="setFormat('csv')" />
1355
+ * </tn-menu>
1356
+ * ```
1357
+ */
1358
+ declare class TnMenuItemComponent {
1359
+ id: _angular_core.InputSignal<string | undefined>;
1360
+ label: _angular_core.InputSignal<string | undefined>;
1361
+ icon: _angular_core.InputSignal<string | undefined>;
1362
+ iconLibrary: _angular_core.InputSignal<"material" | "mdi" | "custom" | "lucide" | undefined>;
1363
+ shortcut: _angular_core.InputSignal<string | undefined>;
1364
+ disabled: _angular_core.InputSignal<boolean>;
1365
+ selected: _angular_core.InputSignal<boolean>;
1366
+ testId: _angular_core.InputSignal<string | undefined>;
1367
+ itemClick: _angular_core.OutputEmitterRef<MouseEvent>;
1368
+ /** Template capturing whatever the consumer projected as item content. */
1369
+ content: _angular_core.Signal<TemplateRef<unknown>>;
1370
+ resolvedTestId: _angular_core.Signal<string | undefined>;
1371
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnMenuItemComponent, never>;
1372
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnMenuItemComponent, "tn-menu-item", never, { "id": { "alias": "id"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "iconLibrary": { "alias": "iconLibrary"; "required": false; "isSignal": true; }; "shortcut": { "alias": "shortcut"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "selected": { "alias": "selected"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, { "itemClick": "itemClick"; }, never, ["*"], true, never>;
1373
+ }
1374
+
1301
1375
  /**
1302
1376
  * Activates CDK menu hover-to-open behavior for menus opened via custom overlays.
1303
1377
  *
@@ -1335,7 +1409,7 @@ interface TnMenuItem {
1335
1409
  */
1336
1410
  selected?: boolean;
1337
1411
  }
1338
- declare class TnMenuComponent {
1412
+ declare class TnMenuComponent implements OnDestroy {
1339
1413
  items: _angular_core.InputSignal<TnMenuItem[]>;
1340
1414
  contextMenu: _angular_core.InputSignal<boolean>;
1341
1415
  menuItemClick: _angular_core.OutputEmitterRef<TnMenuItem>;
@@ -1344,11 +1418,17 @@ declare class TnMenuComponent {
1344
1418
  menuTemplate: _angular_core.Signal<TemplateRef<unknown>>;
1345
1419
  contextMenuTemplate: _angular_core.Signal<TemplateRef<unknown>>;
1346
1420
  private contextOverlayRef?;
1421
+ private contextBackdropSub?;
1347
1422
  private overlay;
1348
1423
  private viewContainerRef;
1349
1424
  onMenuItemClick(item: TnMenuItem): void;
1350
- private contentItems;
1351
- constructor();
1425
+ contentItems: _angular_core.Signal<readonly TnMenuItemComponent[]>;
1426
+ /**
1427
+ * Click handler for projected `<tn-menu-item>` entries. Emits the item's own
1428
+ * `itemClick` output, re-emits a synthetic entry on `menuItemClick` so
1429
+ * trigger-driven menus close uniformly, and closes any open context menu.
1430
+ */
1431
+ onProjectedItemClick(item: TnMenuItemComponent, event: MouseEvent): void;
1352
1432
  hasChildren: _angular_core.Signal<(item: TnMenuItem) => boolean>;
1353
1433
  onMenuOpen(): void;
1354
1434
  onMenuClose(): void;
@@ -1358,10 +1438,11 @@ declare class TnMenuComponent {
1358
1438
  getMenuTemplate(): TemplateRef<unknown> | null;
1359
1439
  openContextMenuAt(x: number, y: number): void;
1360
1440
  private closeContextMenu;
1441
+ ngOnDestroy(): void;
1361
1442
  onContextMenu(event: MouseEvent): void;
1362
1443
  trackByItemId(index: number, item: TnMenuItem): string;
1363
1444
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnMenuComponent, never>;
1364
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnMenuComponent, "tn-menu", never, { "items": { "alias": "items"; "required": false; "isSignal": true; }; "contextMenu": { "alias": "contextMenu"; "required": false; "isSignal": true; }; }, { "menuItemClick": "menuItemClick"; "menuOpen": "menuOpen"; "menuClose": "menuClose"; }, ["contentItems"], ["*", "tn-menu-item, .tn-menu-separator"], true, never>;
1445
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnMenuComponent, "tn-menu", never, { "items": { "alias": "items"; "required": false; "isSignal": true; }; "contextMenu": { "alias": "contextMenu"; "required": false; "isSignal": true; }; }, { "menuItemClick": "menuItemClick"; "menuOpen": "menuOpen"; "menuClose": "menuClose"; }, ["contentItems"], ["*"], true, never>;
1365
1446
  }
1366
1447
 
1367
1448
  declare class TnCardComponent {
@@ -1394,6 +1475,7 @@ declare class TnCardComponent {
1394
1475
  private registerMdiIcons;
1395
1476
  classes: _angular_core.Signal<string[]>;
1396
1477
  hasHeader: _angular_core.Signal<boolean>;
1478
+ hasHeaderRight: _angular_core.Signal<boolean>;
1397
1479
  hasFooter: _angular_core.Signal<boolean>;
1398
1480
  onTitleClick(): void;
1399
1481
  onControlChange(checked: boolean): void;
@@ -2475,71 +2557,48 @@ interface TabsHarnessFilters extends BaseHarnessFilters {
2475
2557
  hasTab?: string | RegExp;
2476
2558
  }
2477
2559
 
2478
- /**
2479
- * Projection-based menu item for use inside `<tn-menu>`.
2480
- *
2481
- * Two authoring modes:
2482
- * 1. **Convenience** — set `label` (and optionally `icon`/`shortcut`); the
2483
- * default icon + label + shortcut layout renders.
2484
- * 2. **Custom content** — omit `label`; project arbitrary content via
2485
- * `<ng-content>` (badges, two-line layouts, etc.).
2486
- *
2487
- * Existing `<tn-menu [items]="...">` consumers don't need this component;
2488
- * the items-array API continues to work unchanged. Items-array entries and
2489
- * projected `<tn-menu-item>` children render together inside one `<tn-menu>`.
2490
- *
2491
- * Subscribe to either the projected item's own `itemClick` output (preferred,
2492
- * per-item handlers) or the parent menu's `menuItemClick` (uniform handler).
2493
- * Trigger-driven menus close automatically on projected-item click.
2494
- *
2495
- * **Note on keyboard navigation:** projected items render as `role="menuitem"`
2496
- * buttons but do not participate in `CdkMenu` arrow-key navigation (CdkMenuItem
2497
- * requires its parent `CdkMenu` in the same injector tree, which projection
2498
- * breaks). For menus that depend on arrow-key navigation between options, use
2499
- * the `items` input form. Tab/Shift+Tab and Enter/Space activation still work.
2500
- *
2501
- * @example
2502
- * ```html
2503
- * <tn-menu>
2504
- * <tn-menu-item label="JSON" [selected]="format === 'json'"
2505
- * (itemClick)="setFormat('json')" />
2506
- * <tn-menu-item label="CSV" [selected]="format === 'csv'"
2507
- * (itemClick)="setFormat('csv')" />
2508
- * </tn-menu>
2509
- * ```
2510
- */
2511
- declare class TnMenuItemComponent {
2512
- id: _angular_core.InputSignal<string | undefined>;
2513
- label: _angular_core.InputSignal<string | undefined>;
2514
- icon: _angular_core.InputSignal<string | undefined>;
2515
- iconLibrary: _angular_core.InputSignal<"material" | "mdi" | "custom" | "lucide" | undefined>;
2516
- shortcut: _angular_core.InputSignal<string | undefined>;
2517
- disabled: _angular_core.InputSignal<boolean>;
2518
- selected: _angular_core.InputSignal<boolean>;
2519
- testId: _angular_core.InputSignal<string | undefined>;
2520
- itemClick: _angular_core.OutputEmitterRef<MouseEvent>;
2521
- resolvedTestId: _angular_core.Signal<string | undefined>;
2522
- handleClick(event: MouseEvent): void;
2523
- static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnMenuItemComponent, never>;
2524
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnMenuItemComponent, "tn-menu-item", never, { "id": { "alias": "id"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "iconLibrary": { "alias": "iconLibrary"; "required": false; "isSignal": true; }; "shortcut": { "alias": "shortcut"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "selected": { "alias": "selected"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, { "itemClick": "itemClick"; }, never, ["*"], true, never>;
2525
- }
2526
-
2527
2560
  /**
2528
2561
  * Directive that attaches a menu to any element.
2529
2562
  * Usage: <button [tnMenuTriggerFor]="menu">Open Menu</button>
2530
2563
  */
2531
- declare class TnMenuTriggerDirective {
2564
+ declare class TnMenuTriggerDirective implements OnDestroy {
2532
2565
  menu: _angular_core.InputSignal<TnMenuComponent>;
2533
2566
  tnMenuPosition: _angular_core.InputSignal<"after" | "before" | "above" | "below">;
2534
2567
  private overlayRef?;
2535
2568
  private isMenuOpen;
2536
2569
  private itemClickSub?;
2570
+ /**
2571
+ * RxJS subscriptions for the current overlay's `backdropClick()` and
2572
+ * `keydownEvents()`. Each `openMenu()` creates a fresh overlay (and fresh
2573
+ * subscriptions), so we collect them here and tear them down in `closeMenu`
2574
+ * / `ngOnDestroy` to keep open→close cycles leak-free.
2575
+ */
2576
+ private overlaySubs;
2537
2577
  private elementRef;
2538
2578
  private overlay;
2539
2579
  private viewContainerRef;
2540
2580
  onClick(): void;
2581
+ onArrowDown(event: Event): void;
2541
2582
  openMenu(): void;
2542
2583
  closeMenu(): void;
2584
+ ngOnDestroy(): void;
2585
+ private disposeOverlaySubs;
2586
+ /**
2587
+ * Return focus to the trigger element so keyboard users land somewhere
2588
+ * sensible after the menu closes (Escape, item click, or backdrop click).
2589
+ *
2590
+ * The host might be a custom-element wrapper like `<tn-button>` /
2591
+ * `<tn-icon-button>` whose host element isn't itself focusable — focus
2592
+ * lives on the inner `<button>` or `<a>`. We focus the host if it's
2593
+ * focusable; otherwise we drill into the first focusable descendant.
2594
+ *
2595
+ * Passes `focusVisible: true` (Chrome/Firefox) so the `:focus-visible`
2596
+ * outline reappears on the restored trigger — without it, browsers treat a
2597
+ * programmatic `.focus()` as non-keyboard and skip the focus ring, which
2598
+ * looks to users like focus has been lost. Safari ignores the option and
2599
+ * falls back to its heuristic; that's acceptable.
2600
+ */
2601
+ private restoreFocusToTrigger;
2543
2602
  private getPositions;
2544
2603
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnMenuTriggerDirective, never>;
2545
2604
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TnMenuTriggerDirective, "[tnMenuTriggerFor]", ["tnMenuTrigger"], { "menu": { "alias": "tnMenuTriggerFor"; "required": true; "isSignal": true; }; "tnMenuPosition": { "alias": "tnMenuPosition"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
@@ -2929,7 +2988,7 @@ interface TnSelectOptionGroup<T = unknown> {
2929
2988
  options: TnSelectOption<T>[];
2930
2989
  disabled?: boolean;
2931
2990
  }
2932
- declare class TnSelectComponent<T = unknown> implements ControlValueAccessor {
2991
+ declare class TnSelectComponent<T = unknown> implements ControlValueAccessor, OnDestroy {
2933
2992
  options: _angular_core.InputSignal<TnSelectOption<T>[]>;
2934
2993
  optionGroups: _angular_core.InputSignal<TnSelectOptionGroup<T>[]>;
2935
2994
  placeholder: _angular_core.InputSignal<string>;
@@ -2956,6 +3015,11 @@ declare class TnSelectComponent<T = unknown> implements ControlValueAccessor {
2956
3015
  * fallback uses `JSON.stringify`, which is key-order dependent and can
2957
3016
  * produce false negatives for structurally equal objects. For primitives the
2958
3017
  * default identity check is fine.
3018
+ *
3019
+ * @example
3020
+ * ```ts
3021
+ * compareWith = (a, b) => a?.id === b?.id;
3022
+ * ```
2959
3023
  */
2960
3024
  compareWith: _angular_core.InputSignal<((a: T | null, b: T | null) => boolean) | undefined>;
2961
3025
  selectionChange: _angular_core.OutputEmitterRef<T>;
@@ -2968,103 +3032,115 @@ declare class TnSelectComponent<T = unknown> implements ControlValueAccessor {
2968
3032
  /** Index into `flatOptions` of the keyboard-focused row (-1 when none). */
2969
3033
  protected focusedIndex: _angular_core.WritableSignal<number>;
2970
3034
  private formDisabled;
2971
- private static readonly DROPDOWN_MAX_HEIGHT_VAR;
2972
- private static readonly DROPDOWN_MAX_HEIGHT_FALLBACK;
2973
- private readonly fallbackId;
2974
- protected uniqueId: _angular_core.Signal<string>;
2975
- isDisabled: _angular_core.Signal<boolean>;
3035
+ private static instanceCounter;
3036
+ private instanceId;
2976
3037
  /**
2977
- * Flattened option list (ungrouped + grouped, in render order). The keyboard
2978
- * navigation walks this list entries from disabled groups are kept but
2979
- * marked disabled so the cursor skips over them correctly.
3038
+ * Id namespace used by all DOM ids the template emits (dropdown panel,
3039
+ * option rows, group labels). Prefers `testId` when set so tests can target
3040
+ * specific instances; otherwise falls back to a per-instance counter so two
3041
+ * `<tn-select>`s on the same page never collide on `aria-controls`/group ids.
2980
3042
  */
2981
- protected flatOptions: _angular_core.Signal<TnSelectOption<T>[]>;
3043
+ protected idNamespace: _angular_core.Signal<string>;
3044
+ isDisabled: _angular_core.Signal<boolean>;
2982
3045
  /**
2983
- * Starting flat-index of each option group, used by the template to
2984
- * translate a (group, option) pair into the matching `flatOptions` index.
3046
+ * Selectable, non-disabled options in display order (regular options first,
3047
+ * then groups). Used by keyboard navigation so we can skip disabled
3048
+ * entries and group headers without a separate filter pass.
2985
3049
  */
2986
- protected groupOffsets: _angular_core.Signal<number[]>;
2987
- /** `aria-activedescendant` id for the focused option (or null). */
2988
- protected activeOptionId: _angular_core.Signal<string | null>;
3050
+ navigableOptions: _angular_core.Signal<{
3051
+ option: TnSelectOption<T>;
3052
+ id: string;
3053
+ }[]>;
3054
+ /** Stable DOM id of the currently-highlighted option, for aria-activedescendant. */
3055
+ focusedOptionId: _angular_core.Signal<string | null>;
3056
+ /** Stable DOM id for an option; matches what navigableOptions() assigns. */
3057
+ optionId(option: TnSelectOption<T>): string | null;
3058
+ /** Whether `option` is the keyboard-highlighted item. */
3059
+ isOptionFocused(option: TnSelectOption<T>): boolean;
2989
3060
  private onChange;
2990
3061
  private onTouched;
2991
3062
  private elementRef;
2992
3063
  private cdr;
2993
- protected triggerEl: _angular_core.Signal<ElementRef<HTMLElement> | undefined>;
2994
- constructor();
3064
+ private overlay;
3065
+ private viewContainerRef;
3066
+ private triggerEl;
3067
+ private dropdownTemplate;
3068
+ private overlayRef?;
3069
+ private overlaySubs;
2995
3070
  writeValue(value: T | T[] | null): void;
2996
3071
  registerOnChange(fn: (value: T | T[] | null) => void): void;
2997
3072
  registerOnTouched(fn: () => void): void;
2998
3073
  setDisabledState(isDisabled: boolean): void;
2999
3074
  toggleDropdown(): void;
3075
+ openDropdown(): void;
3000
3076
  /**
3001
- * Open the dropdown, seed the keyboard cursor on the currently-selected
3002
- * option (or the first focusable one), and decide whether to flip up.
3003
- */
3004
- private openDropdown;
3005
- /**
3006
- * Close the dropdown.
3077
+ * Attach the dropdown panel as a CDK overlay anchored to the trigger.
3007
3078
  *
3008
- * @param restoreFocus When `true` (default), return focus to the trigger so
3009
- * keyboard users land somewhere sensible. Pass `false` for click-outside
3010
- * so we don't steal focus from the element the user just navigated to.
3079
+ * Why an overlay (vs. an inline absolutely-positioned panel):
3080
+ * - Escapes parent `overflow: hidden`/clipping in surrounding layouts.
3081
+ * - `outsidePointerEvents()` notifies on outside pointerdown WITHOUT
3082
+ * intercepting the click (no backdrop) — so the user's click reaches
3083
+ * the underlying target while the select closes silently.
3084
+ * - Position is recomputed on scroll so the panel stays attached.
3085
+ * - Width is matched to the trigger so the panel doesn't jump in size.
3011
3086
  */
3012
- closeDropdown(options?: {
3013
- restoreFocus?: boolean;
3014
- }): void;
3015
- /** Picks the initial focused-row index when the dropdown opens. */
3016
- private initialFocusIndex;
3017
- /**
3018
- * Decide whether the dropdown should open above or below the trigger.
3019
- * Opens above when there isn't enough space below the trigger AND there is
3020
- * more space above — otherwise stays below. Falls back to `'below'` when no
3021
- * trigger element is found yet.
3022
- */
3023
- private computeDropdownPosition;
3087
+ private attachOverlay;
3088
+ private detachOverlay;
3089
+ ngOnDestroy(): void;
3024
3090
  /**
3025
- * Reads the dropdown's max-height from the CSS custom property set in
3026
- * select.component.scss. Single source of truth for the flip-up threshold —
3027
- * if the stylesheet changes, the heuristic follows automatically.
3091
+ * Closes the dropdown.
3092
+ *
3093
+ * @param restoreFocus When `true` (the default), returns focus to the
3094
+ * trigger. Used for explicit closes — Escape, Enter/Space activation,
3095
+ * option click — where the user is still interacting with the select.
3096
+ * Pass `false` for click-outside / blur paths so we don't steal focus
3097
+ * from the element the user actually navigated to.
3028
3098
  */
3029
- private readDropdownMaxHeight;
3099
+ closeDropdown(restoreFocus?: boolean): void;
3030
3100
  onOptionClick(option: TnSelectOption<T>, groupDisabled?: boolean): void;
3031
3101
  selectOption(option: TnSelectOption<T>): void;
3032
3102
  private toggleOption;
3033
3103
  isOptionSelected(option: TnSelectOption<T>): boolean;
3034
- /** Build a stable DOM id for the option at `index` for aria-activedescendant. */
3035
- protected optionId(index: number): string;
3036
3104
  protected displayText: _angular_core.Signal<string>;
3037
3105
  private findOptionByValue;
3038
- protected anyOptionsPresent: _angular_core.Signal<boolean>;
3106
+ protected hasAnyOptions: _angular_core.Signal<boolean>;
3107
+ /** One-shot guard so the object-compare warning fires at most once per instance. */
3108
+ private warnedAboutObjectCompare;
3039
3109
  /**
3040
- * Compares two option values for equality. Uses `compareWith` if provided,
3041
- * otherwise identity (`===`). For object values it falls back to
3042
- * `JSON.stringify`, which is key-order dependent consumers with object
3043
- * values should provide `compareWith` to avoid subtle bugs.
3110
+ * Compares two option values for equality.
3111
+ *
3112
+ * - Uses `compareWith` when provided (the supported path for object values).
3113
+ * - Falls back to strict identity (`===`) — adequate for primitives.
3114
+ * - For object values WITHOUT `compareWith` we return `false` (no
3115
+ * structural compare) and emit a one-time warning. The previous
3116
+ * `JSON.stringify` fallback was key-order sensitive and produced silent
3117
+ * false-negatives that were hard to diagnose; returning `false` makes the
3118
+ * misuse loud (selection won't match) and the warning points to the fix.
3119
+ *
3120
+ * The warning is **unconditional** (not gated on `isDevMode()`) so prod
3121
+ * monitoring picks it up — consumers relying on the old stringify fallback
3122
+ * would otherwise see selections silently stop matching after upgrade with
3123
+ * no signal in production logs.
3044
3124
  */
3045
3125
  private compareValues;
3046
3126
  /**
3047
- * Keyboard handling on the trigger (focus stays on the trigger while the
3048
- * dropdown is open — options use mousedown-preventDefault to avoid stealing
3049
- * it). Implements the WAI-ARIA combobox pattern subset we need:
3127
+ * Keyboard navigation for the combobox trigger.
3050
3128
  *
3051
- * - **Enter / Space**: open closed dropdown, or select the focused row
3052
- * (toggle in multi-mode).
3053
- * - **ArrowDown / ArrowUp**: move the focused row; opens the dropdown first
3054
- * if it's closed.
3055
- * - **Home / End**: jump to first / last focusable row (when open).
3056
- * - **Escape**: close and restore focus to the trigger.
3057
- * - **Tab**: close without preventing default so focus moves to the next
3058
- * element naturally.
3129
+ * - **ArrowDown / ArrowUp** opens the dropdown if closed; otherwise moves
3130
+ * the keyboard-focus highlight (via aria-activedescendant) up/down,
3131
+ * skipping disabled options and group headers.
3132
+ * - **Home / End** jump to the first / last enabled option.
3133
+ * - **Enter / Space** opens the dropdown if closed; if open and an option
3134
+ * is highlighted, selects that option (in single mode) or toggles it
3135
+ * (in multiple mode).
3136
+ * - **Escape** closes the dropdown without changing the selection.
3137
+ *
3138
+ * All navigation keys call `event.preventDefault()` so the page does not
3139
+ * scroll while the user is moving through options.
3059
3140
  */
3060
3141
  onKeydown(event: KeyboardEvent): void;
3061
- /** Step the focused row by ±1 (or more), skipping disabled options. */
3062
3142
  private moveFocus;
3063
- /** Move focus to a specific index, scanning forward/backward to skip disabled. */
3064
- private moveFocusTo;
3065
- /** Select (or toggle, in multi-mode) the currently keyboard-focused row. */
3066
- private selectFocused;
3067
- /** Scrolls the keyboard-focused option into view if it's outside the dropdown's viewport. */
3143
+ private activateFocusedOption;
3068
3144
  private scrollFocusedIntoView;
3069
3145
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnSelectComponent<any>, never>;
3070
3146
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnSelectComponent<any>, "tn-select", never, { "options": { "alias": "options"; "required": false; "isSignal": true; }; "optionGroups": { "alias": "optionGroups"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "noOptionsLabel": { "alias": "noOptionsLabel"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "multiple": { "alias": "multiple"; "required": false; "isSignal": true; }; "compareWith": { "alias": "compareWith"; "required": false; "isSignal": true; }; }, { "selectionChange": "selectionChange"; "multiSelectionChange": "multiSelectionChange"; }, never, never, true, never>;
@@ -3869,8 +3945,49 @@ declare class TnTableComponent<T = unknown> implements OnInit {
3869
3945
  selectable: _angular_core.InputSignal<boolean>;
3870
3946
  expandable: _angular_core.InputSignal<boolean>;
3871
3947
  bordered: _angular_core.InputSignal<boolean>;
3948
+ /**
3949
+ * Marks a single row as "active" — adds the `tn-table__row--active` class
3950
+ * and a left-side indicator bar. Set to `null` (default) to clear.
3951
+ *
3952
+ * **Matched by object identity (`===`)** against the row references in
3953
+ * `dataSource`. Pass the exact reference you got from the data source (e.g.
3954
+ * via the `rowClick` event or a lookup into `dataSource()`), not a
3955
+ * structurally-equal copy — `{ id: 1 } !== { id: 1 }` and the row will not
3956
+ * highlight. This differs from `tn-select`, which supports a `compareWith`
3957
+ * input for object values; the table intentionally does not, because the
3958
+ * common use case (clicking a row to mark it active) already gives the
3959
+ * caller the original reference. If you need structural equality, look up
3960
+ * the row by id in your data source before assigning here.
3961
+ */
3962
+ activeRow: _angular_core.InputSignal<T | null>;
3963
+ /**
3964
+ * Overrides the active-row background color. Accepts any CSS color value
3965
+ * (`#hex`, `rgb()`, `var(--token)`). Defaults to `--tn-bg3` when null.
3966
+ */
3967
+ activeBg: _angular_core.InputSignal<string | null>;
3968
+ /**
3969
+ * Overrides the left-side active-row indicator color. Defaults to
3970
+ * `--tn-primary` when null.
3971
+ */
3972
+ activeIndicator: _angular_core.InputSignal<string | null>;
3973
+ /**
3974
+ * When true, shows a spinner overlay over the table. Existing rows remain
3975
+ * visible (dimmed) so reloads don't cause layout jumps; if there are no rows
3976
+ * yet, the spinner replaces the empty state.
3977
+ */
3978
+ loading: _angular_core.InputSignal<boolean>;
3979
+ /** Accessible label announced while loading. */
3980
+ loadingMessage: _angular_core.InputSignal<string>;
3981
+ /**
3982
+ * When true, rows become keyboard-focusable (tabindex=0) and clicking or
3983
+ * pressing Enter/Space emits `rowClick`. Use this for "click row to view
3984
+ * details" patterns. Independent of `selectable` (checkbox) and `expandable`.
3985
+ */
3986
+ clickable: _angular_core.InputSignal<boolean>;
3872
3987
  sortChange: _angular_core.OutputEmitterRef<TnSortEvent>;
3873
3988
  selectionChange: _angular_core.OutputEmitterRef<T[]>;
3989
+ /** Emits the row when a clickable row is activated (click or Enter/Space). */
3990
+ rowClick: _angular_core.OutputEmitterRef<T>;
3874
3991
  columnDefs: _angular_core.Signal<readonly TnTableColumnDirective[]>;
3875
3992
  detailRowDef: _angular_core.Signal<TnDetailRowDefDirective | undefined>;
3876
3993
  sortColumn: _angular_core.WritableSignal<string>;
@@ -3899,13 +4016,16 @@ declare class TnTableComponent<T = unknown> implements OnInit {
3899
4016
  isSorted(column: string): boolean;
3900
4017
  toggleRowExpansion(row: T): void;
3901
4018
  isRowExpanded(row: T): boolean;
4019
+ isRowActive(row: T): boolean;
4020
+ onRowClick(row: T): void;
4021
+ onRowKeydown(event: KeyboardEvent, row: T): void;
3902
4022
  toggleSelectAll(): void;
3903
4023
  toggleRowSelection(row: T): void;
3904
4024
  isRowSelected(row: T): boolean;
3905
4025
  getColumnDef(columnName: string): TnTableColumnDirective | undefined;
3906
4026
  getCellValue(row: T, column: string): unknown;
3907
4027
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnTableComponent<any>, never>;
3908
- 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; }; "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; }; }, { "sortChange": "sortChange"; "selectionChange": "selectionChange"; }, ["columnDefs", "detailRowDef"], never, true, [{ directive: typeof TnTestIdDirective; inputs: { "tnTestId": "testId"; }; outputs: {}; }]>;
4028
+ 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; }; "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; }; "activeRow": { "alias": "activeRow"; "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; }; }, { "sortChange": "sortChange"; "selectionChange": "selectionChange"; "rowClick": "rowClick"; }, ["columnDefs", "detailRowDef"], never, true, [{ directive: typeof TnTestIdDirective; inputs: { "tnTestId": "testId"; }; outputs: {}; }]>;
3909
4029
  }
3910
4030
 
3911
4031
  /**
@@ -4023,6 +4143,44 @@ declare class TnTableHarness extends ComponentHarness {
4023
4143
  * @returns Promise resolving to true if the row has the expanded class.
4024
4144
  */
4025
4145
  isRowExpanded(rowIndex: number): Promise<boolean>;
4146
+ /**
4147
+ * Clicks a row (for tables with `clickable` enabled).
4148
+ *
4149
+ * @param rowIndex Zero-based index of the data row.
4150
+ */
4151
+ clickRow(rowIndex: number): Promise<void>;
4152
+ /**
4153
+ * Sends a keyboard event to a row (Enter/Space activate clickable rows).
4154
+ *
4155
+ * @param rowIndex Zero-based index of the data row.
4156
+ * @param key Which key to press — Enter or Space.
4157
+ */
4158
+ pressKeyOnRow(rowIndex: number, key: 'enter' | 'space'): Promise<void>;
4159
+ /**
4160
+ * Checks if a row is keyboard-focusable (tabindex=0).
4161
+ *
4162
+ * @param rowIndex Zero-based index of the data row.
4163
+ */
4164
+ isRowFocusable(rowIndex: number): Promise<boolean>;
4165
+ /**
4166
+ * Checks whether the table is currently in the loading state.
4167
+ *
4168
+ * @returns Promise resolving to true if the loading overlay is visible.
4169
+ */
4170
+ isLoading(): Promise<boolean>;
4171
+ /**
4172
+ * Checks if a data row is currently marked active.
4173
+ *
4174
+ * @param rowIndex Zero-based index of the data row.
4175
+ * @returns Promise resolving to true if the row has the active class.
4176
+ */
4177
+ isRowActive(rowIndex: number): Promise<boolean>;
4178
+ /**
4179
+ * Gets the index of the currently active row, or null if none is active.
4180
+ *
4181
+ * @returns Promise resolving to the active row index or null.
4182
+ */
4183
+ getActiveRowIndex(): Promise<number | null>;
4026
4184
  /**
4027
4185
  * Gets the text content of an expanded detail row.
4028
4186
  *
@@ -5272,6 +5430,22 @@ declare class TnButtonToggleGroupComponent implements ControlValueAccessor {
5272
5430
  name: _angular_core.InputSignal<string>;
5273
5431
  ariaLabel: _angular_core.InputSignal<string>;
5274
5432
  ariaLabelledby: _angular_core.InputSignal<string>;
5433
+ /**
5434
+ * Overrides the background color of checked toggles in this group. Accepts
5435
+ * any CSS color value (`#hex`, `rgb()`, `var(--token)`, etc.). When null
5436
+ * (default), falls back to `--tn-alt-bg2`.
5437
+ */
5438
+ checkedBg: _angular_core.InputSignal<string | null>;
5439
+ /**
5440
+ * Overrides the text color of checked toggles in this group. Defaults to
5441
+ * `--tn-fg1` when null.
5442
+ */
5443
+ checkedColor: _angular_core.InputSignal<string | null>;
5444
+ /**
5445
+ * Overrides the border color of checked toggles in this group. Defaults to
5446
+ * `--tn-lines` when null.
5447
+ */
5448
+ checkedBorder: _angular_core.InputSignal<string | null>;
5275
5449
  /**
5276
5450
  * Test-id applied to the group root. Rendered under whichever attribute name
5277
5451
  * is configured via `TN_TEST_ATTR` (default `data-testid`).
@@ -5298,7 +5472,7 @@ declare class TnButtonToggleGroupComponent implements ControlValueAccessor {
5298
5472
  private updateTogglesFromValue;
5299
5473
  private updateTogglesFromValues;
5300
5474
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnButtonToggleGroupComponent, never>;
5301
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnButtonToggleGroupComponent, "tn-button-toggle-group", never, { "multiple": { "alias": "multiple"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "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; }; }, { "change": "change"; }, ["buttonToggles"], ["*"], true, never>;
5475
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnButtonToggleGroupComponent, "tn-button-toggle-group", never, { "multiple": { "alias": "multiple"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "ariaLabelledby"; "required": false; "isSignal": true; }; "checkedBg": { "alias": "checkedBg"; "required": false; "isSignal": true; }; "checkedColor": { "alias": "checkedColor"; "required": false; "isSignal": true; }; "checkedBorder": { "alias": "checkedBorder"; "required": false; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, { "change": "change"; }, ["buttonToggles"], ["*"], true, never>;
5302
5476
  }
5303
5477
 
5304
5478
  declare class TnButtonToggleComponent implements ControlValueAccessor {