@truenas/ui-components 0.4.11 → 0.5.0
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.
|
@@ -6,7 +6,7 @@ import * as i1$3 from '@angular/forms';
|
|
|
6
6
|
import { NgControl, NG_VALUE_ACCESSOR, FormsModule, Validators } from '@angular/forms';
|
|
7
7
|
import { ComponentHarness, HarnessPredicate, TestKey, parallel } from '@angular/cdk/testing';
|
|
8
8
|
import * as i1 from '@angular/cdk/a11y';
|
|
9
|
-
import { A11yModule, FocusMonitor, _IdGenerator } from '@angular/cdk/a11y';
|
|
9
|
+
import { A11yModule, AriaDescriber, FocusMonitor, _IdGenerator } from '@angular/cdk/a11y';
|
|
10
10
|
import * as i1$2 from '@angular/common';
|
|
11
11
|
import { DOCUMENT, NgTemplateOutlet, CommonModule, AsyncPipe, isPlatformBrowser } from '@angular/common';
|
|
12
12
|
import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiOpenInNew, mdiHelpCircle, mdiDotsVertical, mdiClose } from '@mdi/js';
|
|
@@ -2589,6 +2589,22 @@ class TnButtonComponent {
|
|
|
2589
2589
|
this.onClick.emit(event);
|
|
2590
2590
|
}
|
|
2591
2591
|
hostRef = inject((ElementRef));
|
|
2592
|
+
constructor() {
|
|
2593
|
+
// A disabled native <button> suppresses its own click events, but its
|
|
2594
|
+
// pointer-events: none style retargets clicks over it to this host element —
|
|
2595
|
+
// where consumers commonly bind (click). Halt those here so a disabled
|
|
2596
|
+
// tn-button is inert for every consumer, matching what handleAnchorClick
|
|
2597
|
+
// does for the anchor variants. Registered in the constructor (before any
|
|
2598
|
+
// template listener on the host) and capture-phase, so it runs first and
|
|
2599
|
+
// stopImmediatePropagation() reliably blocks host (click) handlers.
|
|
2600
|
+
const host = this.hostRef.nativeElement;
|
|
2601
|
+
host.addEventListener('click', (event) => {
|
|
2602
|
+
if (this.disabled()) {
|
|
2603
|
+
event.preventDefault();
|
|
2604
|
+
event.stopImmediatePropagation();
|
|
2605
|
+
}
|
|
2606
|
+
}, { capture: true });
|
|
2607
|
+
}
|
|
2592
2608
|
// The template renders exactly one of `<a [routerLink]>`, `<a [href]>`, or
|
|
2593
2609
|
// `<button>` via `@if/@else`, and each carries the `#button` ref — so this
|
|
2594
2610
|
// resolves to whichever variant is active. Using a viewChild instead of a
|
|
@@ -2626,7 +2642,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
2626
2642
|
args: [{ selector: 'tn-button', standalone: true, imports: [CommonModule, RouterLink, TnTestIdDirective, LabelMarkupPipe, TnIconComponent], host: {
|
|
2627
2643
|
'[class.tn-button--full-width]': 'fullWidth()',
|
|
2628
2644
|
}, template: "<ng-template #content>\n @if (icon() && iconPosition() === 'left') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n <span class=\"storybook-button__label\" [innerHTML]=\"label() | tnLabelMarkup\"></span>\n @if (icon() && iconPosition() === 'right') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n</ng-template>\n\n@if (isRouterLink()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [routerLink]=\"disabled() ? null : routerLink()\"\n [queryParams]=\"queryParams()\"\n [fragment]=\"fragment()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else if (isAnchor()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [attr.href]=\"disabled() ? null : href()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else {\n <button\n #button\n tnTestIdType=\"button\"\n [attr.type]=\"type()\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [tnTestId]=\"testId()\"\n (click)=\"onClick.emit($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </button>\n}\n", styles: [":host{display:inline-block;width:fit-content;justify-self:center}:host(.tn-button--full-width){display:block;width:100%;justify-self:stretch}:host(.tn-button--full-width) .storybook-button{width:100%}.storybook-button ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.storybook-button{display:inline-block;cursor:pointer;border:1px solid transparent;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif;white-space:nowrap}.storybook-button:disabled,.storybook-button.is-disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.storybook-button--has-icon{display:inline-flex;align-items:center;justify-content:center;gap:8px}.storybook-button__icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}a.storybook-button{text-decoration:none;text-align:center}.button-primary{background-color:var(--tn-primary);color:var(--tn-primary-txt)}.button-default{box-shadow:#00000026 0 0 0 1px inset;background-color:var(--tn-btn-default-bg);color:var(--tn-btn-default-txt)}.button-outline-primary{background-color:transparent;border:1px solid var(--tn-primary);color:var(--tn-primary);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-primary:hover{background-color:var(--tn-primary);border:1px solid var(--tn-primary);color:var(--tn-primary-txt)}.button-outline-default{background-color:transparent;border:1px solid var(--tn-lines, #e5e7eb);color:var(--tn-fg1, #000000);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-default:hover{background-color:var(--tn-btn-default-bg);border:1px solid var(--tn-btn-default-bg);color:var(--tn-btn-default-txt);box-shadow:#00000026 0 0 0 1px inset}.button-warn{background-color:var(--tn-red);color:#fff}.button-outline-warn{background-color:transparent;border:1px solid var(--tn-red);color:var(--tn-red);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-warn:hover{background-color:var(--tn-red);border:1px solid var(--tn-red);color:#fff}.storybook-button--small{padding:10px 16px;font-size:12px}.storybook-button--medium{padding:11px 20px;font-size:14px}.storybook-button--large{padding:12px 24px;font-size:1rem}\n"] }]
|
|
2629
|
-
}], propDecorators: { primary: [{ type: i0.Input, args: [{ isSignal: true, alias: "primary", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], iconPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconPosition", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], queryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParams", required: false }] }], fragment: [{ type: i0.Input, args: [{ isSignal: true, alias: "fragment", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], rel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], onClick: [{ type: i0.Output, args: ["onClick"] }], innerRef: [{ type: i0.ViewChild, args: ['button', { isSignal: true }] }] } });
|
|
2645
|
+
}], ctorParameters: () => [], propDecorators: { primary: [{ type: i0.Input, args: [{ isSignal: true, alias: "primary", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], iconPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconPosition", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], queryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParams", required: false }] }], fragment: [{ type: i0.Input, args: [{ isSignal: true, alias: "fragment", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], rel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], onClick: [{ type: i0.Output, args: ["onClick"] }], innerRef: [{ type: i0.ViewChild, args: ['button', { isSignal: true }] }] } });
|
|
2630
2646
|
|
|
2631
2647
|
/**
|
|
2632
2648
|
* Harness for interacting with tn-button in tests.
|
|
@@ -2815,6 +2831,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
2815
2831
|
/* eslint-disable @angular-eslint/no-input-rename */
|
|
2816
2832
|
// Input aliasing is intentional for directive API consistency (e.g., ixTooltip, ixTooltipPosition)
|
|
2817
2833
|
// This follows the standard Angular pattern used by Material and other directive-based components
|
|
2834
|
+
/**
|
|
2835
|
+
* Elements that can receive focus / are read by assistive tech. Used to decide whether
|
|
2836
|
+
* the directive's host is itself the interactive element, or a wrapper (e.g. `<tn-button>`)
|
|
2837
|
+
* whose inner control should carry `aria-describedby`.
|
|
2838
|
+
*/
|
|
2839
|
+
const INTERACTIVE_SELECTOR = 'button, a[href], input, select, textarea, [tabindex]';
|
|
2818
2840
|
class TnTooltipDirective {
|
|
2819
2841
|
message = input('', { ...(ngDevMode ? { debugName: "message" } : {}), alias: 'tnTooltip' });
|
|
2820
2842
|
position = input('above', { ...(ngDevMode ? { debugName: "position" } : {}), alias: 'tnTooltipPosition' });
|
|
@@ -2828,27 +2850,99 @@ class TnTooltipDirective {
|
|
|
2828
2850
|
_hideTimeout = null;
|
|
2829
2851
|
_isTooltipVisible = false;
|
|
2830
2852
|
_positionSub = null;
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
* Only point `aria-describedby` at the tooltip when there is actually a message to
|
|
2834
|
-
* describe. Otherwise every host (e.g. an icon button with no tooltip) would carry a
|
|
2835
|
-
* dangling reference to a tooltip element that is never rendered.
|
|
2836
|
-
*/
|
|
2837
|
-
get _ariaDescribedBy() {
|
|
2838
|
-
return !this.disabled() && this.message() ? this._tooltipId : null;
|
|
2839
|
-
}
|
|
2853
|
+
// Unique ID for the overlay tooltip element
|
|
2854
|
+
_tooltipId = `tn-tooltip-${Math.random().toString(36).substr(2, 9)}`;
|
|
2840
2855
|
_overlay = inject(Overlay);
|
|
2841
2856
|
_elementRef = inject((ElementRef));
|
|
2842
2857
|
_viewContainerRef = inject(ViewContainerRef);
|
|
2843
2858
|
_overlayPositionBuilder = inject(OverlayPositionBuilder);
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2859
|
+
_ariaDescriber = inject(AriaDescriber);
|
|
2860
|
+
_viewInitialized = false;
|
|
2861
|
+
_describedTarget = null;
|
|
2862
|
+
_describedMessage = '';
|
|
2863
|
+
_innerObserver = null;
|
|
2864
|
+
/**
|
|
2865
|
+
* Re-sync the description (see ngAfterViewInit) when the inputs change. The initial
|
|
2866
|
+
* write cannot happen here: on the first run the host's child components (e.g.
|
|
2867
|
+
* tn-button's inner `<button>`) have not rendered yet.
|
|
2868
|
+
*/
|
|
2869
|
+
_syncDescriptionOnInputChange = effect(() => {
|
|
2870
|
+
this.disabled();
|
|
2871
|
+
this.message();
|
|
2872
|
+
if (this._viewInitialized) {
|
|
2873
|
+
this._syncAriaDescription();
|
|
2874
|
+
}
|
|
2875
|
+
}, ...(ngDevMode ? [{ debugName: "_syncDescriptionOnInputChange" }] : []));
|
|
2876
|
+
/**
|
|
2877
|
+
* Expose the message to assistive tech via CDK's AriaDescriber: it keeps the text in
|
|
2878
|
+
* a persistent visually-hidden element (so `aria-describedby` never dangles — the
|
|
2879
|
+
* overlay tooltip only exists while shown) and adds/removes only its own id token,
|
|
2880
|
+
* leaving descriptions the control already carries (form hints, error ids) intact.
|
|
2881
|
+
* The description goes on the interactive element itself: the host when it is one,
|
|
2882
|
+
* otherwise the first interactive descendant (e.g. `<tn-button>`'s inner `<button>`),
|
|
2883
|
+
* because screen readers read descriptions off the focused control, not wrappers.
|
|
2884
|
+
*/
|
|
2885
|
+
ngAfterViewInit() {
|
|
2886
|
+
this._viewInitialized = true;
|
|
2887
|
+
this._syncAriaDescription();
|
|
2888
|
+
const host = this._elementRef.nativeElement;
|
|
2889
|
+
if (!host.matches(INTERACTIVE_SELECTOR) && typeof MutationObserver !== 'undefined') {
|
|
2890
|
+
// The inner control can render after view init (@if branches inside the wrapper
|
|
2891
|
+
// swapping, deferred content) — re-describe when the host's subtree changes.
|
|
2892
|
+
// AriaDescriber writes attributes only, which produces no childList mutations,
|
|
2893
|
+
// so this cannot loop.
|
|
2894
|
+
this._innerObserver = new MutationObserver(() => this._syncAriaDescription());
|
|
2895
|
+
this._innerObserver.observe(host, { childList: true, subtree: true });
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
_syncAriaDescription() {
|
|
2899
|
+
const host = this._elementRef.nativeElement;
|
|
2900
|
+
let target = host;
|
|
2901
|
+
if (!host.matches(INTERACTIVE_SELECTOR)) {
|
|
2902
|
+
// Forward the description only when it is unambiguous: a wrapper with exactly
|
|
2903
|
+
// one interactive descendant (e.g. tn-button's inner <button>). A container
|
|
2904
|
+
// holding several controls keeps the description on the host — describing an
|
|
2905
|
+
// arbitrary first control would attach the text to the wrong element.
|
|
2906
|
+
const candidates = host.querySelectorAll(INTERACTIVE_SELECTOR);
|
|
2907
|
+
if (candidates.length === 1) {
|
|
2908
|
+
target = candidates[0];
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
const message = !this.disabled() ? this._plainTextMessage(this.message()) : '';
|
|
2912
|
+
if (this._describedTarget === target && this._describedMessage === message) {
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
this._removeAriaDescription();
|
|
2916
|
+
if (message) {
|
|
2917
|
+
this._ariaDescriber.describe(target, message);
|
|
2918
|
+
this._describedTarget = target;
|
|
2919
|
+
this._describedMessage = message;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* The overlay tooltip renders its message as HTML (`[innerHTML]`), but AriaDescriber
|
|
2924
|
+
* writes the description as plain text — strip any markup so screen readers never
|
|
2925
|
+
* announce literal tags. DOMParser parses inert markup (no script execution).
|
|
2926
|
+
*/
|
|
2927
|
+
_plainTextMessage(message) {
|
|
2928
|
+
if (!message.includes('<') && !message.includes('&')) {
|
|
2929
|
+
return message;
|
|
2930
|
+
}
|
|
2931
|
+
return new DOMParser().parseFromString(message, 'text/html').body.textContent ?? '';
|
|
2932
|
+
}
|
|
2933
|
+
_removeAriaDescription() {
|
|
2934
|
+
if (this._describedTarget && this._describedMessage) {
|
|
2935
|
+
this._ariaDescriber.removeDescription(this._describedTarget, this._describedMessage);
|
|
2936
|
+
}
|
|
2937
|
+
this._describedTarget = null;
|
|
2938
|
+
this._describedMessage = '';
|
|
2847
2939
|
}
|
|
2848
2940
|
ngOnDestroy() {
|
|
2849
2941
|
this._clearTimeouts();
|
|
2850
2942
|
this.hide(0);
|
|
2851
2943
|
this._positionSub?.unsubscribe();
|
|
2944
|
+
this._innerObserver?.disconnect();
|
|
2945
|
+
this._removeAriaDescription();
|
|
2852
2946
|
if (this._overlayRef) {
|
|
2853
2947
|
this._overlayRef.dispose();
|
|
2854
2948
|
this._overlayRef = null;
|
|
@@ -2862,12 +2956,22 @@ class TnTooltipDirective {
|
|
|
2862
2956
|
_onMouseLeave() {
|
|
2863
2957
|
this.hide(this.hideDelay());
|
|
2864
2958
|
}
|
|
2865
|
-
|
|
2959
|
+
// focusin/focusout (not focus/blur): focus does not bubble, so on a wrapper host
|
|
2960
|
+
// (e.g. `<tn-button>`) a focus listener never fires when the inner control is
|
|
2961
|
+
// focused via keyboard. focusin/focusout bubble and cover both shapes.
|
|
2962
|
+
_onFocusIn() {
|
|
2866
2963
|
if (!this.disabled() && this.message()) {
|
|
2867
2964
|
this.show(this.showDelay());
|
|
2868
2965
|
}
|
|
2869
2966
|
}
|
|
2870
|
-
|
|
2967
|
+
_onFocusOut(event) {
|
|
2968
|
+
// focusout bubbles for every focus move inside the host too — only hide when
|
|
2969
|
+
// focus actually leaves the host, or a wrapper-internal move would tear down
|
|
2970
|
+
// the visible tooltip via the armed hide timeout.
|
|
2971
|
+
const next = event.relatedTarget;
|
|
2972
|
+
if (next && this._elementRef.nativeElement.contains(next)) {
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2871
2975
|
this.hide(this.hideDelay());
|
|
2872
2976
|
}
|
|
2873
2977
|
_onKeydown(event) {
|
|
@@ -2877,10 +2981,16 @@ class TnTooltipDirective {
|
|
|
2877
2981
|
}
|
|
2878
2982
|
/** Shows the tooltip */
|
|
2879
2983
|
show(delay = 0) {
|
|
2880
|
-
if (this.disabled() || !this.message()
|
|
2984
|
+
if (this.disabled() || !this.message()) {
|
|
2881
2985
|
return;
|
|
2882
2986
|
}
|
|
2987
|
+
// Cancel any pending hide even when already visible: a show intent while the
|
|
2988
|
+
// tooltip is up (e.g. focus or pointer re-entering) must keep it up, not let a
|
|
2989
|
+
// previously armed hide timeout tear it down.
|
|
2883
2990
|
this._clearTimeouts();
|
|
2991
|
+
if (this._isTooltipVisible) {
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2884
2994
|
this._showTimeout = setTimeout(() => {
|
|
2885
2995
|
if (!this._overlayRef) {
|
|
2886
2996
|
this._createOverlay();
|
|
@@ -3001,16 +3111,13 @@ class TnTooltipDirective {
|
|
|
3001
3111
|
}
|
|
3002
3112
|
}
|
|
3003
3113
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
3004
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: TnTooltipDirective, isStandalone: true, selector: "[tnTooltip]", inputs: { message: { classPropertyName: "message", publicName: "tnTooltip", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "tnTooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "tnTooltipDisabled", isSignal: true, isRequired: false, transformFunction: null }, showDelay: { classPropertyName: "showDelay", publicName: "tnTooltipShowDelay", isSignal: true, isRequired: false, transformFunction: null }, hideDelay: { classPropertyName: "hideDelay", publicName: "tnTooltipHideDelay", isSignal: true, isRequired: false, transformFunction: null }, tooltipClass: { classPropertyName: "tooltipClass", publicName: "tnTooltipClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "_onMouseEnter()", "mouseleave": "_onMouseLeave()", "
|
|
3114
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: TnTooltipDirective, isStandalone: true, selector: "[tnTooltip]", inputs: { message: { classPropertyName: "message", publicName: "tnTooltip", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "tnTooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "tnTooltipDisabled", isSignal: true, isRequired: false, transformFunction: null }, showDelay: { classPropertyName: "showDelay", publicName: "tnTooltipShowDelay", isSignal: true, isRequired: false, transformFunction: null }, hideDelay: { classPropertyName: "hideDelay", publicName: "tnTooltipHideDelay", isSignal: true, isRequired: false, transformFunction: null }, tooltipClass: { classPropertyName: "tooltipClass", publicName: "tnTooltipClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "_onMouseEnter()", "mouseleave": "_onMouseLeave()", "focusin": "_onFocusIn()", "focusout": "_onFocusOut($event)", "keydown": "_onKeydown($event)" } }, ngImport: i0 });
|
|
3005
3115
|
}
|
|
3006
3116
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTooltipDirective, decorators: [{
|
|
3007
3117
|
type: Directive,
|
|
3008
3118
|
args: [{
|
|
3009
3119
|
selector: '[tnTooltip]',
|
|
3010
3120
|
standalone: true,
|
|
3011
|
-
host: {
|
|
3012
|
-
'[attr.aria-describedby]': '_ariaDescribedBy',
|
|
3013
|
-
}
|
|
3014
3121
|
}]
|
|
3015
3122
|
}], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltip", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltipPosition", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltipDisabled", required: false }] }], showDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltipShowDelay", required: false }] }], hideDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltipHideDelay", required: false }] }], tooltipClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltipClass", required: false }] }], _onMouseEnter: [{
|
|
3016
3123
|
type: HostListener,
|
|
@@ -3018,12 +3125,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
3018
3125
|
}], _onMouseLeave: [{
|
|
3019
3126
|
type: HostListener,
|
|
3020
3127
|
args: ['mouseleave']
|
|
3021
|
-
}],
|
|
3128
|
+
}], _onFocusIn: [{
|
|
3022
3129
|
type: HostListener,
|
|
3023
|
-
args: ['
|
|
3024
|
-
}],
|
|
3130
|
+
args: ['focusin']
|
|
3131
|
+
}], _onFocusOut: [{
|
|
3025
3132
|
type: HostListener,
|
|
3026
|
-
args: ['
|
|
3133
|
+
args: ['focusout', ['$event']]
|
|
3027
3134
|
}], _onKeydown: [{
|
|
3028
3135
|
type: HostListener,
|
|
3029
3136
|
args: ['keydown', ['$event']]
|
|
@@ -6314,7 +6421,7 @@ class TnCardComponent {
|
|
|
6314
6421
|
return type ? `tn-card__status--${type}` : 'tn-card__status--neutral';
|
|
6315
6422
|
}
|
|
6316
6423
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6317
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCardComponent, isStandalone: true, selector: "tn-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, titleLink: { classPropertyName: "titleLink", publicName: "titleLink", isSignal: true, isRequired: false, transformFunction: null }, titleRouterLink: { classPropertyName: "titleRouterLink", publicName: "titleRouterLink", isSignal: true, isRequired: false, transformFunction: null }, titleQueryParams: { classPropertyName: "titleQueryParams", publicName: "titleQueryParams", isSignal: true, isRequired: false, transformFunction: null }, titleTooltip: { classPropertyName: "titleTooltip", publicName: "titleTooltip", isSignal: true, isRequired: false, transformFunction: null }, elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, padContent: { classPropertyName: "padContent", publicName: "padContent", isSignal: true, isRequired: false, transformFunction: null }, fillHeight: { classPropertyName: "fillHeight", publicName: "fillHeight", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null }, headerStatus: { classPropertyName: "headerStatus", publicName: "headerStatus", isSignal: true, isRequired: false, transformFunction: null }, headerControl: { classPropertyName: "headerControl", publicName: "headerControl", isSignal: true, isRequired: false, transformFunction: null }, headerMenu: { classPropertyName: "headerMenu", publicName: "headerMenu", isSignal: true, isRequired: false, transformFunction: null }, headerMenuTriggerTestId: { classPropertyName: "headerMenuTriggerTestId", publicName: "headerMenuTriggerTestId", isSignal: true, isRequired: false, transformFunction: null }, primaryAction: { classPropertyName: "primaryAction", publicName: "primaryAction", isSignal: true, isRequired: false, transformFunction: null }, secondaryAction: { classPropertyName: "secondaryAction", publicName: "secondaryAction", isSignal: true, isRequired: false, transformFunction: null }, footerLink: { classPropertyName: "footerLink", publicName: "footerLink", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.tn-card-host--content-height": "!fillHeight()" } }, queries: [{ propertyName: "projectedHeader", first: true, predicate: TnCardHeaderDirective, descendants: true, isSignal: true }, { propertyName: "headerActions", first: true, predicate: TnCardHeaderActionsDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "footerActions", first: true, predicate: TnCardFooterActionsDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3 class=\"tn-card__title\">\n @if (isTitleRouterLink()) {\n <a\n class=\"tn-card__title-link\"\n [routerLink]=\"titleRouterLink()\"\n [queryParams]=\"titleQueryParams()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </a>\n } @else if (titleLink()) {\n <span\n class=\"tn-card__title-link\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick(); $event.preventDefault()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </span>\n } @else {\n {{ title() }}\n }\n\n @if (titleTooltip(); as tooltip) {\n <button\n type=\"button\"\n class=\"tn-card__title-tooltip\"\n aria-label=\"More information\"\n [tnTooltip]=\"tooltip\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </button>\n }\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Projected header actions (escape hatch for custom/permission-gated controls) -->\n @if (headerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n <!-- Projected footer actions (escape hatch for custom/permission-gated controls) -->\n @if (footerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n </div>\n </div>\n }\n</div>", styles: [":host{display:block}:host(.tn-card-host--content-height){height:auto}:host(.tn-card-host--content-height) .tn-card{height:auto}.tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{display:inline-flex;align-items:center;gap:8px;margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title-link{display:inline-flex;align-items:center;gap:4px;color:inherit;cursor:pointer;text-decoration:none;transition:color .2s ease}.tn-card__title-link:hover{color:var(--tn-primary, #2563eb)}.tn-card__title-link-icon{flex-shrink:0}.tn-card__title-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-card__title-tooltip:hover,.tn-card__title-tooltip:focus-visible{color:var(--tn-fg1, #1f2937)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "component", type: TnSlideToggleComponent, selector: "tn-slide-toggle", inputs: ["labelPosition", "label", "fullWidth", "disabled", "required", "color", "testId", "ariaLabel", "ariaLabelledby", "checked"], outputs: ["change", "toggleChange"] }, { kind: "component", type: TnMenuComponent, selector: "tn-menu", inputs: ["items", "contextMenu", "testId"], outputs: ["menuItemClick", "menuOpen", "menuClose"] }, { kind: "directive", type: TnMenuTriggerDirective, selector: "[tnMenuTriggerFor]", inputs: ["tnMenuTriggerFor", "tnMenuPosition"], exportAs: ["tnMenuTrigger"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "directive", type: TnTooltipDirective, selector: "[tnTooltip]", inputs: ["tnTooltip", "tnTooltipPosition", "tnTooltipDisabled", "tnTooltipShowDelay", "tnTooltipHideDelay", "tnTooltipClass"] }] });
|
|
6424
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCardComponent, isStandalone: true, selector: "tn-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, titleLink: { classPropertyName: "titleLink", publicName: "titleLink", isSignal: true, isRequired: false, transformFunction: null }, titleRouterLink: { classPropertyName: "titleRouterLink", publicName: "titleRouterLink", isSignal: true, isRequired: false, transformFunction: null }, titleQueryParams: { classPropertyName: "titleQueryParams", publicName: "titleQueryParams", isSignal: true, isRequired: false, transformFunction: null }, titleTooltip: { classPropertyName: "titleTooltip", publicName: "titleTooltip", isSignal: true, isRequired: false, transformFunction: null }, elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, padContent: { classPropertyName: "padContent", publicName: "padContent", isSignal: true, isRequired: false, transformFunction: null }, fillHeight: { classPropertyName: "fillHeight", publicName: "fillHeight", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null }, headerStatus: { classPropertyName: "headerStatus", publicName: "headerStatus", isSignal: true, isRequired: false, transformFunction: null }, headerControl: { classPropertyName: "headerControl", publicName: "headerControl", isSignal: true, isRequired: false, transformFunction: null }, headerMenu: { classPropertyName: "headerMenu", publicName: "headerMenu", isSignal: true, isRequired: false, transformFunction: null }, headerMenuTriggerTestId: { classPropertyName: "headerMenuTriggerTestId", publicName: "headerMenuTriggerTestId", isSignal: true, isRequired: false, transformFunction: null }, primaryAction: { classPropertyName: "primaryAction", publicName: "primaryAction", isSignal: true, isRequired: false, transformFunction: null }, secondaryAction: { classPropertyName: "secondaryAction", publicName: "secondaryAction", isSignal: true, isRequired: false, transformFunction: null }, footerLink: { classPropertyName: "footerLink", publicName: "footerLink", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.tn-card-host--content-height": "!fillHeight()" } }, queries: [{ propertyName: "projectedHeader", first: true, predicate: TnCardHeaderDirective, descendants: true, isSignal: true }, { propertyName: "headerActions", first: true, predicate: TnCardHeaderActionsDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "footerActions", first: true, predicate: TnCardFooterActionsDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3 class=\"tn-card__title\">\n @if (isTitleRouterLink()) {\n <a\n class=\"tn-card__title-link\"\n [routerLink]=\"titleRouterLink()\"\n [queryParams]=\"titleQueryParams()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </a>\n } @else if (titleLink()) {\n <span\n class=\"tn-card__title-link\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick(); $event.preventDefault()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </span>\n } @else {\n {{ title() }}\n }\n\n @if (titleTooltip(); as tooltip) {\n <button\n type=\"button\"\n class=\"tn-card__title-tooltip\"\n aria-label=\"More information\"\n [tnTooltip]=\"tooltip\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </button>\n }\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Projected header actions (escape hatch for custom/permission-gated controls) -->\n @if (headerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n [tnTooltip]=\"action.tooltip || ''\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n [tnTooltip]=\"action.tooltip || ''\"\n (click)=\"action.handler()\" />\n }\n\n <!-- Projected footer actions (escape hatch for custom/permission-gated controls) -->\n @if (footerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n </div>\n </div>\n }\n</div>", styles: [":host{display:block}:host(.tn-card-host--content-height){height:auto}:host(.tn-card-host--content-height) .tn-card{height:auto}.tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{display:inline-flex;align-items:center;gap:8px;margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title-link{display:inline-flex;align-items:center;gap:4px;color:inherit;cursor:pointer;text-decoration:none;transition:color .2s ease}.tn-card__title-link:hover{color:var(--tn-primary, #2563eb)}.tn-card__title-link-icon{flex-shrink:0}.tn-card__title-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-card__title-tooltip:hover,.tn-card__title-tooltip:focus-visible{color:var(--tn-fg1, #1f2937)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "component", type: TnSlideToggleComponent, selector: "tn-slide-toggle", inputs: ["labelPosition", "label", "fullWidth", "disabled", "required", "color", "testId", "ariaLabel", "ariaLabelledby", "checked"], outputs: ["change", "toggleChange"] }, { kind: "component", type: TnMenuComponent, selector: "tn-menu", inputs: ["items", "contextMenu", "testId"], outputs: ["menuItemClick", "menuOpen", "menuClose"] }, { kind: "directive", type: TnMenuTriggerDirective, selector: "[tnMenuTriggerFor]", inputs: ["tnMenuTriggerFor", "tnMenuPosition"], exportAs: ["tnMenuTrigger"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "directive", type: TnTooltipDirective, selector: "[tnTooltip]", inputs: ["tnTooltip", "tnTooltipPosition", "tnTooltipDisabled", "tnTooltipShowDelay", "tnTooltipHideDelay", "tnTooltipClass"] }] });
|
|
6318
6425
|
}
|
|
6319
6426
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, decorators: [{
|
|
6320
6427
|
type: Component,
|
|
@@ -6331,7 +6438,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
6331
6438
|
TnTooltipDirective,
|
|
6332
6439
|
], host: {
|
|
6333
6440
|
'[class.tn-card-host--content-height]': '!fillHeight()',
|
|
6334
|
-
}, template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3 class=\"tn-card__title\">\n @if (isTitleRouterLink()) {\n <a\n class=\"tn-card__title-link\"\n [routerLink]=\"titleRouterLink()\"\n [queryParams]=\"titleQueryParams()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </a>\n } @else if (titleLink()) {\n <span\n class=\"tn-card__title-link\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick(); $event.preventDefault()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </span>\n } @else {\n {{ title() }}\n }\n\n @if (titleTooltip(); as tooltip) {\n <button\n type=\"button\"\n class=\"tn-card__title-tooltip\"\n aria-label=\"More information\"\n [tnTooltip]=\"tooltip\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </button>\n }\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Projected header actions (escape hatch for custom/permission-gated controls) -->\n @if (headerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n <!-- Projected footer actions (escape hatch for custom/permission-gated controls) -->\n @if (footerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n </div>\n </div>\n }\n</div>", styles: [":host{display:block}:host(.tn-card-host--content-height){height:auto}:host(.tn-card-host--content-height) .tn-card{height:auto}.tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{display:inline-flex;align-items:center;gap:8px;margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title-link{display:inline-flex;align-items:center;gap:4px;color:inherit;cursor:pointer;text-decoration:none;transition:color .2s ease}.tn-card__title-link:hover{color:var(--tn-primary, #2563eb)}.tn-card__title-link-icon{flex-shrink:0}.tn-card__title-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-card__title-tooltip:hover,.tn-card__title-tooltip:focus-visible{color:var(--tn-fg1, #1f2937)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"] }]
|
|
6441
|
+
}, template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3 class=\"tn-card__title\">\n @if (isTitleRouterLink()) {\n <a\n class=\"tn-card__title-link\"\n [routerLink]=\"titleRouterLink()\"\n [queryParams]=\"titleQueryParams()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </a>\n } @else if (titleLink()) {\n <span\n class=\"tn-card__title-link\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick(); $event.preventDefault()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </span>\n } @else {\n {{ title() }}\n }\n\n @if (titleTooltip(); as tooltip) {\n <button\n type=\"button\"\n class=\"tn-card__title-tooltip\"\n aria-label=\"More information\"\n [tnTooltip]=\"tooltip\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </button>\n }\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Projected header actions (escape hatch for custom/permission-gated controls) -->\n @if (headerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n [tnTooltip]=\"action.tooltip || ''\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n [tnTooltip]=\"action.tooltip || ''\"\n (click)=\"action.handler()\" />\n }\n\n <!-- Projected footer actions (escape hatch for custom/permission-gated controls) -->\n @if (footerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n </div>\n </div>\n }\n</div>", styles: [":host{display:block}:host(.tn-card-host--content-height){height:auto}:host(.tn-card-host--content-height) .tn-card{height:auto}.tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{display:inline-flex;align-items:center;gap:8px;margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title-link{display:inline-flex;align-items:center;gap:4px;color:inherit;cursor:pointer;text-decoration:none;transition:color .2s ease}.tn-card__title-link:hover{color:var(--tn-primary, #2563eb)}.tn-card__title-link-icon{flex-shrink:0}.tn-card__title-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-card__title-tooltip:hover,.tn-card__title-tooltip:focus-visible{color:var(--tn-fg1, #1f2937)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"] }]
|
|
6335
6442
|
}], ctorParameters: () => [], propDecorators: { projectedHeader: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCardHeaderDirective), { isSignal: true }] }], headerActions: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCardHeaderActionsDirective), { ...{ read: TemplateRef }, isSignal: true }] }], footerActions: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCardFooterActionsDirective), { ...{ read: TemplateRef }, isSignal: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], titleLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleLink", required: false }] }], titleRouterLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleRouterLink", required: false }] }], titleQueryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleQueryParams", required: false }] }], titleTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleTooltip", required: false }] }], elevation: [{ type: i0.Input, args: [{ isSignal: true, alias: "elevation", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], padContent: [{ type: i0.Input, args: [{ isSignal: true, alias: "padContent", required: false }] }], fillHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillHeight", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], background: [{ type: i0.Input, args: [{ isSignal: true, alias: "background", required: false }] }], headerStatus: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerStatus", required: false }] }], headerControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerControl", required: false }] }], headerMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMenu", required: false }] }], headerMenuTriggerTestId: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMenuTriggerTestId", required: false }] }], primaryAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "primaryAction", required: false }] }], secondaryAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondaryAction", required: false }] }], footerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerLink", required: false }] }] } });
|
|
6336
6443
|
|
|
6337
6444
|
class TnExpansionPanelComponent {
|
|
@@ -11137,10 +11244,51 @@ class TnTableColumnDirective {
|
|
|
11137
11244
|
name = input.required({ ...(ngDevMode ? { debugName: "name" } : {}), alias: 'tnColumnDef' });
|
|
11138
11245
|
sortable = input(false, ...(ngDevMode ? [{ debugName: "sortable" }] : []));
|
|
11139
11246
|
width = input(undefined, ...(ngDevMode ? [{ debugName: "width" }] : []));
|
|
11247
|
+
/**
|
|
11248
|
+
* Human-readable label for the column. Used as the default header text in the
|
|
11249
|
+
* regular table layout AND the default field label in card mode, so a single
|
|
11250
|
+
* annotation serves both — prefer this over a text-only `tnHeaderCellDef`.
|
|
11251
|
+
* A `tnHeaderCellDef` template still overrides the header; `cardLabel` still
|
|
11252
|
+
* overrides the card field label. Falls back to the column `name` when unset.
|
|
11253
|
+
*/
|
|
11254
|
+
label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
11255
|
+
/**
|
|
11256
|
+
* Relative importance of this column in card mode (see `mobileLayout` on
|
|
11257
|
+
* `tn-table`). Higher numbers render first; fields ranked beyond
|
|
11258
|
+
* `cardPrimaryCount` fold under a "More fields" disclosure. Defaults to `0`,
|
|
11259
|
+
* in which case columns fall back to their `displayedColumns` order. Has no
|
|
11260
|
+
* effect in the regular (wide) table layout.
|
|
11261
|
+
*/
|
|
11262
|
+
cardPriority = input(0, ...(ngDevMode ? [{ debugName: "cardPriority" }] : []));
|
|
11263
|
+
/**
|
|
11264
|
+
* Marks this column as the card's title in card mode — rendered prominently
|
|
11265
|
+
* in the card header alongside any row actions, never as a label/value field.
|
|
11266
|
+
* If no column sets this, the first displayed column that isn't `cardHidden` is
|
|
11267
|
+
* used, falling back to the first displayed column when every column is hidden —
|
|
11268
|
+
* a column deliberately kept off the card is not promoted to its most prominent
|
|
11269
|
+
* slot.
|
|
11270
|
+
*/
|
|
11271
|
+
cardTitle = input(false, ...(ngDevMode ? [{ debugName: "cardTitle" }] : []));
|
|
11272
|
+
/**
|
|
11273
|
+
* When true, this column is omitted entirely from the card layout (e.g. a
|
|
11274
|
+
* redundant id, or a column whose meaning is already conveyed by the title).
|
|
11275
|
+
* Ignored in the regular table layout.
|
|
11276
|
+
*/
|
|
11277
|
+
cardHidden = input(false, ...(ngDevMode ? [{ debugName: "cardHidden" }] : []));
|
|
11278
|
+
/**
|
|
11279
|
+
* Overrides the field label shown for this column in card mode. Defaults to the
|
|
11280
|
+
* column's `label`, then to the column name.
|
|
11281
|
+
*
|
|
11282
|
+
* A `tnHeaderCellDef` template is NOT used here — it is an `ng-template`, not
|
|
11283
|
+
* text, so there is nothing to read out. A column whose header comes only from
|
|
11284
|
+
* that template renders its bare column name as the card field label; set
|
|
11285
|
+
* `label` (or `cardLabel`) to give the card something readable.
|
|
11286
|
+
*/
|
|
11287
|
+
cardLabel = input(undefined, ...(ngDevMode ? [{ debugName: "cardLabel" }] : []));
|
|
11140
11288
|
headerTemplate = contentChild(TnHeaderCellDefDirective, { ...(ngDevMode ? { debugName: "headerTemplate" } : {}), read: TemplateRef });
|
|
11141
11289
|
cellTemplate = contentChild(TnCellDefDirective, { ...(ngDevMode ? { debugName: "cellTemplate" } : {}), read: TemplateRef });
|
|
11142
11290
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableColumnDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
11143
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.1.0", type: TnTableColumnDirective, isStandalone: true, selector: "[tnColumnDef]", inputs: { name: { classPropertyName: "name", publicName: "tnColumnDef", isSignal: true, isRequired: true, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "headerTemplate", first: true, predicate: TnHeaderCellDefDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "cellTemplate", first: true, predicate: TnCellDefDirective, descendants: true, read: TemplateRef, isSignal: true }], exportAs: ["tnColumnDef"], ngImport: i0 });
|
|
11291
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.1.0", type: TnTableColumnDirective, isStandalone: true, selector: "[tnColumnDef]", inputs: { name: { classPropertyName: "name", publicName: "tnColumnDef", isSignal: true, isRequired: true, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, cardPriority: { classPropertyName: "cardPriority", publicName: "cardPriority", isSignal: true, isRequired: false, transformFunction: null }, cardTitle: { classPropertyName: "cardTitle", publicName: "cardTitle", isSignal: true, isRequired: false, transformFunction: null }, cardHidden: { classPropertyName: "cardHidden", publicName: "cardHidden", isSignal: true, isRequired: false, transformFunction: null }, cardLabel: { classPropertyName: "cardLabel", publicName: "cardLabel", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "headerTemplate", first: true, predicate: TnHeaderCellDefDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "cellTemplate", first: true, predicate: TnCellDefDirective, descendants: true, read: TemplateRef, isSignal: true }], exportAs: ["tnColumnDef"], ngImport: i0 });
|
|
11144
11292
|
}
|
|
11145
11293
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableColumnDirective, decorators: [{
|
|
11146
11294
|
type: Directive,
|
|
@@ -11149,7 +11297,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11149
11297
|
standalone: true,
|
|
11150
11298
|
exportAs: 'tnColumnDef',
|
|
11151
11299
|
}]
|
|
11152
|
-
}], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnColumnDef", required: true }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], headerTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnHeaderCellDefDirective), { ...{
|
|
11300
|
+
}], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnColumnDef", required: true }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], cardPriority: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardPriority", required: false }] }], cardTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardTitle", required: false }] }], cardHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardHidden", required: false }] }], cardLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardLabel", required: false }] }], headerTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnHeaderCellDefDirective), { ...{
|
|
11153
11301
|
read: TemplateRef,
|
|
11154
11302
|
}, isSignal: true }] }], cellTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCellDefDirective), { ...{ read: TemplateRef }, isSignal: true }] }] } });
|
|
11155
11303
|
/**
|
|
@@ -11178,6 +11326,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11178
11326
|
standalone: true,
|
|
11179
11327
|
}]
|
|
11180
11328
|
}] });
|
|
11329
|
+
/**
|
|
11330
|
+
* Directive to define a row's action controls (e.g. edit/delete buttons).
|
|
11331
|
+
*
|
|
11332
|
+
* Place inside `tn-table` to render a trailing actions column, pinned to the right edge
|
|
11333
|
+
* in `scroll` mode (see `mobileLayout` on `tn-table`) and scrolling with the rest of the
|
|
11334
|
+
* table above `cardBreakpoint`. In card mode the same template is rendered inline beside
|
|
11335
|
+
* the card title, so row actions stay reachable at every viewport width. The template
|
|
11336
|
+
* receives the row as its implicit context.
|
|
11337
|
+
*
|
|
11338
|
+
* @example
|
|
11339
|
+
* ```html
|
|
11340
|
+
* <tn-table [dataSource]="data" [displayedColumns]="columns">
|
|
11341
|
+
* ...column defs...
|
|
11342
|
+
* <ng-template tnRowActionsDef let-row>
|
|
11343
|
+
* <tn-icon-button name="mat-edit" ariaLabel="Edit" (click)="edit(row)" />
|
|
11344
|
+
* <tn-icon-button name="mat-delete" ariaLabel="Delete" (click)="remove(row)" />
|
|
11345
|
+
* </ng-template>
|
|
11346
|
+
* </tn-table>
|
|
11347
|
+
* ```
|
|
11348
|
+
*/
|
|
11349
|
+
class TnRowActionsDefDirective {
|
|
11350
|
+
template = inject((TemplateRef));
|
|
11351
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnRowActionsDefDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
11352
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnRowActionsDefDirective, isStandalone: true, selector: "[tnRowActionsDef]", ngImport: i0 });
|
|
11353
|
+
}
|
|
11354
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnRowActionsDefDirective, decorators: [{
|
|
11355
|
+
type: Directive,
|
|
11356
|
+
args: [{
|
|
11357
|
+
selector: '[tnRowActionsDef]',
|
|
11358
|
+
standalone: true,
|
|
11359
|
+
}]
|
|
11360
|
+
}] });
|
|
11181
11361
|
|
|
11182
11362
|
/**
|
|
11183
11363
|
* Animation duration for detail row expand/collapse.
|
|
@@ -11200,6 +11380,16 @@ function getExpandDuration() {
|
|
|
11200
11380
|
}
|
|
11201
11381
|
class TnTableComponent {
|
|
11202
11382
|
destroyRef = inject(DestroyRef);
|
|
11383
|
+
elementRef = inject((ElementRef));
|
|
11384
|
+
injector = inject(Injector);
|
|
11385
|
+
/**
|
|
11386
|
+
* Element that had focus when `loading` went true, so it can be restored afterwards.
|
|
11387
|
+
*
|
|
11388
|
+
* `inert` blurs whatever is focused inside it and refuses focus while set, so without this
|
|
11389
|
+
* a keyboard user who pressed Enter on a sortable header — the flow that *starts* most
|
|
11390
|
+
* loads — was dropped to `<body>` and had to tab back from the top of the document.
|
|
11391
|
+
*/
|
|
11392
|
+
focusBeforeLoading = null;
|
|
11203
11393
|
// --- Core inputs ---
|
|
11204
11394
|
dataSource = input([], ...(ngDevMode ? [{ debugName: "dataSource" }] : []));
|
|
11205
11395
|
displayedColumns = input([], ...(ngDevMode ? [{ debugName: "displayedColumns" }] : []));
|
|
@@ -11267,6 +11457,12 @@ class TnTableComponent {
|
|
|
11267
11457
|
* When true, shows a spinner overlay over the table. Existing rows remain
|
|
11268
11458
|
* visible (dimmed) so reloads don't cause layout jumps; if there are no rows
|
|
11269
11459
|
* yet, the spinner replaces the empty state.
|
|
11460
|
+
*
|
|
11461
|
+
* The rendered surfaces are `inert` while this is true, so they leave the tab order *and*
|
|
11462
|
+
* the accessibility tree — a screen-reader user hears the overlay's "Loading..." status
|
|
11463
|
+
* rather than the stale rows. That is deliberate: `pointer-events` alone left every row,
|
|
11464
|
+
* header and projected control activatable by keyboard against data being replaced.
|
|
11465
|
+
* Focus is captured on the way in and restored on the way out.
|
|
11270
11466
|
*/
|
|
11271
11467
|
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
11272
11468
|
/** Accessible label announced while loading. */
|
|
@@ -11275,6 +11471,16 @@ class TnTableComponent {
|
|
|
11275
11471
|
* When true, rows become keyboard-focusable (tabindex=0) and clicking or
|
|
11276
11472
|
* pressing Enter/Space emits `rowClick`. Use this for "click row to view
|
|
11277
11473
|
* details" patterns. Independent of `selectable` (checkbox) and `expandable`.
|
|
11474
|
+
*
|
|
11475
|
+
* Accessibility limitation in card mode: a card is `role="listitem"`, and the
|
|
11476
|
+
* roles that would announce it as activatable (`button`, `option`) have
|
|
11477
|
+
* presentational children, which would hide the card's own checkbox, row
|
|
11478
|
+
* actions and "Details" toggle from assistive tech. So a clickable card is
|
|
11479
|
+
* focusable and responds to Enter/Space, but is not announced as a control.
|
|
11480
|
+
* Active state is conveyed with `aria-current`, and expansion with
|
|
11481
|
+
* `aria-expanded` when `expandOnRowClick` is set. If an action must be
|
|
11482
|
+
* discoverable by screen-reader users, project an explicit control through
|
|
11483
|
+
* `[tnRowActionsDef]` rather than relying on card activation alone.
|
|
11278
11484
|
*/
|
|
11279
11485
|
clickable = input(false, ...(ngDevMode ? [{ debugName: "clickable" }] : []));
|
|
11280
11486
|
/**
|
|
@@ -11282,6 +11488,12 @@ class TnTableComponent {
|
|
|
11282
11488
|
* chevron. Requires `clickable` — that is what makes rows activatable at all — and `expandable`;
|
|
11283
11489
|
* rows the `isRowExpandable` predicate rejects are unaffected, since `toggleRowExpansion` gates
|
|
11284
11490
|
* on it. `rowClick` still emits, so a consumer can both expand and react to the click.
|
|
11491
|
+
*
|
|
11492
|
+
* Applies in card mode too, where activating the card toggles its detail section. Both controls
|
|
11493
|
+
* report the state: the card carries `aria-expanded` while it is the trigger (`listitem` does
|
|
11494
|
+
* permit it, unlike `aria-selected`), and the "Details" button carries it unconditionally,
|
|
11495
|
+
* because that button is what a screen-reader user reaches by tabbing. The redundancy is
|
|
11496
|
+
* deliberate — see the comment above the button in the template.
|
|
11285
11497
|
*/
|
|
11286
11498
|
expandOnRowClick = input(false, ...(ngDevMode ? [{ debugName: "expandOnRowClick" }] : []));
|
|
11287
11499
|
/**
|
|
@@ -11336,9 +11548,52 @@ class TnTableComponent {
|
|
|
11336
11548
|
if (!perColumn || !this.fixedLayout()) {
|
|
11337
11549
|
return null;
|
|
11338
11550
|
}
|
|
11339
|
-
|
|
11551
|
+
// The actions column adds its own fixed width rather than a `minColumnWidth` share, because
|
|
11552
|
+
// that is exactly what it claims under `fixedLayout` — and the cell is `border-box`, so the
|
|
11553
|
+
// value added here is its real outer width. (`--tn-table-select-width` and
|
|
11554
|
+
// `--tn-table-expand-width` are treated as ordinary shares instead: they over-reserve
|
|
11555
|
+
// slightly, which is the behavior this component has always had and has tests for. Unifying
|
|
11556
|
+
// all three belongs in its own change.)
|
|
11557
|
+
//
|
|
11558
|
+
// No `var()` fallback on purpose. A duplicated default here is a second source of truth for a
|
|
11559
|
+
// number the stylesheet owns, and it drifted the moment that default changed — leaving the
|
|
11560
|
+
// exact value whose shortfall clipped the actions column sitting in code labelled "the
|
|
11561
|
+
// default". `:host` always defines the property, so the only way to reach a fallback is to
|
|
11562
|
+
// unset it deliberately, and an invalid calc() (no floor) is a more debuggable outcome than a
|
|
11563
|
+
// floor computed from a stale constant.
|
|
11564
|
+
const columns = `${perColumn} * ${this.effectiveDisplayedColumns().length}`;
|
|
11565
|
+
return this.rowActionsDef()
|
|
11566
|
+
? `calc(${columns} + var(--tn-table-actions-width))`
|
|
11567
|
+
: `calc(${columns})`;
|
|
11340
11568
|
}, ...(ngDevMode ? [{ debugName: "resolvedMinWidth" }] : []));
|
|
11569
|
+
// --- Responsive (card) inputs ---
|
|
11570
|
+
/**
|
|
11571
|
+
* How the table adapts when its container is narrower than `cardBreakpoint`.
|
|
11572
|
+
* See {@link TnTableMobileLayout}. Defaults to `scroll`, which preserves the
|
|
11573
|
+
* existing horizontal-scroll behavior; set to `cards` to opt into the stacked
|
|
11574
|
+
* card layout.
|
|
11575
|
+
*/
|
|
11576
|
+
mobileLayout = input('scroll', ...(ngDevMode ? [{ debugName: "mobileLayout" }] : []));
|
|
11577
|
+
/**
|
|
11578
|
+
* Container width (px) below which `mobileLayout` takes effect. The component
|
|
11579
|
+
* observes its own host width (via `ResizeObserver`), so this responds to the
|
|
11580
|
+
* available container — a table in a narrow sidebar adapts on a wide screen.
|
|
11581
|
+
*
|
|
11582
|
+
* Measured against the host's **content box** — its border and padding are excluded — so a
|
|
11583
|
+
* `[bordered]` table crosses the threshold at the same content width as an unbordered one.
|
|
11584
|
+
*/
|
|
11585
|
+
cardBreakpoint = input(640, ...(ngDevMode ? [{ debugName: "cardBreakpoint" }] : []));
|
|
11586
|
+
/**
|
|
11587
|
+
* Number of fields shown directly on each card before the rest fold under a
|
|
11588
|
+
* "More fields" disclosure. The title column is not counted. Defaults to `3`.
|
|
11589
|
+
*/
|
|
11590
|
+
cardPrimaryCount = input(3, ...(ngDevMode ? [{ debugName: "cardPrimaryCount" }] : []));
|
|
11341
11591
|
// --- Outputs ---
|
|
11592
|
+
/**
|
|
11593
|
+
* Emits the resulting sort state whenever the user changes it, from either layout.
|
|
11594
|
+
* Clearing a sort emits `{ column: '', direction: '' }` rather than naming the column
|
|
11595
|
+
* that was cleared — see {@link TnSortEvent}.
|
|
11596
|
+
*/
|
|
11342
11597
|
sortChange = output();
|
|
11343
11598
|
selectionChange = output();
|
|
11344
11599
|
/** Emits the row when a clickable row is activated (click or Enter/Space). */
|
|
@@ -11353,6 +11608,11 @@ class TnTableComponent {
|
|
|
11353
11608
|
// --- Content queries ---
|
|
11354
11609
|
columnDefs = contentChildren(TnTableColumnDirective, ...(ngDevMode ? [{ debugName: "columnDefs" }] : []));
|
|
11355
11610
|
detailRowDef = contentChild(TnDetailRowDefDirective, ...(ngDevMode ? [{ debugName: "detailRowDef" }] : []));
|
|
11611
|
+
rowActionsDef = contentChild(TnRowActionsDefDirective, ...(ngDevMode ? [{ debugName: "rowActionsDef" }] : []));
|
|
11612
|
+
// --- Responsive state ---
|
|
11613
|
+
/** Observed host width in px; drives the switch into card mode. */
|
|
11614
|
+
containerWidth = signal(Infinity, ...(ngDevMode ? [{ debugName: "containerWidth" }] : []));
|
|
11615
|
+
resizeObserver;
|
|
11356
11616
|
// --- Sort state ---
|
|
11357
11617
|
/**
|
|
11358
11618
|
* Sorted column and direction. Two-way bindable (`[(sortColumn)]`), so a consumer that owns the
|
|
@@ -11368,6 +11628,9 @@ class TnTableComponent {
|
|
|
11368
11628
|
* approach could address this.
|
|
11369
11629
|
*/
|
|
11370
11630
|
expandedRows = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedRows" }] : []));
|
|
11631
|
+
// Per-instance prefix for generated DOM ids, so two tables on a page can't collide.
|
|
11632
|
+
static instanceCount = 0;
|
|
11633
|
+
instanceId = `tn-table-${TnTableComponent.instanceCount++}`;
|
|
11371
11634
|
// --- Selection state ---
|
|
11372
11635
|
selection = new SelectionModel(true, []);
|
|
11373
11636
|
selectionCount = signal(0, ...(ngDevMode ? [{ debugName: "selectionCount" }] : []));
|
|
@@ -11428,14 +11691,147 @@ class TnTableComponent {
|
|
|
11428
11691
|
this.expandedRows.set(next);
|
|
11429
11692
|
}
|
|
11430
11693
|
});
|
|
11694
|
+
// Save focus on the way into `loading`, restore it on the way out. The restore is
|
|
11695
|
+
// deferred to after the next render because the surfaces only drop `inert` when the
|
|
11696
|
+
// template updates, and focusing an element still inside an inert subtree is a no-op.
|
|
11697
|
+
effect(() => {
|
|
11698
|
+
const host = this.elementRef.nativeElement;
|
|
11699
|
+
if (typeof document === 'undefined') {
|
|
11700
|
+
return;
|
|
11701
|
+
}
|
|
11702
|
+
if (this.loading()) {
|
|
11703
|
+
const active = document.activeElement;
|
|
11704
|
+
// `<body>` is what `inert` leaves behind, not evidence the user moved on. Signals are
|
|
11705
|
+
// glitch-free, so a false -> true flip inside one task (a store that clears loading on
|
|
11706
|
+
// a cache hit and re-sets it for the network request) runs this effect once, seeing
|
|
11707
|
+
// only `true` — and overwriting a still-valid saved element with null there would lose
|
|
11708
|
+
// the focus permanently, which is the symptom this whole effect exists to prevent.
|
|
11709
|
+
if (active && active !== document.body) {
|
|
11710
|
+
this.focusBeforeLoading = host.contains(active) ? active : null;
|
|
11711
|
+
}
|
|
11712
|
+
return;
|
|
11713
|
+
}
|
|
11714
|
+
const previous = this.focusBeforeLoading;
|
|
11715
|
+
if (!previous) {
|
|
11716
|
+
return;
|
|
11717
|
+
}
|
|
11718
|
+
this.focusBeforeLoading = null;
|
|
11719
|
+
afterNextRender(() => {
|
|
11720
|
+
// Never steal focus back from wherever the user moved on to.
|
|
11721
|
+
if (document.activeElement !== document.body) {
|
|
11722
|
+
return;
|
|
11723
|
+
}
|
|
11724
|
+
if (previous.isConnected) {
|
|
11725
|
+
previous.focus();
|
|
11726
|
+
if (document.activeElement === previous) {
|
|
11727
|
+
return;
|
|
11728
|
+
}
|
|
11729
|
+
// Focus was refused. `loading()` discriminates why: true means a second load
|
|
11730
|
+
// re-applied `inert` before this hook ran, so keep the element for next time.
|
|
11731
|
+
// False means it is simply no longer focusable — a `<th>` reused for a
|
|
11732
|
+
// non-sortable column (the header `@for` tracks `$index`), a projected action
|
|
11733
|
+
// that came back `disabled` — and re-arming there would retry the same dead
|
|
11734
|
+
// element on every load, parking the user on `<body>` indefinitely. Reading the
|
|
11735
|
+
// signal here is untracked, so it costs nothing.
|
|
11736
|
+
if (this.loading()) {
|
|
11737
|
+
this.focusBeforeLoading = previous;
|
|
11738
|
+
return;
|
|
11739
|
+
}
|
|
11740
|
+
}
|
|
11741
|
+
// The remembered element is gone: a reload dropped the focused row under an
|
|
11742
|
+
// id-based `trackBy`, or the container flipped to cards mid-load. Keep focus in the
|
|
11743
|
+
// region rather than leaving the user at the top of the document.
|
|
11744
|
+
if (!host.hasAttribute('tabindex')) {
|
|
11745
|
+
host.setAttribute('tabindex', '-1');
|
|
11746
|
+
}
|
|
11747
|
+
host.focus();
|
|
11748
|
+
}, { injector: this.injector });
|
|
11749
|
+
});
|
|
11750
|
+
// Measure the host width to drive card/scroll mode. The initial read is
|
|
11751
|
+
// taken in `afterNextRender` (guaranteed post-layout, so we get the real
|
|
11752
|
+
// width rather than a pre-layout 0), then a `ResizeObserver` keeps it in
|
|
11753
|
+
// sync as the container resizes. Both are browser-only — `afterNextRender`
|
|
11754
|
+
// does not run during SSR and `ResizeObserver` is feature-detected.
|
|
11755
|
+
afterNextRender(() => {
|
|
11756
|
+
const host = this.elementRef.nativeElement;
|
|
11757
|
+
this.measureContainer(host);
|
|
11758
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
11759
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
11760
|
+
const width = entries[0]?.contentRect.width;
|
|
11761
|
+
if (typeof width === 'number') {
|
|
11762
|
+
// `|| Infinity` for the same reason `measureContainer` applies it: a 0 width
|
|
11763
|
+
// means unmeasurable, not narrow. An element inside a `display: none`
|
|
11764
|
+
// container reports 0x0, so without this a table in a tab that gets hidden
|
|
11765
|
+
// flips to card mode and tears down anything keyed off `isCardMode()` — the
|
|
11766
|
+
// open `<details>` state, for one — for a resize that never happened.
|
|
11767
|
+
this.containerWidth.set(width || Infinity);
|
|
11768
|
+
}
|
|
11769
|
+
});
|
|
11770
|
+
this.resizeObserver.observe(host);
|
|
11771
|
+
}
|
|
11772
|
+
});
|
|
11773
|
+
}
|
|
11774
|
+
/**
|
|
11775
|
+
* Reads the host's current **content-box** width into `containerWidth`.
|
|
11776
|
+
*
|
|
11777
|
+
* Content box specifically, to match the `ResizeObserver` callback's `contentRect`. The
|
|
11778
|
+
* initial read used `getBoundingClientRect()` — the border box — so the two paths measured
|
|
11779
|
+
* different quantities, and `[bordered]` puts a 1px border on each side: a bordered table
|
|
11780
|
+
* whose content box is 639px rendered as a table on first paint (641 is not < 640) and
|
|
11781
|
+
* flipped to cards on the observer's first callback, with no resize having happened. Host
|
|
11782
|
+
* padding widens the gap further.
|
|
11783
|
+
*/
|
|
11784
|
+
measureContainer(host) {
|
|
11785
|
+
// The computed `width` is the *used* content-box width — content box even under
|
|
11786
|
+
// `box-sizing: border-box`, already net of the border and of any vertical scrollbar
|
|
11787
|
+
// gutter, and untransformed. That matches `contentRect.width` down to the fraction,
|
|
11788
|
+
// which is the point: `:host` sets `overflow-x: auto` (which promotes `overflow-y` to
|
|
11789
|
+
// auto), so on a platform with classic scrollbars a border-box width measured ~15px
|
|
11790
|
+
// wider here than the observer did, and `getBoundingClientRect()` reports the
|
|
11791
|
+
// *transformed* size, so a table inside a scaled dialog mis-measured as well.
|
|
11792
|
+
//
|
|
11793
|
+
// `clientWidth` minus horizontal padding gets all of that right too, but it is
|
|
11794
|
+
// integer-rounded, and the rounding is not neutral at the boundary: a 639.6px content
|
|
11795
|
+
// box against the default 640 breakpoint reads as 640 here and 639.6 on the observer's
|
|
11796
|
+
// first callback, so the table renders for a frame and then flips to cards with no
|
|
11797
|
+
// resize having happened — the first-paint flip this measurement exists to remove.
|
|
11798
|
+
//
|
|
11799
|
+
// Only a `px` value is a used width, and the unit check is load-bearing rather than
|
|
11800
|
+
// defensive. `width` resolves to the *used* value only for an element that has a layout
|
|
11801
|
+
// box; without one it resolves to the computed value, and `:host` declares `width: 100%`
|
|
11802
|
+
// — so a table built inside a `display: none` container (an inactive tab body, a
|
|
11803
|
+
// collapsed panel) resolves to the string '100%'. Parsed bare that is the number 100,
|
|
11804
|
+
// which is under every sane `cardBreakpoint`: a container that was never measured would
|
|
11805
|
+
// render as cards. Rejecting the non-px value falls through to `clientWidth`, which is 0
|
|
11806
|
+
// there, and on to the 0 -> Infinity guard below — the same answer the `ResizeObserver`
|
|
11807
|
+
// callback gives a 0x0 contentRect. jsdom takes this path too: with no rule matching the
|
|
11808
|
+
// host it resolves `width` to the empty string rather than filling in an initial value
|
|
11809
|
+
// (checked against jsdom 26.1.0, the version jest-environment-jsdom resolves here).
|
|
11810
|
+
const hostStyle = getComputedStyle(host);
|
|
11811
|
+
const resolvedWidth = hostStyle.width;
|
|
11812
|
+
const usedWidth = resolvedWidth.endsWith('px') ? parseFloat(resolvedWidth) : NaN;
|
|
11813
|
+
let contentWidth;
|
|
11814
|
+
if (Number.isFinite(usedWidth)) {
|
|
11815
|
+
contentWidth = usedWidth;
|
|
11816
|
+
}
|
|
11817
|
+
else {
|
|
11818
|
+
const padding = parseFloat(hostStyle.paddingLeft || '0') + parseFloat(hostStyle.paddingRight || '0');
|
|
11819
|
+
contentWidth = host.clientWidth - (Number.isNaN(padding) ? 0 : padding);
|
|
11820
|
+
}
|
|
11821
|
+
this.containerWidth.set(contentWidth > 0 ? contentWidth : Infinity);
|
|
11431
11822
|
}
|
|
11432
11823
|
ngOnInit() {
|
|
11433
11824
|
this.initialized = true;
|
|
11434
|
-
// Clean up SelectionModel on destroy
|
|
11435
11825
|
this.destroyRef.onDestroy(() => {
|
|
11436
11826
|
this.selection.clear();
|
|
11827
|
+
this.resizeObserver?.disconnect();
|
|
11437
11828
|
});
|
|
11438
11829
|
}
|
|
11830
|
+
// --- Responsive computeds ---
|
|
11831
|
+
/** True when the layout should collapse rows into cards. */
|
|
11832
|
+
isCardMode = computed(() => this.mobileLayout() === 'cards' && this.containerWidth() < this.cardBreakpoint(), ...(ngDevMode ? [{ debugName: "isCardMode" }] : []));
|
|
11833
|
+
/** True when the layout should keep the table but pin edge columns and scroll. */
|
|
11834
|
+
isScrollMode = computed(() => this.mobileLayout() === 'scroll' && this.containerWidth() < this.cardBreakpoint(), ...(ngDevMode ? [{ debugName: "isScrollMode" }] : []));
|
|
11439
11835
|
// --- Computed ---
|
|
11440
11836
|
data = computed(() => {
|
|
11441
11837
|
const source = this.dataSource();
|
|
@@ -11454,6 +11850,13 @@ class TnTableComponent {
|
|
|
11454
11850
|
}
|
|
11455
11851
|
return cols;
|
|
11456
11852
|
}, ...(ngDevMode ? [{ debugName: "effectiveDisplayedColumns" }] : []));
|
|
11853
|
+
/**
|
|
11854
|
+
* Every column the table actually renders, including the trailing actions column, which
|
|
11855
|
+
* `effectiveDisplayedColumns` does not track because it comes from a content template rather
|
|
11856
|
+
* than `displayedColumns`. Used for the detail row's `colspan`, so a detail row spans the full
|
|
11857
|
+
* width no matter which structural columns are on.
|
|
11858
|
+
*/
|
|
11859
|
+
totalColumnCount = computed(() => this.effectiveDisplayedColumns().length + (this.rowActionsDef() ? 1 : 0), ...(ngDevMode ? [{ debugName: "totalColumnCount" }] : []));
|
|
11457
11860
|
isAllSelected = computed(() => {
|
|
11458
11861
|
const numSelected = this.selectionCount();
|
|
11459
11862
|
const numRows = this.data().length;
|
|
@@ -11476,13 +11879,15 @@ class TnTableComponent {
|
|
|
11476
11879
|
if (!colDef?.sortable()) {
|
|
11477
11880
|
return;
|
|
11478
11881
|
}
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
|
|
11882
|
+
// `&& this.sortDirection()` matters: a column set with an empty direction is *not* sorted, and
|
|
11883
|
+
// that state is reachable through the two-way `[(sortColumn)]` binding — a consumer restoring
|
|
11884
|
+
// only the column lands in it. Without the guard neither inner branch matched, so the click
|
|
11885
|
+
// mutated nothing and still emitted a "cleared" event, leaving a header the user could click
|
|
11886
|
+
// forever with no arrow to explain it. Folding it into the not-sorted branch starts a fresh
|
|
11887
|
+
// ascending sort, which is what clicking an unsorted header does everywhere else.
|
|
11888
|
+
if (this.sortColumn() === column && this.sortDirection()) {
|
|
11889
|
+
this.sortDirection.set(this.sortDirection() === 'asc' ? 'desc' : '');
|
|
11890
|
+
if (!this.sortDirection()) {
|
|
11486
11891
|
this.sortColumn.set('');
|
|
11487
11892
|
}
|
|
11488
11893
|
}
|
|
@@ -11490,8 +11895,12 @@ class TnTableComponent {
|
|
|
11490
11895
|
this.sortColumn.set(column);
|
|
11491
11896
|
this.sortDirection.set('asc');
|
|
11492
11897
|
}
|
|
11898
|
+
// Emits the *resulting* sort state, so clearing a sort reports `{ column: '', direction: '' }`
|
|
11899
|
+
// rather than naming the column that was just cleared. Card mode's `setSortColumn('')` has
|
|
11900
|
+
// always emitted the empty column, and a consumer keying off `event.column` should not get a
|
|
11901
|
+
// different answer depending on which layout the container width happens to be showing.
|
|
11493
11902
|
this.sortChange.emit({
|
|
11494
|
-
column: this.sortColumn()
|
|
11903
|
+
column: this.sortColumn(),
|
|
11495
11904
|
direction: this.sortDirection(),
|
|
11496
11905
|
});
|
|
11497
11906
|
}
|
|
@@ -11542,6 +11951,21 @@ class TnTableComponent {
|
|
|
11542
11951
|
isRowExpandTrigger(row) {
|
|
11543
11952
|
return this.expandOnRowClick() && this.clickable() && this.canExpandRow(row);
|
|
11544
11953
|
}
|
|
11954
|
+
/**
|
|
11955
|
+
* Card-mode counterpart of {@link isRowExpandTrigger}, additionally requiring a detail
|
|
11956
|
+
* template. Card mode renders no expansion affordance at all without one, so `aria-expanded`
|
|
11957
|
+
* on the card would advertise a state change that produces nothing.
|
|
11958
|
+
*
|
|
11959
|
+
* Table mode deliberately keeps the looser check: its rows are asserted to carry
|
|
11960
|
+
* `aria-expanded` from `expandOnRowClick` alone, and that predates this layout.
|
|
11961
|
+
*/
|
|
11962
|
+
isCardExpandTrigger(row) {
|
|
11963
|
+
return this.isRowExpandTrigger(row) && !!this.detailRowDef();
|
|
11964
|
+
}
|
|
11965
|
+
/** DOM id for a row's detail panel, so the expand trigger can point `aria-controls` at it. */
|
|
11966
|
+
detailPanelId(rowIndex) {
|
|
11967
|
+
return `${this.instanceId}-detail-${rowIndex}`;
|
|
11968
|
+
}
|
|
11545
11969
|
// --- Active row ---
|
|
11546
11970
|
isRowActive(row) {
|
|
11547
11971
|
if (this.activeWhen()?.(row)) {
|
|
@@ -11551,25 +11975,47 @@ class TnTableComponent {
|
|
|
11551
11975
|
return active !== null && active === row;
|
|
11552
11976
|
}
|
|
11553
11977
|
// --- Row click ---
|
|
11554
|
-
|
|
11978
|
+
/**
|
|
11979
|
+
* @param row The activated row.
|
|
11980
|
+
* @param event The originating DOM event, when there is one. Optional so a consumer (or a
|
|
11981
|
+
* test) can still activate a row programmatically; the control-target guard only applies
|
|
11982
|
+
* to real events, which is the only case where a projected control can be the source.
|
|
11983
|
+
*/
|
|
11984
|
+
onRowClick(row, event) {
|
|
11555
11985
|
if (!this.clickable()) {
|
|
11556
11986
|
return;
|
|
11557
11987
|
}
|
|
11988
|
+
// Mirrors `onCardClick`. Only the select, expand and actions cells stop `click`
|
|
11989
|
+
// themselves, so a control projected into an ordinary `tnCellDef` — a checkbox, a
|
|
11990
|
+
// link — used to activate the row as well: `rowClick` fired and, under
|
|
11991
|
+
// `expandOnRowClick`, the detail panel toggled out from under the user. Card mode
|
|
11992
|
+
// never did that, so the same consumer template behaved differently either side of
|
|
11993
|
+
// `cardBreakpoint`.
|
|
11994
|
+
if (event && this.isRowControlTarget(event)) {
|
|
11995
|
+
return;
|
|
11996
|
+
}
|
|
11558
11997
|
if (this.expandOnRowClick()) {
|
|
11559
11998
|
this.toggleRowExpansion(row);
|
|
11560
11999
|
}
|
|
11561
12000
|
this.rowClick.emit(row);
|
|
11562
12001
|
}
|
|
11563
|
-
|
|
12002
|
+
/** @param event See {@link onRowClick} — same guard, same optionality. */
|
|
12003
|
+
onRowDoubleClick(row, event) {
|
|
11564
12004
|
if (!this.clickable()) {
|
|
11565
12005
|
return;
|
|
11566
12006
|
}
|
|
12007
|
+
if (event && this.isRowControlTarget(event)) {
|
|
12008
|
+
return;
|
|
12009
|
+
}
|
|
11567
12010
|
this.rowDoubleClick.emit(row);
|
|
11568
12011
|
}
|
|
11569
12012
|
onRowKeydown(event, row) {
|
|
11570
12013
|
if (!this.clickable()) {
|
|
11571
12014
|
return;
|
|
11572
12015
|
}
|
|
12016
|
+
if (this.isRowControlTarget(event)) {
|
|
12017
|
+
return;
|
|
12018
|
+
}
|
|
11573
12019
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
11574
12020
|
event.preventDefault();
|
|
11575
12021
|
if (this.expandOnRowClick()) {
|
|
@@ -11578,6 +12024,135 @@ class TnTableComponent {
|
|
|
11578
12024
|
this.rowClick.emit(row);
|
|
11579
12025
|
}
|
|
11580
12026
|
}
|
|
12027
|
+
// --- Card activation ---
|
|
12028
|
+
// Cards reuse `rowClick`, but the card header embeds its own controls
|
|
12029
|
+
// (selection checkbox, row actions, "more"/detail toggles). Activating the
|
|
12030
|
+
// card must ignore clicks/keys that originate from those controls so a tap on
|
|
12031
|
+
// an action button doesn't also fire `rowClick`.
|
|
12032
|
+
onCardClick(event, row) {
|
|
12033
|
+
if (!this.clickable() || this.isCardControlTarget(event)) {
|
|
12034
|
+
return;
|
|
12035
|
+
}
|
|
12036
|
+
if (this.expandOnRowClick()) {
|
|
12037
|
+
this.toggleRowExpansion(row);
|
|
12038
|
+
}
|
|
12039
|
+
this.rowClick.emit(row);
|
|
12040
|
+
}
|
|
12041
|
+
/** Card-mode counterpart of {@link onRowDoubleClick}, with the same guard. */
|
|
12042
|
+
onCardDoubleClick(event, row) {
|
|
12043
|
+
if (!this.clickable() || this.isCardControlTarget(event)) {
|
|
12044
|
+
return;
|
|
12045
|
+
}
|
|
12046
|
+
this.rowDoubleClick.emit(row);
|
|
12047
|
+
}
|
|
12048
|
+
onCardKeydown(event, row) {
|
|
12049
|
+
if (!this.clickable()) {
|
|
12050
|
+
return;
|
|
12051
|
+
}
|
|
12052
|
+
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
12053
|
+
return;
|
|
12054
|
+
}
|
|
12055
|
+
if (this.isCardControlTarget(event)) {
|
|
12056
|
+
return;
|
|
12057
|
+
}
|
|
12058
|
+
event.preventDefault();
|
|
12059
|
+
if (this.expandOnRowClick()) {
|
|
12060
|
+
this.toggleRowExpansion(row);
|
|
12061
|
+
}
|
|
12062
|
+
this.rowClick.emit(row);
|
|
12063
|
+
}
|
|
12064
|
+
/** Anything focusable or otherwise interactive, regardless of layout. */
|
|
12065
|
+
static INTERACTIVE_SELECTOR = 'a[href], button, input, select, textarea, summary,'
|
|
12066
|
+
+ ' [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])';
|
|
12067
|
+
/**
|
|
12068
|
+
* Whether an event originated on a control *within* the activation surface rather than on
|
|
12069
|
+
* the surface itself.
|
|
12070
|
+
*
|
|
12071
|
+
* @param event The DOM event; its `currentTarget` is the activation surface.
|
|
12072
|
+
* @param containerSelectors Layout-specific wrappers that count as controls.
|
|
12073
|
+
*/
|
|
12074
|
+
isControlTarget(event, containerSelectors) {
|
|
12075
|
+
const target = event.target;
|
|
12076
|
+
const selector = containerSelectors
|
|
12077
|
+
? `${TnTableComponent.INTERACTIVE_SELECTOR}, ${containerSelectors}`
|
|
12078
|
+
: TnTableComponent.INTERACTIVE_SELECTOR;
|
|
12079
|
+
const match = target?.closest(selector);
|
|
12080
|
+
// The row/card itself is focusable when `clickable`, so it matches the focusable clause.
|
|
12081
|
+
// It is the activation surface, not a control within it.
|
|
12082
|
+
return !!match && match !== event.currentTarget;
|
|
12083
|
+
}
|
|
12084
|
+
/**
|
|
12085
|
+
* Interactive elements plus the card's own controls and its projected detail panel.
|
|
12086
|
+
*
|
|
12087
|
+
* A class allowlist is not enough: the detail panel renders consumer content inside the
|
|
12088
|
+
* clickable card, so an allowlist lets a click on a projected button bubble up and fire
|
|
12089
|
+
* `rowClick` — and with `expandOnRowClick`, collapse the very panel being used. Table mode
|
|
12090
|
+
* never had that problem, because there the detail row is a sibling `<tr>` outside the
|
|
12091
|
+
* clickable row.
|
|
12092
|
+
*
|
|
12093
|
+
* The card's own field values are deliberately not interactive, so they still activate the
|
|
12094
|
+
* card whether primary or folded under "More fields".
|
|
12095
|
+
*/
|
|
12096
|
+
isCardControlTarget(event) {
|
|
12097
|
+
return this.isControlTarget(event, '.tn-table__card-actions, .tn-table__card-select, .tn-table__card-more-summary,'
|
|
12098
|
+
+ ' .tn-table__card-detail-toggle, .tn-table__card-detail');
|
|
12099
|
+
}
|
|
12100
|
+
/**
|
|
12101
|
+
* Table-mode counterpart. The cells stop `click` themselves, but nothing stopped `keydown`,
|
|
12102
|
+
* so Enter on a projected action button bubbled to the row and got `preventDefault()`d —
|
|
12103
|
+
* which *is* how Enter activates a `<button>`, so the consumer's handler never ran and
|
|
12104
|
+
* `rowClick` fired for the row instead. Card mode has always guarded this; the two layouts
|
|
12105
|
+
* disagreed about what a projected control does under the keyboard.
|
|
12106
|
+
*/
|
|
12107
|
+
isRowControlTarget(event) {
|
|
12108
|
+
return this.isControlTarget(event, '.tn-table__select-cell, .tn-table__expand-cell, .tn-table__actions-cell');
|
|
12109
|
+
}
|
|
12110
|
+
/**
|
|
12111
|
+
* Header-cell activation, guarded like the row and the card.
|
|
12112
|
+
*
|
|
12113
|
+
* A `<th>` is an activation surface too, and a control projected through
|
|
12114
|
+
* `tnHeaderCellDef` broke both ways without this: clicking a filter button re-sorted the
|
|
12115
|
+
* table as well as running the button's own handler, and Space on a checkbox in a
|
|
12116
|
+
* *non-sortable* header was swallowed entirely — `onSortClick` returned early on the
|
|
12117
|
+
* missing `sortable()`, but the template's `preventDefault()` ran regardless, so the
|
|
12118
|
+
* control was simply dead.
|
|
12119
|
+
*
|
|
12120
|
+
* @param column The column the header belongs to.
|
|
12121
|
+
* @param event The originating DOM event.
|
|
12122
|
+
*/
|
|
12123
|
+
onSortHeaderClick(column, event) {
|
|
12124
|
+
if (this.isControlTarget(event, '')) {
|
|
12125
|
+
return;
|
|
12126
|
+
}
|
|
12127
|
+
if (!this.getColumnDef(column)?.sortable()) {
|
|
12128
|
+
return;
|
|
12129
|
+
}
|
|
12130
|
+
this.onSortClick(column);
|
|
12131
|
+
}
|
|
12132
|
+
/**
|
|
12133
|
+
* Keyboard counterpart of {@link onSortHeaderClick}.
|
|
12134
|
+
*
|
|
12135
|
+
* Takes `Event` rather than `KeyboardEvent` because Angular types `$event` as `Event` for
|
|
12136
|
+
* the `keydown.enter` / `keydown.space` pseudo-events; narrowing happens here.
|
|
12137
|
+
*/
|
|
12138
|
+
onSortHeaderKeydown(column, event) {
|
|
12139
|
+
if (this.isControlTarget(event, '')) {
|
|
12140
|
+
return;
|
|
12141
|
+
}
|
|
12142
|
+
if (!this.getColumnDef(column)?.sortable()) {
|
|
12143
|
+
return;
|
|
12144
|
+
}
|
|
12145
|
+
// Only swallow the key once we know this header owns it, so Space on a projected
|
|
12146
|
+
// control still reaches that control.
|
|
12147
|
+
if (event.key === ' ') {
|
|
12148
|
+
event.preventDefault();
|
|
12149
|
+
}
|
|
12150
|
+
this.onSortClick(column);
|
|
12151
|
+
}
|
|
12152
|
+
/** Handles the card-mode sort `<select>` change. */
|
|
12153
|
+
onSortSelectChange(event) {
|
|
12154
|
+
this.setSortColumn(event.target.value);
|
|
12155
|
+
}
|
|
11581
12156
|
// --- Selection methods ---
|
|
11582
12157
|
toggleSelectAll() {
|
|
11583
12158
|
if (this.isAllSelected()) {
|
|
@@ -11597,15 +12172,138 @@ class TnTableComponent {
|
|
|
11597
12172
|
isRowSelected(row) {
|
|
11598
12173
|
return this.selection.isSelected(row);
|
|
11599
12174
|
}
|
|
12175
|
+
// --- Card-mode computeds ---
|
|
12176
|
+
/**
|
|
12177
|
+
* The column rendered as the card title. The first `displayedColumns` entry
|
|
12178
|
+
* whose def sets `cardTitle`, else the first column not marked `cardHidden` —
|
|
12179
|
+
* a column the consumer asked to keep off the card must not be promoted to its
|
|
12180
|
+
* most prominent slot. Falls back to the first displayed column only when every
|
|
12181
|
+
* column is hidden, since a card still needs a title.
|
|
12182
|
+
*/
|
|
12183
|
+
cardTitleColumn = computed(() => {
|
|
12184
|
+
const cols = this.displayedColumns();
|
|
12185
|
+
const explicit = cols.find((c) => this.getColumnDef(c)?.cardTitle());
|
|
12186
|
+
const visible = cols.find((c) => !this.getColumnDef(c)?.cardHidden());
|
|
12187
|
+
return explicit ?? visible ?? cols[0] ?? '';
|
|
12188
|
+
}, ...(ngDevMode ? [{ debugName: "cardTitleColumn" }] : []));
|
|
12189
|
+
/**
|
|
12190
|
+
* Columns rendered as label/value fields in a card, ordered by descending
|
|
12191
|
+
* `cardPriority` (ties keep `displayedColumns` order). Excludes the title column
|
|
12192
|
+
* and any `cardHidden` columns.
|
|
12193
|
+
*/
|
|
12194
|
+
cardFieldColumns = computed(() => {
|
|
12195
|
+
const title = this.cardTitleColumn();
|
|
12196
|
+
const fields = this.displayedColumns()
|
|
12197
|
+
.map((name, index) => ({ name, index }))
|
|
12198
|
+
.filter(({ name }) => name !== title && !this.getColumnDef(name)?.cardHidden());
|
|
12199
|
+
fields.sort((a, b) => {
|
|
12200
|
+
const pa = this.getColumnDef(a.name)?.cardPriority() ?? 0;
|
|
12201
|
+
const pb = this.getColumnDef(b.name)?.cardPriority() ?? 0;
|
|
12202
|
+
return pb - pa || a.index - b.index;
|
|
12203
|
+
});
|
|
12204
|
+
return fields.map((f) => f.name);
|
|
12205
|
+
}, ...(ngDevMode ? [{ debugName: "cardFieldColumns" }] : []));
|
|
12206
|
+
/** Fields shown directly on the card (up to `cardPrimaryCount`). */
|
|
12207
|
+
cardPrimaryColumns = computed(() => this.cardFieldColumns().slice(0, this.cardPrimaryCount()), ...(ngDevMode ? [{ debugName: "cardPrimaryColumns" }] : []));
|
|
12208
|
+
/** Fields tucked behind the "More fields" disclosure. */
|
|
12209
|
+
cardSecondaryColumns = computed(() => this.cardFieldColumns().slice(this.cardPrimaryCount()), ...(ngDevMode ? [{ debugName: "cardSecondaryColumns" }] : []));
|
|
12210
|
+
/**
|
|
12211
|
+
* Displayed columns that are sortable — populates the card-mode sort menu.
|
|
12212
|
+
*
|
|
12213
|
+
* `cardHidden` columns are excluded, because this menu *is* part of the card layout
|
|
12214
|
+
* and `cardHidden` promises the column is "omitted entirely" from it. Offering one
|
|
12215
|
+
* here would let a user reorder the cards by a value no card shows, with the
|
|
12216
|
+
* direction toggle giving no clue what changed — and such a column often has no
|
|
12217
|
+
* readable label either, since `getCardLabel()` falls back to the raw column name
|
|
12218
|
+
* when the header lives only in a `tnHeaderCellDef` template.
|
|
12219
|
+
*
|
|
12220
|
+
* The title column stays eligible: it is excluded from the *fields*, but it is
|
|
12221
|
+
* displayed, so sorting by it is meaningful.
|
|
12222
|
+
*/
|
|
12223
|
+
sortableColumns = computed(() => {
|
|
12224
|
+
const listed = this.displayedColumns().filter((c) => {
|
|
12225
|
+
const def = this.getColumnDef(c);
|
|
12226
|
+
return !!def?.sortable() && !def.cardHidden();
|
|
12227
|
+
});
|
|
12228
|
+
// Reconciled against the rendered option set, not against one reason for exclusion.
|
|
12229
|
+
// Whatever the active column is, it must have an `<option>`: with none matching, the
|
|
12230
|
+
// browser resets the picker to "Unsorted" while the direction toggle still renders an
|
|
12231
|
+
// arrow beside it, and picking "Unsorted" fires no `change` — it is already the
|
|
12232
|
+
// selected option — so the sort becomes unclearable from card mode. `cardHidden` was
|
|
12233
|
+
// only one way in; `sortColumn` is a two-way `model()`, and a dynamic
|
|
12234
|
+
// `displayedColumns` (a column-visibility toggle) can drop a sorted column or leave
|
|
12235
|
+
// one that was never `sortable()`.
|
|
12236
|
+
const active = this.sortColumn();
|
|
12237
|
+
return active && !listed.includes(active) ? [...listed, active] : listed;
|
|
12238
|
+
}, ...(ngDevMode ? [{ debugName: "sortableColumns" }] : []));
|
|
12239
|
+
/**
|
|
12240
|
+
* Whether the active sort column is one the table can actually sort by.
|
|
12241
|
+
*
|
|
12242
|
+
* `sortableColumns` rescues an active column into the menu so it stays clearable, even
|
|
12243
|
+
* when it is `cardHidden`, absent from `displayedColumns`, or never `sortable()`. That
|
|
12244
|
+
* escape hatch must not hand card mode a capability table mode refuses: clicking a
|
|
12245
|
+
* non-sortable header does nothing, so the direction toggle is hidden — and
|
|
12246
|
+
* {@link toggleSortDirection} refuses — for the same column.
|
|
12247
|
+
*/
|
|
12248
|
+
canSortActiveColumn = computed(() => {
|
|
12249
|
+
const column = this.sortColumn();
|
|
12250
|
+
return !!column && !!this.getColumnDef(column)?.sortable();
|
|
12251
|
+
}, ...(ngDevMode ? [{ debugName: "canSortActiveColumn" }] : []));
|
|
12252
|
+
// --- Card-mode sort ---
|
|
12253
|
+
/**
|
|
12254
|
+
* Sets (or clears, when passed `''`) the active sort column for card mode.
|
|
12255
|
+
* Switching columns resets to ascending, and clearing emits an empty `column` —
|
|
12256
|
+
* both matching {@link onSortClick}, since the same `sortChange` contract should
|
|
12257
|
+
* not depend on which layout the container width happens to be showing.
|
|
12258
|
+
*/
|
|
12259
|
+
setSortColumn(column) {
|
|
12260
|
+
if (!column) {
|
|
12261
|
+
this.sortColumn.set('');
|
|
12262
|
+
this.sortDirection.set('');
|
|
12263
|
+
}
|
|
12264
|
+
else {
|
|
12265
|
+
const changed = this.sortColumn() !== column;
|
|
12266
|
+
this.sortColumn.set(column);
|
|
12267
|
+
if (changed || this.sortDirection() === '') {
|
|
12268
|
+
this.sortDirection.set('asc');
|
|
12269
|
+
}
|
|
12270
|
+
}
|
|
12271
|
+
this.sortChange.emit({ column: this.sortColumn(), direction: this.sortDirection() });
|
|
12272
|
+
}
|
|
12273
|
+
/**
|
|
12274
|
+
* Flips the active sort direction between ascending and descending.
|
|
12275
|
+
*
|
|
12276
|
+
* Starts from ascending, so a column carrying an empty direction — reachable by restoring only
|
|
12277
|
+
* `sortColumn` through its two-way binding — sorts ascending rather than skipping straight to
|
|
12278
|
+
* descending. Same reading of "empty direction means not sorted" as {@link onSortClick}.
|
|
12279
|
+
*/
|
|
12280
|
+
toggleSortDirection() {
|
|
12281
|
+
// Guarded, not just hidden: the control is gone from the template for a non-sortable
|
|
12282
|
+
// active column, and the method refuses too, so the API can't reorder by a column the
|
|
12283
|
+
// table header would ignore.
|
|
12284
|
+
if (!this.canSortActiveColumn()) {
|
|
12285
|
+
return;
|
|
12286
|
+
}
|
|
12287
|
+
this.sortDirection.set(this.sortDirection() === 'asc' ? 'desc' : 'asc');
|
|
12288
|
+
this.sortChange.emit({ column: this.sortColumn(), direction: this.sortDirection() });
|
|
12289
|
+
}
|
|
11600
12290
|
// --- Column helpers ---
|
|
11601
12291
|
getColumnDef(columnName) {
|
|
11602
12292
|
return this.columnDefMap().get(columnName);
|
|
11603
12293
|
}
|
|
12294
|
+
/**
|
|
12295
|
+
* Field label for a column in card mode. Precedence: `cardLabel` override →
|
|
12296
|
+
* shared `label` → the column name.
|
|
12297
|
+
*/
|
|
12298
|
+
getCardLabel(column) {
|
|
12299
|
+
const def = this.getColumnDef(column);
|
|
12300
|
+
return def?.cardLabel() ?? def?.label() ?? column;
|
|
12301
|
+
}
|
|
11604
12302
|
getCellValue(row, column) {
|
|
11605
12303
|
return row[column];
|
|
11606
12304
|
}
|
|
11607
12305
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11608
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTableComponent, isStandalone: true, selector: "tn-table", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayedColumns: { classPropertyName: "displayedColumns", publicName: "displayedColumns", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, emptyDescription: { classPropertyName: "emptyDescription", publicName: "emptyDescription", isSignal: true, isRequired: false, transformFunction: null }, emptyIcon: { classPropertyName: "emptyIcon", publicName: "emptyIcon", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, isRowExpandable: { classPropertyName: "isRowExpandable", publicName: "isRowExpandable", isSignal: true, isRequired: false, transformFunction: null }, activeRow: { classPropertyName: "activeRow", publicName: "activeRow", isSignal: true, isRequired: false, transformFunction: null }, activeWhen: { classPropertyName: "activeWhen", publicName: "activeWhen", isSignal: true, isRequired: false, transformFunction: null }, activeBg: { classPropertyName: "activeBg", publicName: "activeBg", isSignal: true, isRequired: false, transformFunction: null }, activeIndicator: { classPropertyName: "activeIndicator", publicName: "activeIndicator", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMessage: { classPropertyName: "loadingMessage", publicName: "loadingMessage", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null }, expandOnRowClick: { classPropertyName: "expandOnRowClick", publicName: "expandOnRowClick", isSignal: true, isRequired: false, transformFunction: null }, singleExpand: { classPropertyName: "singleExpand", publicName: "singleExpand", isSignal: true, isRequired: false, transformFunction: null }, fixedLayout: { classPropertyName: "fixedLayout", publicName: "fixedLayout", isSignal: true, isRequired: false, transformFunction: null }, minColumnWidth: { classPropertyName: "minColumnWidth", publicName: "minColumnWidth", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, sortColumn: { classPropertyName: "sortColumn", publicName: "sortColumn", isSignal: true, isRequired: false, transformFunction: null }, sortDirection: { classPropertyName: "sortDirection", publicName: "sortDirection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick", rowDoubleClick: "rowDoubleClick", sortColumn: "sortColumnChange", sortDirection: "sortDirectionChange" }, host: { properties: { "class.tn-table--bordered": "bordered()", "class.tn-table--fixed-layout": "fixedLayout()", "class.tn-table--loading": "loading()", "style.--tn-table-active-bg": "activeBg()", "style.--tn-table-active-indicator": "activeIndicator()" }, classAttribute: "tn-table" }, queries: [{ propertyName: "columnDefs", predicate: TnTableColumnDirective, isSignal: true }, { propertyName: "detailRowDef", first: true, predicate: TnDetailRowDefDirective, descendants: true, isSignal: true }], hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<table class=\"tn-table__table\" [style.min-width]=\"resolvedMinWidth()\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? sortDirection() === 'asc' ? 'ascending' : 'descending'\n : null\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"getColumnDef(column)?.sortable() && onSortClick(column)\"\n (keydown.enter)=\"onSortClick(column)\"\n (keydown.space)=\"onSortClick(column); $event.preventDefault()\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n [attr.aria-expanded]=\"isRowExpandTrigger(row) ? isRowExpanded(row) : null\"\n (click)=\"onRowClick(row)\"\n (dblclick)=\"onRowDoubleClick(row)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"effectiveDisplayedColumns().length\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n</table>\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [description]=\"emptyDescription()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative;width:100%;overflow-x:auto}:host(.tn-table--fixed-layout) table{table-layout:fixed;width:100%}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2);border-radius:4px;overflow:hidden}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:normal;overflow-wrap:anywhere;overflow:hidden;padding:4px;margin:-4px}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{width:48px;padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{width:48px;padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;z-index:1;border-radius:4px}.tn-table--loading .tn-table__table{pointer-events:none;-webkit-user-select:none;user-select:none}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnEmptyComponent, selector: "tn-empty", inputs: ["title", "description", "icon", "iconLibrary", "iconSize", "actionText", "bordered", "size"], outputs: ["onAction"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnSpinnerComponent, selector: "tn-spinner", inputs: ["mode", "value", "diameter", "strokeWidth", "ariaLabel", "ariaLabelledby"] }], animations: [
|
|
12306
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTableComponent, isStandalone: true, selector: "tn-table", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayedColumns: { classPropertyName: "displayedColumns", publicName: "displayedColumns", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, emptyDescription: { classPropertyName: "emptyDescription", publicName: "emptyDescription", isSignal: true, isRequired: false, transformFunction: null }, emptyIcon: { classPropertyName: "emptyIcon", publicName: "emptyIcon", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, isRowExpandable: { classPropertyName: "isRowExpandable", publicName: "isRowExpandable", isSignal: true, isRequired: false, transformFunction: null }, activeRow: { classPropertyName: "activeRow", publicName: "activeRow", isSignal: true, isRequired: false, transformFunction: null }, activeWhen: { classPropertyName: "activeWhen", publicName: "activeWhen", isSignal: true, isRequired: false, transformFunction: null }, activeBg: { classPropertyName: "activeBg", publicName: "activeBg", isSignal: true, isRequired: false, transformFunction: null }, activeIndicator: { classPropertyName: "activeIndicator", publicName: "activeIndicator", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMessage: { classPropertyName: "loadingMessage", publicName: "loadingMessage", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null }, expandOnRowClick: { classPropertyName: "expandOnRowClick", publicName: "expandOnRowClick", isSignal: true, isRequired: false, transformFunction: null }, singleExpand: { classPropertyName: "singleExpand", publicName: "singleExpand", isSignal: true, isRequired: false, transformFunction: null }, fixedLayout: { classPropertyName: "fixedLayout", publicName: "fixedLayout", isSignal: true, isRequired: false, transformFunction: null }, minColumnWidth: { classPropertyName: "minColumnWidth", publicName: "minColumnWidth", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, mobileLayout: { classPropertyName: "mobileLayout", publicName: "mobileLayout", isSignal: true, isRequired: false, transformFunction: null }, cardBreakpoint: { classPropertyName: "cardBreakpoint", publicName: "cardBreakpoint", isSignal: true, isRequired: false, transformFunction: null }, cardPrimaryCount: { classPropertyName: "cardPrimaryCount", publicName: "cardPrimaryCount", isSignal: true, isRequired: false, transformFunction: null }, sortColumn: { classPropertyName: "sortColumn", publicName: "sortColumn", isSignal: true, isRequired: false, transformFunction: null }, sortDirection: { classPropertyName: "sortDirection", publicName: "sortDirection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick", rowDoubleClick: "rowDoubleClick", sortColumn: "sortColumnChange", sortDirection: "sortDirectionChange" }, host: { properties: { "class.tn-table--bordered": "bordered()", "class.tn-table--fixed-layout": "fixedLayout()", "class.tn-table--loading": "loading()", "class.tn-table--cards": "isCardMode()", "class.tn-table--scroll": "isScrollMode()", "style.--tn-table-active-bg": "activeBg()", "style.--tn-table-active-indicator": "activeIndicator()" }, classAttribute: "tn-table" }, queries: [{ propertyName: "columnDefs", predicate: TnTableColumnDirective, isSignal: true }, { propertyName: "detailRowDef", first: true, predicate: TnDetailRowDefDirective, descendants: true, isSignal: true }, { propertyName: "rowActionsDef", first: true, predicate: TnRowActionsDefDirective, descendants: true, isSignal: true }], hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "@if (isCardMode()) {\n <!-- Card layout (narrow container) -->\n @if (selectable() || sortableColumns().length > 0) {\n <div class=\"tn-table__cards-toolbar\" [attr.inert]=\"loading() ? '' : null\">\n @if (selectable()) {\n <!--\n A plain <div>, not a <label>: tn-checkbox renders its own <label for>,\n and nesting one label inside another is invalid. The visible text is\n passed through the checkbox's own `label` input so it stays associated.\n\n The handler goes on THIS wrapper, not on <tn-checkbox>: the checkbox is\n `pointer-events: none` (see `__checkbox` in the stylesheet), so it and its\n subtree are outside hit testing and a real click lands here instead. A\n handler bound on the checkbox is never in the event path \u2014 which is how the\n label -> div change silently killed mouse and touch selection while keyboard\n kept working, since Space on the hidden input dispatches its own bubbling\n click. Table mode binds the same handler on its <td> for this reason.\n\n The click/key and focusable lint rules are suppressed rather than satisfied:\n this wrapper is a pointer hit area around a control that is already\n focusable and already handles Space. Giving it `tabindex` and a key handler\n would add a second tab stop and a second activation path for the same\n checkbox, which is worse than the rule it silences.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-table__cards-selectall\"\n (click)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </div>\n }\n @if (sortableColumns().length > 0) {\n <div class=\"tn-table__cards-sort\">\n <tn-icon name=\"mat-sort\" size=\"sm\" aria-hidden=\"true\" />\n <select\n class=\"tn-table__cards-sort-select\"\n aria-label=\"Sort by\"\n [value]=\"sortColumn()\"\n (change)=\"onSortSelectChange($event)\">\n <!--\n `[selected]` on the options, not just `[value]` on the <select>:\n the select's value is bound before the @for options exist, so the\n browser discards it and the binding is never dirty again \u2014 a table\n sorted before card mode engaged would render as \"Unsorted\".\n -->\n <option value=\"\" [selected]=\"!sortColumn()\">Unsorted</option>\n @for (column of sortableColumns(); track column) {\n <option [value]=\"column\" [selected]=\"column === sortColumn()\">\n {{ getCardLabel(column) }}\n </option>\n }\n </select>\n @if (canSortActiveColumn()) {\n <!--\n Gated on the column being genuinely sortable, not merely active:\n `sortableColumns` keeps a non-sortable active column listed so the sort stays\n clearable, and this toggle must not ride along \u2014 card mode would otherwise\n reorder by a column whose table header ignores clicks.\n\n Three states, not two. A column with an empty direction is not sorted \u2014\n reachable through the two-way binding \u2014 and this control used to render an\n up arrow labelled \"Sort descending\" for it, while table mode showed no\n arrow and no `aria-sort` for the same state. It keeps rendering, because\n it is how the user escapes that state, but shows the neutral\n `mat-unfold_more` the table header already uses for unsorted columns.\n\n The label names the action, so it is \"Sort ascending\" both when unsorted\n and when descending \u2014 `toggleSortDirection()` starts at ascending.\n -->\n <button\n type=\"button\"\n class=\"tn-table__cards-sort-dir\"\n [attr.aria-label]=\"sortDirection() === 'asc' ? 'Sort descending' : 'Sort ascending'\"\n [attr.data-sort-direction]=\"sortDirection()\"\n (click)=\"toggleSortDirection()\">\n @if (sortDirection() === 'asc') {\n <tn-icon size=\"sm\" name=\"mat-arrow_upward\" aria-hidden=\"true\" />\n } @else if (sortDirection() === 'desc') {\n <tn-icon size=\"sm\" name=\"mat-arrow_downward\" aria-hidden=\"true\" />\n } @else {\n <tn-icon size=\"sm\" name=\"mat-unfold_more\" aria-hidden=\"true\" />\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!--\n Decorative `tn-icon`s carry `aria-hidden=\"true\"`. tn-icon renders `role=\"img\"` and\n falls back to its `name` for the accessible name, so without this the raw token\n leaks: a standalone \"mat-sort\" image beside Select all, and disclosures announced\n as \"More fields mat-keyboard_arrow_down\". The repo convention is the same \u2014 see\n button, card, input and file-picker-popup.\n -->\n <ng-template #cardField let-column let-row=\"row\">\n <div class=\"tn-table__card-field\" [attr.data-column]=\"column\">\n <dt class=\"tn-table__card-field-label\">{{ getCardLabel(column) }}</dt>\n <dd class=\"tn-table__card-field-value\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n {{ getCellValue(row, column) }}\n }\n </dd>\n </div>\n </ng-template>\n\n <div class=\"tn-table__cards\" role=\"list\" [attr.inert]=\"loading() ? '' : null\">\n <!--\n Card ARIA differs from the table row on purpose, because `listitem` supports\n far less than `row` does:\n - Active state uses `aria-current`, not `aria-selected`, which `listitem`\n does not support (an earlier revision set it here and it was dropped for\n that reason; axe reports it as a critical aria-allowed-attr violation).\n - Selection state stays on the checkbox, which is a real control.\n - `aria-expanded` IS permitted here \u2014 `listitem` is a subclass of the\n abstract `section` role, and `section` supports `aria-expanded`, so\n `listitem` inherits it (axe-core's own role table lists it in\n `listitem`'s allowedAttrs alongside aria-level/posinset/setsize). So the\n card carries it whenever the card itself acts as the expand trigger,\n matching what table mode does for its rows. The \"Details\" button reports\n the same state unconditionally, because it is the control a screen-reader\n user actually tabs to \u2014 see the note above it.\n The card is deliberately NOT `role=\"button\"`: that role has presentational\n children, which would hide the card's checkbox, row actions and \"Details\"\n toggle from assistive tech entirely.\n -->\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <div\n class=\"tn-table__card\"\n role=\"listitem\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__card--active]=\"isRowActive(row)\"\n [class.tn-table__card--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-current]=\"isRowActive(row) ? 'true' : null\"\n [attr.aria-expanded]=\"isCardExpandTrigger(row) ? isRowExpanded(row) : null\"\n [attr.aria-controls]=\"isCardExpandTrigger(row) && isRowExpanded(row) ? detailPanelId(rowIdx) : null\"\n (click)=\"onCardClick($event, row)\"\n (dblclick)=\"onCardDoubleClick($event, row)\"\n (keydown)=\"onCardKeydown($event, row)\">\n <div class=\"tn-table__card-header\">\n @if (selectable()) {\n <!--\n <div>, not <label>, and the handler is on the wrapper rather than the\n checkbox \u2014 see the toolbar's select-all above for both reasons.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-table__card-select\"\n (click)=\"toggleRowSelection(row); $event.preventDefault(); $event.stopPropagation()\">\n <!--\n Named from the card's title, not \"Select row N\": with `hideLabel` this is the\n checkbox's entire accessible name, and a card list has no visible row numbers\n to match it against. Falls back to the index when the title column renders\n derived data through a `tnCellDef` and `getCellValue` has nothing to return \u2014\n the same case `getCardLabel()` already falls back for.\n -->\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"\n 'Select ' + (getCellValue(row, cardTitleColumn()) ?? 'row ' + (rowIdx + 1))\n \"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </div>\n }\n <span class=\"tn-table__card-title\">\n @if (getColumnDef(cardTitleColumn())?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: cardTitleColumn() }\" />\n } @else {\n {{ getCellValue(row, cardTitleColumn()) }}\n }\n </span>\n @if (rowActionsDef()) {\n <div class=\"tn-table__card-actions\">\n <ng-container\n [ngTemplateOutlet]=\"rowActionsDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n }\n </div>\n\n <dl class=\"tn-table__card-fields\">\n @for (column of cardPrimaryColumns(); track column) {\n <ng-container\n [ngTemplateOutlet]=\"cardField\"\n [ngTemplateOutletContext]=\"{ $implicit: column, row: row }\" />\n }\n </dl>\n\n <!--\n The disclosure sits outside the primary <dl> and wraps its fields in a\n <dl> of its own: <details> is not a permitted child of <dl>, so nesting\n it there detached the folded dt/dd pairs from any description list.\n -->\n @if (cardSecondaryColumns().length > 0) {\n <details class=\"tn-table__card-more\">\n <summary class=\"tn-table__card-more-summary\">\n <span>More fields</span>\n <tn-icon size=\"sm\" name=\"mat-keyboard_arrow_down\" aria-hidden=\"true\" />\n </summary>\n <dl class=\"tn-table__card-fields\">\n @for (column of cardSecondaryColumns(); track column) {\n <ng-container\n [ngTemplateOutlet]=\"cardField\"\n [ngTemplateOutletContext]=\"{ $implicit: column, row: row }\" />\n }\n </dl>\n </details>\n }\n\n @if (canExpandRow(row) && detailRowDef()) {\n <!--\n The button always carries the state, even when the card is also a trigger.\n An earlier revision suppressed it here to avoid announcing the same panel\n twice, but that muted the primary control: this button is what a\n screen-reader user reaches by tabbing through controls, and it was\n announced as a bare \"Details, button\" with no expanded state, before or\n after activation. The state sat on the `role=\"listitem\"` card, heard only\n if the user happened to focus the card itself.\n\n Two controls that each toggle the same region and each report its state is\n the disclosure pattern applied twice \u2014 redundant, not invalid. Redundancy\n is the cheaper failure.\n -->\n <button\n type=\"button\"\n class=\"tn-table__card-detail-toggle\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-controls]=\"isRowExpanded(row) ? detailPanelId(rowIdx) : null\"\n (click)=\"toggleRowExpansion(row); $event.stopPropagation()\">\n <span>Details</span>\n <tn-icon\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n @if (isRowExpanded(row)) {\n <div class=\"tn-table__card-detail\" [attr.id]=\"detailPanelId(rowIdx)\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n }\n }\n </div>\n }\n </div>\n} @else {\n <table\n class=\"tn-table__table\"\n [style.min-width]=\"resolvedMinWidth()\"\n [attr.inert]=\"loading() ? '' : null\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'ascending' : 'descending')\n : (getColumnDef(column)?.sortable() ? 'none' : null)\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"onSortHeaderClick(column, $event)\"\n (keydown.enter)=\"onSortHeaderKeydown(column, $event)\"\n (keydown.space)=\"onSortHeaderKeydown(column, $event)\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ getColumnDef(column)?.label() ?? column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <!--\n aria-hidden: the <th> has no aria-label, so its accessible name is\n computed from its contents \u2014 without this, every sortable column\n announced as \"Name mat-unfold_more\". `aria-sort` on the <th> already\n carries the state properly, so the token is pure noise.\n -->\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n @if (rowActionsDef()) {\n <th class=\"tn-table__header-cell tn-table__actions-cell\">\n <span class=\"cdk-visually-hidden\">Actions</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n [attr.aria-expanded]=\"isRowExpandTrigger(row) ? isRowExpanded(row) : null\"\n (click)=\"onRowClick(row, $event)\"\n (dblclick)=\"onRowDoubleClick(row, $event)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n aria-hidden=\"true\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n @if (rowActionsDef()) {\n <td class=\"tn-table__cell tn-table__actions-cell\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"tn-table__actions-content\">\n <ng-container\n [ngTemplateOutlet]=\"rowActionsDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n </td>\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"totalColumnCount()\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n}\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [description]=\"emptyDescription()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative;width:100%;overflow-x:auto;border-radius:4px}:host{--tn-table-select-width: 72px;--tn-table-actions-width: 128px;--tn-table-expand-width: 56px}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}:host(.tn-table--fixed-layout) table{table-layout:fixed;width:100%}:host(.tn-table--fixed-layout) .tn-table__actions-cell{box-sizing:border-box;width:var(--tn-table-actions-width)}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}:host(.tn-table--loading) .tn-table__table,:host(.tn-table--loading) .tn-table__cards,:host(.tn-table--loading) .tn-table__cards-toolbar{pointer-events:none;-webkit-user-select:none;user-select:none}:host(.tn-table--scroll) .tn-table__select-cell{position:sticky;left:0}:host(.tn-table--scroll) .tn-table__header-cell:first-child,:host(.tn-table--scroll) .tn-table__row>.tn-table__cell:first-child,:host(.tn-table--scroll) .tn-table__select-cell+.tn-table__header-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{position:sticky;left:0;z-index:1}:host(.tn-table--scroll) .tn-table__actions-cell{position:sticky;right:0;z-index:3}:host(.tn-table--scroll) .tn-table__header-row>.tn-table__header-cell.tn-table__select-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__cell.tn-table__select-cell{z-index:2}:host(.tn-table--scroll) .tn-table__select-cell+.tn-table__header-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{left:var(--tn-table-select-width)}:host(.tn-table--scroll) .tn-table__row>.tn-table__cell:first-child,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{background-color:inherit}:host(.tn-table--scroll) .tn-table__header-row .tn-table__header-cell:first-child:not(.tn-table__header-cell--sortable:hover),:host(.tn-table--scroll) .tn-table__header-row .tn-table__select-cell+.tn-table__header-cell:not(.tn-table__header-cell--sortable:hover){background-color:var(--tn-topbar)}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2)}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon,.tn-table__header-cell--sortable:focus-visible .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease;background-color:var(--tn-bg2)}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:normal;overflow-wrap:anywhere;overflow:hidden;padding:4px;margin:-4px}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{box-sizing:border-box;width:var(--tn-table-select-width);padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{box-sizing:border-box;width:var(--tn-table-expand-width);padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__actions-cell{width:1%;white-space:nowrap;text-align:right;background-color:inherit;border-left:1px solid var(--tn-lines)}.tn-table__header .tn-table__actions-cell{background-color:var(--tn-topbar)}.tn-table__actions-content{display:inline-flex;align-items:center;gap:4px}.tn-table__loading-overlay{position:absolute;inset:0;z-index:4;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;border-radius:4px}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}.tn-table__cards-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 4px 12px}.tn-table__cards-selectall{display:inline-flex;align-items:center;gap:8px;font-size:14px;color:var(--tn-fg1);cursor:pointer}.tn-table__cards-sort{display:inline-flex;align-items:center;gap:6px;margin-left:auto;color:var(--tn-fg2)}.tn-table__cards-sort-select{height:32px;padding:0 8px;border:1px solid var(--tn-lines);border-radius:4px;background-color:var(--tn-bg2);color:var(--tn-fg1);font-size:14px}.tn-table__cards-sort-select:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__cards-sort-dir{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:1px solid var(--tn-lines);border-radius:4px;background:none;color:var(--tn-fg2);cursor:pointer}.tn-table__cards-sort-dir:hover{background-color:var(--tn-alt-bg1)}.tn-table__cards-sort-dir:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__cards{display:flex;flex-direction:column;gap:8px}.tn-table__card{background-color:var(--tn-bg2);border:1px solid var(--tn-lines);border-radius:6px;padding:12px}.tn-table__card--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333));box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__card--clickable{cursor:pointer}.tn-table__card--clickable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--tn-primary)}.tn-table__card--active.tn-table__card--clickable:focus-visible{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__card-header{display:flex;align-items:center;gap:10px}.tn-table__card-select{display:inline-flex;align-items:center;flex-shrink:0;cursor:pointer}.tn-table__card-title{flex:1;min-width:0;font-weight:600;font-size:15px;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-table__card-actions{display:inline-flex;align-items:center;gap:4px;flex-shrink:0}.tn-table__card-fields{margin:0;padding:10px 0 0}.tn-table__card-field{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:3px 0;font-size:13px}.tn-table__card-field-label{flex-shrink:0;color:var(--tn-fg2)}.tn-table__card-field-value{margin:0;min-width:0;text-align:right;color:var(--tn-fg1);overflow-wrap:anywhere}.tn-table__card-more[open]>.tn-table__card-more-summary tn-icon{transform:rotate(180deg)}.tn-table__card-more{margin-top:2px}.tn-table__card-more-summary{display:inline-flex;align-items:center;gap:4px;padding:4px 0;font-size:13px;color:var(--tn-fg1);cursor:pointer;list-style:none}.tn-table__card-more-summary::-webkit-details-marker{display:none}.tn-table__card-more-summary tn-icon{transition:transform .2s ease}.tn-table__card-more-summary:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__card-detail-toggle{display:inline-flex;align-items:center;gap:4px;margin-top:8px;padding:4px 0;background:none;border:none;color:var(--tn-fg1);font-size:13px;cursor:pointer}.tn-table__card-detail-toggle:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__card-detail{margin-top:8px;padding-top:8px;border-top:1px solid var(--tn-lines)}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__card-more-summary tn-icon,.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnEmptyComponent, selector: "tn-empty", inputs: ["title", "description", "icon", "iconLibrary", "iconSize", "actionText", "bordered", "size"], outputs: ["onAction"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnSpinnerComponent, selector: "tn-spinner", inputs: ["mode", "value", "diameter", "strokeWidth", "ariaLabel", "ariaLabelledby"] }], animations: [
|
|
11609
12307
|
trigger('detailExpand', [
|
|
11610
12308
|
state('collapsed,void', style({ height: '0px', minHeight: '0', overflow: 'hidden' })),
|
|
11611
12309
|
state('expanded', style({ height: '*' })),
|
|
@@ -11626,10 +12324,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11626
12324
|
'[class.tn-table--bordered]': 'bordered()',
|
|
11627
12325
|
'[class.tn-table--fixed-layout]': 'fixedLayout()',
|
|
11628
12326
|
'[class.tn-table--loading]': 'loading()',
|
|
12327
|
+
// `--cards` is a state hook only: card mode is styled through the
|
|
12328
|
+
// `__cards*` element classes, not from the host. It is kept as the
|
|
12329
|
+
// documented way for consumers and tests to detect the active layout
|
|
12330
|
+
// without reaching for internal element classes. `--scroll` is both a
|
|
12331
|
+
// hook and the selector the pinned-column rules key off.
|
|
12332
|
+
'[class.tn-table--cards]': 'isCardMode()',
|
|
12333
|
+
'[class.tn-table--scroll]': 'isScrollMode()',
|
|
11629
12334
|
'[style.--tn-table-active-bg]': 'activeBg()',
|
|
11630
12335
|
'[style.--tn-table-active-indicator]': 'activeIndicator()',
|
|
11631
|
-
}, template: "<table class=\"tn-table__table\" [style.min-width]=\"resolvedMinWidth()\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? sortDirection() === 'asc' ? 'ascending' : 'descending'\n : null\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"getColumnDef(column)?.sortable() && onSortClick(column)\"\n (keydown.enter)=\"onSortClick(column)\"\n (keydown.space)=\"onSortClick(column); $event.preventDefault()\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n [attr.aria-expanded]=\"isRowExpandTrigger(row) ? isRowExpanded(row) : null\"\n (click)=\"onRowClick(row)\"\n (dblclick)=\"onRowDoubleClick(row)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"effectiveDisplayedColumns().length\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n</table>\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [description]=\"emptyDescription()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative;width:100%;overflow-x:auto}:host(.tn-table--fixed-layout) table{table-layout:fixed;width:100%}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2);border-radius:4px;overflow:hidden}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:normal;overflow-wrap:anywhere;overflow:hidden;padding:4px;margin:-4px}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{width:48px;padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{width:48px;padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;z-index:1;border-radius:4px}.tn-table--loading .tn-table__table{pointer-events:none;-webkit-user-select:none;user-select:none}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"] }]
|
|
11632
|
-
}], ctorParameters: () => [], propDecorators: { dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayedColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayedColumns", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], emptyDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyDescription", required: false }] }], emptyIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIcon", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], isRowExpandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isRowExpandable", required: false }] }], activeRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeRow", required: false }] }], activeWhen: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeWhen", required: false }] }], activeBg: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeBg", required: false }] }], activeIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndicator", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMessage", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], expandOnRowClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandOnRowClick", required: false }] }], singleExpand: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleExpand", required: false }] }], fixedLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixedLayout", required: false }] }], minColumnWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minColumnWidth", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDoubleClick: [{ type: i0.Output, args: ["rowDoubleClick"] }], columnDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnTableColumnDirective), { isSignal: true }] }], detailRowDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnDetailRowDefDirective), { isSignal: true }] }], sortColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortColumn", required: false }] }, { type: i0.Output, args: ["sortColumnChange"] }], sortDirection: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortDirection", required: false }] }, { type: i0.Output, args: ["sortDirectionChange"] }] } });
|
|
12336
|
+
}, template: "@if (isCardMode()) {\n <!-- Card layout (narrow container) -->\n @if (selectable() || sortableColumns().length > 0) {\n <div class=\"tn-table__cards-toolbar\" [attr.inert]=\"loading() ? '' : null\">\n @if (selectable()) {\n <!--\n A plain <div>, not a <label>: tn-checkbox renders its own <label for>,\n and nesting one label inside another is invalid. The visible text is\n passed through the checkbox's own `label` input so it stays associated.\n\n The handler goes on THIS wrapper, not on <tn-checkbox>: the checkbox is\n `pointer-events: none` (see `__checkbox` in the stylesheet), so it and its\n subtree are outside hit testing and a real click lands here instead. A\n handler bound on the checkbox is never in the event path \u2014 which is how the\n label -> div change silently killed mouse and touch selection while keyboard\n kept working, since Space on the hidden input dispatches its own bubbling\n click. Table mode binds the same handler on its <td> for this reason.\n\n The click/key and focusable lint rules are suppressed rather than satisfied:\n this wrapper is a pointer hit area around a control that is already\n focusable and already handles Space. Giving it `tabindex` and a key handler\n would add a second tab stop and a second activation path for the same\n checkbox, which is worse than the rule it silences.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-table__cards-selectall\"\n (click)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </div>\n }\n @if (sortableColumns().length > 0) {\n <div class=\"tn-table__cards-sort\">\n <tn-icon name=\"mat-sort\" size=\"sm\" aria-hidden=\"true\" />\n <select\n class=\"tn-table__cards-sort-select\"\n aria-label=\"Sort by\"\n [value]=\"sortColumn()\"\n (change)=\"onSortSelectChange($event)\">\n <!--\n `[selected]` on the options, not just `[value]` on the <select>:\n the select's value is bound before the @for options exist, so the\n browser discards it and the binding is never dirty again \u2014 a table\n sorted before card mode engaged would render as \"Unsorted\".\n -->\n <option value=\"\" [selected]=\"!sortColumn()\">Unsorted</option>\n @for (column of sortableColumns(); track column) {\n <option [value]=\"column\" [selected]=\"column === sortColumn()\">\n {{ getCardLabel(column) }}\n </option>\n }\n </select>\n @if (canSortActiveColumn()) {\n <!--\n Gated on the column being genuinely sortable, not merely active:\n `sortableColumns` keeps a non-sortable active column listed so the sort stays\n clearable, and this toggle must not ride along \u2014 card mode would otherwise\n reorder by a column whose table header ignores clicks.\n\n Three states, not two. A column with an empty direction is not sorted \u2014\n reachable through the two-way binding \u2014 and this control used to render an\n up arrow labelled \"Sort descending\" for it, while table mode showed no\n arrow and no `aria-sort` for the same state. It keeps rendering, because\n it is how the user escapes that state, but shows the neutral\n `mat-unfold_more` the table header already uses for unsorted columns.\n\n The label names the action, so it is \"Sort ascending\" both when unsorted\n and when descending \u2014 `toggleSortDirection()` starts at ascending.\n -->\n <button\n type=\"button\"\n class=\"tn-table__cards-sort-dir\"\n [attr.aria-label]=\"sortDirection() === 'asc' ? 'Sort descending' : 'Sort ascending'\"\n [attr.data-sort-direction]=\"sortDirection()\"\n (click)=\"toggleSortDirection()\">\n @if (sortDirection() === 'asc') {\n <tn-icon size=\"sm\" name=\"mat-arrow_upward\" aria-hidden=\"true\" />\n } @else if (sortDirection() === 'desc') {\n <tn-icon size=\"sm\" name=\"mat-arrow_downward\" aria-hidden=\"true\" />\n } @else {\n <tn-icon size=\"sm\" name=\"mat-unfold_more\" aria-hidden=\"true\" />\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!--\n Decorative `tn-icon`s carry `aria-hidden=\"true\"`. tn-icon renders `role=\"img\"` and\n falls back to its `name` for the accessible name, so without this the raw token\n leaks: a standalone \"mat-sort\" image beside Select all, and disclosures announced\n as \"More fields mat-keyboard_arrow_down\". The repo convention is the same \u2014 see\n button, card, input and file-picker-popup.\n -->\n <ng-template #cardField let-column let-row=\"row\">\n <div class=\"tn-table__card-field\" [attr.data-column]=\"column\">\n <dt class=\"tn-table__card-field-label\">{{ getCardLabel(column) }}</dt>\n <dd class=\"tn-table__card-field-value\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n {{ getCellValue(row, column) }}\n }\n </dd>\n </div>\n </ng-template>\n\n <div class=\"tn-table__cards\" role=\"list\" [attr.inert]=\"loading() ? '' : null\">\n <!--\n Card ARIA differs from the table row on purpose, because `listitem` supports\n far less than `row` does:\n - Active state uses `aria-current`, not `aria-selected`, which `listitem`\n does not support (an earlier revision set it here and it was dropped for\n that reason; axe reports it as a critical aria-allowed-attr violation).\n - Selection state stays on the checkbox, which is a real control.\n - `aria-expanded` IS permitted here \u2014 `listitem` is a subclass of the\n abstract `section` role, and `section` supports `aria-expanded`, so\n `listitem` inherits it (axe-core's own role table lists it in\n `listitem`'s allowedAttrs alongside aria-level/posinset/setsize). So the\n card carries it whenever the card itself acts as the expand trigger,\n matching what table mode does for its rows. The \"Details\" button reports\n the same state unconditionally, because it is the control a screen-reader\n user actually tabs to \u2014 see the note above it.\n The card is deliberately NOT `role=\"button\"`: that role has presentational\n children, which would hide the card's checkbox, row actions and \"Details\"\n toggle from assistive tech entirely.\n -->\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <div\n class=\"tn-table__card\"\n role=\"listitem\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__card--active]=\"isRowActive(row)\"\n [class.tn-table__card--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-current]=\"isRowActive(row) ? 'true' : null\"\n [attr.aria-expanded]=\"isCardExpandTrigger(row) ? isRowExpanded(row) : null\"\n [attr.aria-controls]=\"isCardExpandTrigger(row) && isRowExpanded(row) ? detailPanelId(rowIdx) : null\"\n (click)=\"onCardClick($event, row)\"\n (dblclick)=\"onCardDoubleClick($event, row)\"\n (keydown)=\"onCardKeydown($event, row)\">\n <div class=\"tn-table__card-header\">\n @if (selectable()) {\n <!--\n <div>, not <label>, and the handler is on the wrapper rather than the\n checkbox \u2014 see the toolbar's select-all above for both reasons.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-table__card-select\"\n (click)=\"toggleRowSelection(row); $event.preventDefault(); $event.stopPropagation()\">\n <!--\n Named from the card's title, not \"Select row N\": with `hideLabel` this is the\n checkbox's entire accessible name, and a card list has no visible row numbers\n to match it against. Falls back to the index when the title column renders\n derived data through a `tnCellDef` and `getCellValue` has nothing to return \u2014\n the same case `getCardLabel()` already falls back for.\n -->\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"\n 'Select ' + (getCellValue(row, cardTitleColumn()) ?? 'row ' + (rowIdx + 1))\n \"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </div>\n }\n <span class=\"tn-table__card-title\">\n @if (getColumnDef(cardTitleColumn())?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: cardTitleColumn() }\" />\n } @else {\n {{ getCellValue(row, cardTitleColumn()) }}\n }\n </span>\n @if (rowActionsDef()) {\n <div class=\"tn-table__card-actions\">\n <ng-container\n [ngTemplateOutlet]=\"rowActionsDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n }\n </div>\n\n <dl class=\"tn-table__card-fields\">\n @for (column of cardPrimaryColumns(); track column) {\n <ng-container\n [ngTemplateOutlet]=\"cardField\"\n [ngTemplateOutletContext]=\"{ $implicit: column, row: row }\" />\n }\n </dl>\n\n <!--\n The disclosure sits outside the primary <dl> and wraps its fields in a\n <dl> of its own: <details> is not a permitted child of <dl>, so nesting\n it there detached the folded dt/dd pairs from any description list.\n -->\n @if (cardSecondaryColumns().length > 0) {\n <details class=\"tn-table__card-more\">\n <summary class=\"tn-table__card-more-summary\">\n <span>More fields</span>\n <tn-icon size=\"sm\" name=\"mat-keyboard_arrow_down\" aria-hidden=\"true\" />\n </summary>\n <dl class=\"tn-table__card-fields\">\n @for (column of cardSecondaryColumns(); track column) {\n <ng-container\n [ngTemplateOutlet]=\"cardField\"\n [ngTemplateOutletContext]=\"{ $implicit: column, row: row }\" />\n }\n </dl>\n </details>\n }\n\n @if (canExpandRow(row) && detailRowDef()) {\n <!--\n The button always carries the state, even when the card is also a trigger.\n An earlier revision suppressed it here to avoid announcing the same panel\n twice, but that muted the primary control: this button is what a\n screen-reader user reaches by tabbing through controls, and it was\n announced as a bare \"Details, button\" with no expanded state, before or\n after activation. The state sat on the `role=\"listitem\"` card, heard only\n if the user happened to focus the card itself.\n\n Two controls that each toggle the same region and each report its state is\n the disclosure pattern applied twice \u2014 redundant, not invalid. Redundancy\n is the cheaper failure.\n -->\n <button\n type=\"button\"\n class=\"tn-table__card-detail-toggle\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-controls]=\"isRowExpanded(row) ? detailPanelId(rowIdx) : null\"\n (click)=\"toggleRowExpansion(row); $event.stopPropagation()\">\n <span>Details</span>\n <tn-icon\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n @if (isRowExpanded(row)) {\n <div class=\"tn-table__card-detail\" [attr.id]=\"detailPanelId(rowIdx)\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n }\n }\n </div>\n }\n </div>\n} @else {\n <table\n class=\"tn-table__table\"\n [style.min-width]=\"resolvedMinWidth()\"\n [attr.inert]=\"loading() ? '' : null\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'ascending' : 'descending')\n : (getColumnDef(column)?.sortable() ? 'none' : null)\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"onSortHeaderClick(column, $event)\"\n (keydown.enter)=\"onSortHeaderKeydown(column, $event)\"\n (keydown.space)=\"onSortHeaderKeydown(column, $event)\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ getColumnDef(column)?.label() ?? column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <!--\n aria-hidden: the <th> has no aria-label, so its accessible name is\n computed from its contents \u2014 without this, every sortable column\n announced as \"Name mat-unfold_more\". `aria-sort` on the <th> already\n carries the state properly, so the token is pure noise.\n -->\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n @if (rowActionsDef()) {\n <th class=\"tn-table__header-cell tn-table__actions-cell\">\n <span class=\"cdk-visually-hidden\">Actions</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n [attr.aria-expanded]=\"isRowExpandTrigger(row) ? isRowExpanded(row) : null\"\n (click)=\"onRowClick(row, $event)\"\n (dblclick)=\"onRowDoubleClick(row, $event)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n aria-hidden=\"true\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n @if (rowActionsDef()) {\n <td class=\"tn-table__cell tn-table__actions-cell\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"tn-table__actions-content\">\n <ng-container\n [ngTemplateOutlet]=\"rowActionsDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n </td>\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"totalColumnCount()\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n}\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [description]=\"emptyDescription()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative;width:100%;overflow-x:auto;border-radius:4px}:host{--tn-table-select-width: 72px;--tn-table-actions-width: 128px;--tn-table-expand-width: 56px}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}:host(.tn-table--fixed-layout) table{table-layout:fixed;width:100%}:host(.tn-table--fixed-layout) .tn-table__actions-cell{box-sizing:border-box;width:var(--tn-table-actions-width)}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}:host(.tn-table--loading) .tn-table__table,:host(.tn-table--loading) .tn-table__cards,:host(.tn-table--loading) .tn-table__cards-toolbar{pointer-events:none;-webkit-user-select:none;user-select:none}:host(.tn-table--scroll) .tn-table__select-cell{position:sticky;left:0}:host(.tn-table--scroll) .tn-table__header-cell:first-child,:host(.tn-table--scroll) .tn-table__row>.tn-table__cell:first-child,:host(.tn-table--scroll) .tn-table__select-cell+.tn-table__header-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{position:sticky;left:0;z-index:1}:host(.tn-table--scroll) .tn-table__actions-cell{position:sticky;right:0;z-index:3}:host(.tn-table--scroll) .tn-table__header-row>.tn-table__header-cell.tn-table__select-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__cell.tn-table__select-cell{z-index:2}:host(.tn-table--scroll) .tn-table__select-cell+.tn-table__header-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{left:var(--tn-table-select-width)}:host(.tn-table--scroll) .tn-table__row>.tn-table__cell:first-child,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{background-color:inherit}:host(.tn-table--scroll) .tn-table__header-row .tn-table__header-cell:first-child:not(.tn-table__header-cell--sortable:hover),:host(.tn-table--scroll) .tn-table__header-row .tn-table__select-cell+.tn-table__header-cell:not(.tn-table__header-cell--sortable:hover){background-color:var(--tn-topbar)}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2)}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon,.tn-table__header-cell--sortable:focus-visible .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease;background-color:var(--tn-bg2)}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:normal;overflow-wrap:anywhere;overflow:hidden;padding:4px;margin:-4px}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{box-sizing:border-box;width:var(--tn-table-select-width);padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{box-sizing:border-box;width:var(--tn-table-expand-width);padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__actions-cell{width:1%;white-space:nowrap;text-align:right;background-color:inherit;border-left:1px solid var(--tn-lines)}.tn-table__header .tn-table__actions-cell{background-color:var(--tn-topbar)}.tn-table__actions-content{display:inline-flex;align-items:center;gap:4px}.tn-table__loading-overlay{position:absolute;inset:0;z-index:4;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;border-radius:4px}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}.tn-table__cards-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 4px 12px}.tn-table__cards-selectall{display:inline-flex;align-items:center;gap:8px;font-size:14px;color:var(--tn-fg1);cursor:pointer}.tn-table__cards-sort{display:inline-flex;align-items:center;gap:6px;margin-left:auto;color:var(--tn-fg2)}.tn-table__cards-sort-select{height:32px;padding:0 8px;border:1px solid var(--tn-lines);border-radius:4px;background-color:var(--tn-bg2);color:var(--tn-fg1);font-size:14px}.tn-table__cards-sort-select:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__cards-sort-dir{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:1px solid var(--tn-lines);border-radius:4px;background:none;color:var(--tn-fg2);cursor:pointer}.tn-table__cards-sort-dir:hover{background-color:var(--tn-alt-bg1)}.tn-table__cards-sort-dir:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__cards{display:flex;flex-direction:column;gap:8px}.tn-table__card{background-color:var(--tn-bg2);border:1px solid var(--tn-lines);border-radius:6px;padding:12px}.tn-table__card--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333));box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__card--clickable{cursor:pointer}.tn-table__card--clickable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--tn-primary)}.tn-table__card--active.tn-table__card--clickable:focus-visible{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__card-header{display:flex;align-items:center;gap:10px}.tn-table__card-select{display:inline-flex;align-items:center;flex-shrink:0;cursor:pointer}.tn-table__card-title{flex:1;min-width:0;font-weight:600;font-size:15px;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-table__card-actions{display:inline-flex;align-items:center;gap:4px;flex-shrink:0}.tn-table__card-fields{margin:0;padding:10px 0 0}.tn-table__card-field{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:3px 0;font-size:13px}.tn-table__card-field-label{flex-shrink:0;color:var(--tn-fg2)}.tn-table__card-field-value{margin:0;min-width:0;text-align:right;color:var(--tn-fg1);overflow-wrap:anywhere}.tn-table__card-more[open]>.tn-table__card-more-summary tn-icon{transform:rotate(180deg)}.tn-table__card-more{margin-top:2px}.tn-table__card-more-summary{display:inline-flex;align-items:center;gap:4px;padding:4px 0;font-size:13px;color:var(--tn-fg1);cursor:pointer;list-style:none}.tn-table__card-more-summary::-webkit-details-marker{display:none}.tn-table__card-more-summary tn-icon{transition:transform .2s ease}.tn-table__card-more-summary:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__card-detail-toggle{display:inline-flex;align-items:center;gap:4px;margin-top:8px;padding:4px 0;background:none;border:none;color:var(--tn-fg1);font-size:13px;cursor:pointer}.tn-table__card-detail-toggle:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__card-detail{margin-top:8px;padding-top:8px;border-top:1px solid var(--tn-lines)}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__card-more-summary tn-icon,.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"] }]
|
|
12337
|
+
}], ctorParameters: () => [], propDecorators: { dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayedColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayedColumns", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], emptyDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyDescription", required: false }] }], emptyIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIcon", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], isRowExpandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isRowExpandable", required: false }] }], activeRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeRow", required: false }] }], activeWhen: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeWhen", required: false }] }], activeBg: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeBg", required: false }] }], activeIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndicator", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMessage", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], expandOnRowClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandOnRowClick", required: false }] }], singleExpand: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleExpand", required: false }] }], fixedLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixedLayout", required: false }] }], minColumnWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minColumnWidth", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], mobileLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileLayout", required: false }] }], cardBreakpoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardBreakpoint", required: false }] }], cardPrimaryCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardPrimaryCount", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDoubleClick: [{ type: i0.Output, args: ["rowDoubleClick"] }], columnDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnTableColumnDirective), { isSignal: true }] }], detailRowDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnDetailRowDefDirective), { isSignal: true }] }], rowActionsDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnRowActionsDefDirective), { isSignal: true }] }], sortColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortColumn", required: false }] }, { type: i0.Output, args: ["sortColumnChange"] }], sortDirection: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortDirection", required: false }] }, { type: i0.Output, args: ["sortDirectionChange"] }] } });
|
|
11633
12338
|
|
|
11634
12339
|
/**
|
|
11635
12340
|
* Harness for interacting with `tn-table` in tests.
|
|
@@ -11663,18 +12368,59 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11663
12368
|
/**
|
|
11664
12369
|
* Gets the number of data rows (excludes header and detail rows).
|
|
11665
12370
|
*
|
|
11666
|
-
*
|
|
12371
|
+
* Layout-aware: counts rendered cards in the card layout, so the count tracks what the
|
|
12372
|
+
* user sees rather than reporting 0 for a container that happens to be narrow.
|
|
12373
|
+
*
|
|
12374
|
+
* @returns Promise resolving to the row count, or the card count in the card layout.
|
|
11667
12375
|
*/
|
|
11668
12376
|
async getRowCount() {
|
|
11669
|
-
|
|
12377
|
+
// Counts cards in card mode. Answering 0 there was the silent-wrong-answer shape the
|
|
12378
|
+
// selection block rejects, and this is the harness's own headline example — a consumer
|
|
12379
|
+
// that narrows the container and asserts emptiness would pass vacuously.
|
|
12380
|
+
const selector = (await this.isCards()) ? '.tn-table__card' : '.tn-table__row';
|
|
12381
|
+
const rows = await this.locatorForAll(selector)();
|
|
11670
12382
|
return rows.length;
|
|
11671
12383
|
}
|
|
12384
|
+
/**
|
|
12385
|
+
* Throws when the rendered layout is `cards`, for queries that have no card equivalent.
|
|
12386
|
+
* The alternative is an empty array or a 0, which reads as "the table is empty" and
|
|
12387
|
+
* greens a test over a rendered card list — the failure mode the selection and
|
|
12388
|
+
* active-row methods were made layout-aware to avoid.
|
|
12389
|
+
*/
|
|
12390
|
+
async assertTableLayout(method) {
|
|
12391
|
+
if (await this.isCards()) {
|
|
12392
|
+
throw new Error(`TnTableHarness.${method}() has no meaning in the card layout. `
|
|
12393
|
+
+ 'Use the card API instead — getCardCount(), getCardTitle(), getCardFieldValue(), '
|
|
12394
|
+
+ 'toggleCardDetail(), expandCardMoreFields(), getCardSortColumn(), '
|
|
12395
|
+
+ 'getCardSortDirection(), toggleCardSortDirection(), clickCard(), '
|
|
12396
|
+
+ 'doubleClickCard(), pressKeyOnCard(), isCardFocusable() — or widen the container '
|
|
12397
|
+
+ 'above cardBreakpoint.');
|
|
12398
|
+
}
|
|
12399
|
+
}
|
|
12400
|
+
/**
|
|
12401
|
+
* Mirror of {@link assertTableLayout} for the card API. `getCardCount()` answering 0 in
|
|
12402
|
+
* table mode is the same silent-wrong-answer shape rejected for `getRowCount()`: a spec
|
|
12403
|
+
* that forgot to push a sub-breakpoint width would assert emptiness and pass over 20
|
|
12404
|
+
* rendered rows.
|
|
12405
|
+
*/
|
|
12406
|
+
async assertCardLayout(method) {
|
|
12407
|
+
if (!(await this.isCards())) {
|
|
12408
|
+
throw new Error(`TnTableHarness.${method}() has no meaning in the table layout. `
|
|
12409
|
+
+ 'Use the row API instead — getRowCount(), getCellText(), getRowTexts(), '
|
|
12410
|
+
+ 'toggleRowExpansion(), clickSortHeader(), clickRow(), doubleClickRow(), '
|
|
12411
|
+
+ 'pressKeyOnRow(), isRowFocusable() — or narrow the container below '
|
|
12412
|
+
+ 'cardBreakpoint.');
|
|
12413
|
+
}
|
|
12414
|
+
}
|
|
11672
12415
|
/**
|
|
11673
12416
|
* Gets the text content of header cells (excludes sort icons).
|
|
11674
12417
|
*
|
|
11675
12418
|
* @returns Promise resolving to an array of header text strings.
|
|
12419
|
+
* @throws Error in the card layout, which renders no header row;
|
|
12420
|
+
* the thrown message names the card API.
|
|
11676
12421
|
*/
|
|
11677
12422
|
async getHeaderTexts() {
|
|
12423
|
+
await this.assertTableLayout('getHeaderTexts');
|
|
11678
12424
|
const textEls = await this.locatorForAll('.tn-table__header-text')();
|
|
11679
12425
|
const texts = [];
|
|
11680
12426
|
for (const el of textEls) {
|
|
@@ -11687,8 +12433,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11687
12433
|
*
|
|
11688
12434
|
* @param rowIndex Zero-based index of the data row.
|
|
11689
12435
|
* @returns Promise resolving to an array of cell text strings.
|
|
12436
|
+
* @throws Error in the card layout, which renders no rows; the thrown message names the card API.
|
|
11690
12437
|
*/
|
|
11691
12438
|
async getRowTexts(rowIndex) {
|
|
12439
|
+
await this.assertTableLayout('getRowTexts');
|
|
11692
12440
|
await this.assertRowExists(rowIndex);
|
|
11693
12441
|
const cells = await this.locatorForAll(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__cell[data-column]`)();
|
|
11694
12442
|
const texts = [];
|
|
@@ -11703,8 +12451,11 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11703
12451
|
* @param rowIndex Zero-based index of the data row.
|
|
11704
12452
|
* @param columnName The column's data-column attribute value.
|
|
11705
12453
|
* @returns Promise resolving to the cell text.
|
|
12454
|
+
* @throws Error in the card layout, which renders no cells;
|
|
12455
|
+
* the thrown message names the card API.
|
|
11706
12456
|
*/
|
|
11707
12457
|
async getCellText(rowIndex, columnName) {
|
|
12458
|
+
await this.assertTableLayout('getCellText');
|
|
11708
12459
|
await this.assertRowExists(rowIndex);
|
|
11709
12460
|
const cell = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"] [data-column="${columnName}"]`)();
|
|
11710
12461
|
return (await cell.text()).trim();
|
|
@@ -11713,8 +12464,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11713
12464
|
* Gets all row texts as a 2D string array (data-column cells only).
|
|
11714
12465
|
*
|
|
11715
12466
|
* @returns Promise resolving to an array of row text arrays.
|
|
12467
|
+
* @throws Error in the card layout, which renders no rows; the thrown message names the card API.
|
|
11716
12468
|
*/
|
|
11717
12469
|
async getAllRowTexts() {
|
|
12470
|
+
await this.assertTableLayout('getAllRowTexts');
|
|
11718
12471
|
const count = await this.getRowCount();
|
|
11719
12472
|
const result = [];
|
|
11720
12473
|
for (let i = 0; i < count; i++) {
|
|
@@ -11727,8 +12480,11 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11727
12480
|
* Clicks a sortable column header to cycle sort direction.
|
|
11728
12481
|
*
|
|
11729
12482
|
* @param columnName The column's data-column attribute value.
|
|
12483
|
+
* @throws Error in the card layout, which renders no column headers;
|
|
12484
|
+
* the thrown message names the card API.
|
|
11730
12485
|
*/
|
|
11731
12486
|
async clickSortHeader(columnName) {
|
|
12487
|
+
await this.assertTableLayout('clickSortHeader');
|
|
11732
12488
|
const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
|
|
11733
12489
|
await header.click();
|
|
11734
12490
|
}
|
|
@@ -11737,59 +12493,105 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11737
12493
|
*
|
|
11738
12494
|
* @param columnName The column's data-column attribute value.
|
|
11739
12495
|
* @returns Promise resolving to true if the column is sortable.
|
|
12496
|
+
* @throws Error in the card layout, which renders no column headers;
|
|
12497
|
+
* the thrown message names the card API.
|
|
11740
12498
|
*/
|
|
11741
12499
|
async isSortable(columnName) {
|
|
12500
|
+
await this.assertTableLayout('isSortable');
|
|
11742
12501
|
const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
|
|
11743
12502
|
return header.hasClass('tn-table__header-cell--sortable');
|
|
11744
12503
|
}
|
|
11745
12504
|
/**
|
|
11746
12505
|
* Gets the current sort direction for a column via aria-sort.
|
|
11747
12506
|
*
|
|
12507
|
+
* `aria-sort="none"` — which a sortable-but-unsorted header now carries, so it is
|
|
12508
|
+
* discoverable as sortable — is normalised to null, keeping "not sorted" a single value
|
|
12509
|
+
* for callers. Only a non-sortable header has no attribute at all, and both answer null.
|
|
12510
|
+
*
|
|
11748
12511
|
* @param columnName The column's data-column attribute value.
|
|
11749
12512
|
* @returns Promise resolving to 'ascending', 'descending', or null.
|
|
12513
|
+
* @throws Error in the card layout, which renders no column headers;
|
|
12514
|
+
* the thrown message names the card API.
|
|
11750
12515
|
*/
|
|
11751
12516
|
async getSortDirection(columnName) {
|
|
12517
|
+
await this.assertTableLayout('getSortDirection');
|
|
11752
12518
|
const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
|
|
11753
|
-
|
|
12519
|
+
const sort = await header.getAttribute('aria-sort');
|
|
12520
|
+
return sort === 'none' ? null : sort;
|
|
11754
12521
|
}
|
|
11755
12522
|
// --- Selection ---
|
|
12523
|
+
//
|
|
12524
|
+
// Layout-aware, all four of them. Card mode renders no `.tn-table__row` at all —
|
|
12525
|
+
// selection lives in `.tn-table__card .tn-table__card-select` — so a row-based
|
|
12526
|
+
// locator either throws or, worse, answers 0 for a table with rows selected.
|
|
12527
|
+
// Consumers shouldn't have to branch on the rendered layout to ask "is this row
|
|
12528
|
+
// selected", so these resolve the right selectors themselves.
|
|
12529
|
+
//
|
|
12530
|
+
// The click target is the WRAPPER in both layouts (the `<td>`, or the card's
|
|
12531
|
+
// `<div>`): `.tn-table__checkbox` is `pointer-events: none`, so clicking the
|
|
12532
|
+
// checkbox itself exercises a path no user can take.
|
|
12533
|
+
/** True when the container is narrow enough that card mode is rendered. */
|
|
12534
|
+
async isCards() {
|
|
12535
|
+
return (await this.getLayoutMode()) === 'cards';
|
|
12536
|
+
}
|
|
11756
12537
|
/**
|
|
11757
|
-
* Clicks the select-all checkbox in the
|
|
12538
|
+
* Clicks the select-all control — the header checkbox in table mode, the card
|
|
12539
|
+
* toolbar's "Select all" in card mode.
|
|
11758
12540
|
*/
|
|
11759
12541
|
async toggleSelectAll() {
|
|
11760
|
-
const
|
|
11761
|
-
|
|
12542
|
+
const selector = (await this.isCards())
|
|
12543
|
+
? '.tn-table__cards-selectall'
|
|
12544
|
+
: '.tn-table__header-row .tn-table__select-cell';
|
|
12545
|
+
const target = await this.locatorFor(selector)();
|
|
12546
|
+
await target.click();
|
|
11762
12547
|
}
|
|
11763
12548
|
/**
|
|
11764
|
-
* Toggles selection for a specific row by clicking its checkbox
|
|
12549
|
+
* Toggles selection for a specific row (or its card) by clicking its checkbox
|
|
12550
|
+
* wrapper.
|
|
11765
12551
|
*
|
|
11766
12552
|
* @param rowIndex Zero-based index of the data row.
|
|
11767
12553
|
*/
|
|
11768
12554
|
async toggleRowSelection(rowIndex) {
|
|
11769
|
-
await this.
|
|
11770
|
-
const
|
|
11771
|
-
|
|
12555
|
+
await this.assertIndexExists(rowIndex);
|
|
12556
|
+
const selector = (await this.isCards())
|
|
12557
|
+
? `.tn-table__card[data-row-index="${rowIndex}"] .tn-table__card-select`
|
|
12558
|
+
: `.tn-table__row[data-row-index="${rowIndex}"] .tn-table__select-cell`;
|
|
12559
|
+
const target = await this.locatorFor(selector)();
|
|
12560
|
+
await target.click();
|
|
11772
12561
|
}
|
|
11773
12562
|
/**
|
|
11774
|
-
* Checks if a specific row
|
|
12563
|
+
* Checks if a specific row (or its card) is selected.
|
|
11775
12564
|
*
|
|
11776
12565
|
* @param rowIndex Zero-based index of the data row.
|
|
11777
12566
|
* @returns Promise resolving to true if the row's checkbox is checked.
|
|
11778
12567
|
*/
|
|
11779
12568
|
async isRowSelected(rowIndex) {
|
|
11780
|
-
await this.
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
12569
|
+
await this.assertIndexExists(rowIndex);
|
|
12570
|
+
// Scoped to the select cell, matching `toggleRowSelection` and `getSelectedRowCount`.
|
|
12571
|
+
// Resolving against the whole row/card takes the first checkbox in document order, which
|
|
12572
|
+
// is the selection one only while `selectable` is on — with it off and a `tn-checkbox`
|
|
12573
|
+
// projected through `[tnRowActionsDef]`, this reported that checkbox's state instead of
|
|
12574
|
+
// failing to find a selection control.
|
|
12575
|
+
const ancestor = (await this.isCards())
|
|
12576
|
+
? `.tn-table__card[data-row-index="${rowIndex}"] .tn-table__card-select`
|
|
12577
|
+
: `.tn-table__row[data-row-index="${rowIndex}"] .tn-table__select-cell`;
|
|
12578
|
+
const checkbox = await this.locatorFor(TnCheckboxHarness.with({ ancestor }))();
|
|
11784
12579
|
return checkbox.isChecked();
|
|
11785
12580
|
}
|
|
11786
12581
|
/**
|
|
11787
|
-
* Gets the count of currently selected rows.
|
|
12582
|
+
* Gets the count of currently selected rows, in either layout.
|
|
11788
12583
|
*
|
|
11789
12584
|
* @returns Promise resolving to the number of checked row checkboxes.
|
|
11790
12585
|
*/
|
|
11791
12586
|
async getSelectedRowCount() {
|
|
11792
|
-
|
|
12587
|
+
// Scoped to the select cell, not the whole row/card: a checkbox projected through
|
|
12588
|
+
// `[tnRowActionsDef]` sits inside the row, and in card mode the detail panel is a
|
|
12589
|
+
// descendant of the card, so a checkbox in a detail template would inflate the count.
|
|
12590
|
+
// (Table mode escaped the second case only because its detail row is a sibling `<tr>`.)
|
|
12591
|
+
const ancestor = (await this.isCards())
|
|
12592
|
+
? '.tn-table__card-select'
|
|
12593
|
+
: '.tn-table__row .tn-table__select-cell';
|
|
12594
|
+
const checkboxes = await this.locatorForAll(TnCheckboxHarness.with({ ancestor }))();
|
|
11793
12595
|
let count = 0;
|
|
11794
12596
|
for (const cb of checkboxes) {
|
|
11795
12597
|
if (await cb.isChecked()) {
|
|
@@ -11803,8 +12605,11 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11803
12605
|
* Clicks the expand button for a specific row.
|
|
11804
12606
|
*
|
|
11805
12607
|
* @param rowIndex Zero-based index of the data row.
|
|
12608
|
+
* @throws Error in the card layout — use `toggleCardDetail()`;
|
|
12609
|
+
* the thrown message names the card API.
|
|
11806
12610
|
*/
|
|
11807
12611
|
async toggleRowExpansion(rowIndex) {
|
|
12612
|
+
await this.assertTableLayout('toggleRowExpansion');
|
|
11808
12613
|
await this.assertRowExists(rowIndex);
|
|
11809
12614
|
const button = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__expand-button`)();
|
|
11810
12615
|
await button.click();
|
|
@@ -11814,8 +12619,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11814
12619
|
*
|
|
11815
12620
|
* @param rowIndex Zero-based index of the data row.
|
|
11816
12621
|
* @returns Promise resolving to true if the row has the expanded class.
|
|
12622
|
+
* @throws Error in the card layout, which renders no rows; the thrown message names the card API.
|
|
11817
12623
|
*/
|
|
11818
12624
|
async isRowExpanded(rowIndex) {
|
|
12625
|
+
await this.assertTableLayout('isRowExpanded');
|
|
11819
12626
|
await this.assertRowExists(rowIndex);
|
|
11820
12627
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11821
12628
|
return row.hasClass('tn-table__row--expanded');
|
|
@@ -11826,8 +12633,11 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11826
12633
|
*
|
|
11827
12634
|
* @param rowIndex Zero-based index of the data row.
|
|
11828
12635
|
* @returns Promise resolving to true if the row has an expand button.
|
|
12636
|
+
* @throws Error in the card layout, whose expand control is the card's Details button;
|
|
12637
|
+
* the thrown message names the card API.
|
|
11829
12638
|
*/
|
|
11830
12639
|
async hasExpandControl(rowIndex) {
|
|
12640
|
+
await this.assertTableLayout('hasExpandControl');
|
|
11831
12641
|
await this.assertRowExists(rowIndex);
|
|
11832
12642
|
const button = await this.locatorForOptional(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__expand-button`)();
|
|
11833
12643
|
return button !== null;
|
|
@@ -11837,8 +12647,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11837
12647
|
* Clicks a row (for tables with `clickable` enabled).
|
|
11838
12648
|
*
|
|
11839
12649
|
* @param rowIndex Zero-based index of the data row.
|
|
12650
|
+
* @throws Error in the card layout, which renders no rows; use {@link clickCard} there.
|
|
11840
12651
|
*/
|
|
11841
12652
|
async clickRow(rowIndex) {
|
|
12653
|
+
await this.assertTableLayout('clickRow');
|
|
11842
12654
|
await this.assertRowExists(rowIndex);
|
|
11843
12655
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11844
12656
|
await row.click();
|
|
@@ -11849,8 +12661,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11849
12661
|
* clicks first; this helper dispatches only the `dblclick` event.
|
|
11850
12662
|
*
|
|
11851
12663
|
* @param rowIndex Zero-based index of the data row.
|
|
12664
|
+
* @throws Error in the card layout, which renders no rows; use {@link doubleClickCard} there.
|
|
11852
12665
|
*/
|
|
11853
12666
|
async doubleClickRow(rowIndex) {
|
|
12667
|
+
await this.assertTableLayout('doubleClickRow');
|
|
11854
12668
|
await this.assertRowExists(rowIndex);
|
|
11855
12669
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11856
12670
|
await row.dispatchEvent('dblclick');
|
|
@@ -11860,8 +12674,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11860
12674
|
*
|
|
11861
12675
|
* @param rowIndex Zero-based index of the data row.
|
|
11862
12676
|
* @param key Which key to press — Enter or Space.
|
|
12677
|
+
* @throws Error in the card layout, which renders no rows; use {@link pressKeyOnCard} there.
|
|
11863
12678
|
*/
|
|
11864
12679
|
async pressKeyOnRow(rowIndex, key) {
|
|
12680
|
+
await this.assertTableLayout('pressKeyOnRow');
|
|
11865
12681
|
await this.assertRowExists(rowIndex);
|
|
11866
12682
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11867
12683
|
await row.focus();
|
|
@@ -11876,8 +12692,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11876
12692
|
* Checks if a row is keyboard-focusable (tabindex=0).
|
|
11877
12693
|
*
|
|
11878
12694
|
* @param rowIndex Zero-based index of the data row.
|
|
12695
|
+
* @throws Error in the card layout, which renders no rows; use {@link isCardFocusable} there.
|
|
11879
12696
|
*/
|
|
11880
12697
|
async isRowFocusable(rowIndex) {
|
|
12698
|
+
await this.assertTableLayout('isRowFocusable');
|
|
11881
12699
|
await this.assertRowExists(rowIndex);
|
|
11882
12700
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11883
12701
|
return (await row.getAttribute('tabindex')) === '0';
|
|
@@ -11896,10 +12714,17 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11896
12714
|
/**
|
|
11897
12715
|
* Checks if a data row is currently marked active.
|
|
11898
12716
|
*
|
|
11899
|
-
*
|
|
11900
|
-
*
|
|
12717
|
+
* Layout-aware: reads the card's active class in the card layout, which marks the same
|
|
12718
|
+
* state with `aria-current` rather than the row's `aria-selected`.
|
|
12719
|
+
*
|
|
12720
|
+
* @param rowIndex Zero-based index of the data row (or card).
|
|
12721
|
+
* @returns Promise resolving to true if the row — or card — has the active class.
|
|
11901
12722
|
*/
|
|
11902
12723
|
async isRowActive(rowIndex) {
|
|
12724
|
+
if (await this.isCards()) {
|
|
12725
|
+
const card = await this.locatorFor(`.tn-table__card[data-row-index="${rowIndex}"]`)();
|
|
12726
|
+
return card.hasClass('tn-table__card--active');
|
|
12727
|
+
}
|
|
11903
12728
|
await this.assertRowExists(rowIndex);
|
|
11904
12729
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11905
12730
|
return row.hasClass('tn-table__row--active');
|
|
@@ -11910,11 +12735,19 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11910
12735
|
* @returns Promise resolving to the active row index or null.
|
|
11911
12736
|
*/
|
|
11912
12737
|
async getActiveRowIndex() {
|
|
11913
|
-
|
|
11914
|
-
|
|
12738
|
+
// Layout-aware for the same reason as the selection block: card mode marks the active
|
|
12739
|
+
// row as `.tn-table__card--active` and renders no `.tn-table__row` at all, so a
|
|
12740
|
+
// row-only locator answered null — "nothing is active" — over a visibly active card.
|
|
12741
|
+
// Silent wrong answers are worse than a throw; this one could green a test after a
|
|
12742
|
+
// resize.
|
|
12743
|
+
const selector = (await this.isCards())
|
|
12744
|
+
? '.tn-table__card--active'
|
|
12745
|
+
: '.tn-table__row--active';
|
|
12746
|
+
const active = await this.locatorForOptional(selector)();
|
|
12747
|
+
if (!active) {
|
|
11915
12748
|
return null;
|
|
11916
12749
|
}
|
|
11917
|
-
const attr = await
|
|
12750
|
+
const attr = await active.getAttribute('data-row-index');
|
|
11918
12751
|
return attr === null ? null : Number(attr);
|
|
11919
12752
|
}
|
|
11920
12753
|
/**
|
|
@@ -11922,8 +12755,11 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11922
12755
|
*
|
|
11923
12756
|
* @param detailIndex Zero-based index among currently visible detail rows.
|
|
11924
12757
|
* @returns Promise resolving to the detail row text.
|
|
12758
|
+
* @throws Error in the card layout — use the card detail panel;
|
|
12759
|
+
* the thrown message names the card API.
|
|
11925
12760
|
*/
|
|
11926
12761
|
async getDetailRowContent(detailIndex) {
|
|
12762
|
+
await this.assertTableLayout('getDetailRowContent');
|
|
11927
12763
|
const detailRows = await this.locatorForAll('.tn-table__detail-row')();
|
|
11928
12764
|
if (detailIndex >= detailRows.length) {
|
|
11929
12765
|
throw new Error(`Detail row index ${detailIndex} out of bounds (${detailRows.length} detail rows)`);
|
|
@@ -11933,21 +12769,355 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11933
12769
|
/**
|
|
11934
12770
|
* Gets the count of currently expanded detail rows.
|
|
11935
12771
|
*
|
|
11936
|
-
*
|
|
12772
|
+
* Layout-aware: counts open card detail panels in the card layout, which renders the same
|
|
12773
|
+
* expansion state without a detail `<tr>`.
|
|
12774
|
+
*
|
|
12775
|
+
* @returns Promise resolving to the number of visible detail rows, or open card detail
|
|
12776
|
+
* panels in the card layout.
|
|
11937
12777
|
*/
|
|
11938
12778
|
async getExpandedRowCount() {
|
|
12779
|
+
if (await this.isCards()) {
|
|
12780
|
+
const panels = await this.locatorForAll('.tn-table__card-detail')();
|
|
12781
|
+
return panels.length;
|
|
12782
|
+
}
|
|
11939
12783
|
const detailRows = await this.locatorForAll('.tn-table__detail-row')();
|
|
11940
12784
|
return detailRows.length;
|
|
11941
12785
|
}
|
|
12786
|
+
// --- Card layout (responsive) ---
|
|
12787
|
+
/**
|
|
12788
|
+
* Reports the currently rendered layout: `cards` when the container is narrow
|
|
12789
|
+
* enough that `mobileLayout="cards"` has taken effect, otherwise `table`.
|
|
12790
|
+
*
|
|
12791
|
+
* @returns Promise resolving to 'cards' or 'table'.
|
|
12792
|
+
*/
|
|
12793
|
+
async getLayoutMode() {
|
|
12794
|
+
const cards = await this.locatorForOptional('.tn-table__cards')();
|
|
12795
|
+
return cards ? 'cards' : 'table';
|
|
12796
|
+
}
|
|
12797
|
+
/**
|
|
12798
|
+
* Gets the number of rendered cards (card layout only).
|
|
12799
|
+
*
|
|
12800
|
+
* @returns Promise resolving to the card count.
|
|
12801
|
+
*/
|
|
12802
|
+
async getCardCount() {
|
|
12803
|
+
await this.assertCardLayout('getCardCount');
|
|
12804
|
+
const cards = await this.locatorForAll('.tn-table__card')();
|
|
12805
|
+
return cards.length;
|
|
12806
|
+
}
|
|
12807
|
+
/**
|
|
12808
|
+
* Gets the title text of a card.
|
|
12809
|
+
*
|
|
12810
|
+
* @param cardIndex Zero-based index of the card.
|
|
12811
|
+
* @returns Promise resolving to the card title text.
|
|
12812
|
+
*/
|
|
12813
|
+
async getCardTitle(cardIndex) {
|
|
12814
|
+
await this.assertCardLayout('getCardTitle');
|
|
12815
|
+
const title = await this.locatorFor(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-title`)();
|
|
12816
|
+
return (await title.text()).trim();
|
|
12817
|
+
}
|
|
12818
|
+
/**
|
|
12819
|
+
* Gets the value text of a field within a card, by column name. The field
|
|
12820
|
+
* may be a primary field or one tucked under "More fields".
|
|
12821
|
+
*
|
|
12822
|
+
* @param cardIndex Zero-based index of the card.
|
|
12823
|
+
* @param columnName The column's data-column attribute value.
|
|
12824
|
+
* @returns Promise resolving to the field's value text.
|
|
12825
|
+
*/
|
|
12826
|
+
async getCardFieldValue(cardIndex, columnName) {
|
|
12827
|
+
await this.assertCardLayout('getCardFieldValue');
|
|
12828
|
+
const value = await this.locatorFor(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-field[data-column="${columnName}"] .tn-table__card-field-value`)();
|
|
12829
|
+
return (await value.text()).trim();
|
|
12830
|
+
}
|
|
12831
|
+
/**
|
|
12832
|
+
* Gets the column names of the fields shown directly on a card (i.e. not
|
|
12833
|
+
* those hidden behind the "More fields" disclosure).
|
|
12834
|
+
*
|
|
12835
|
+
* @param cardIndex Zero-based index of the card.
|
|
12836
|
+
* @returns Promise resolving to an array of column names.
|
|
12837
|
+
*/
|
|
12838
|
+
async getCardPrimaryFieldColumns(cardIndex) {
|
|
12839
|
+
await this.assertCardLayout('getCardPrimaryFieldColumns');
|
|
12840
|
+
const fields = await this.locatorForAll(`.tn-table__card[data-row-index="${cardIndex}"] > .tn-table__card-fields > .tn-table__card-field[data-column]`)();
|
|
12841
|
+
const columns = [];
|
|
12842
|
+
for (const field of fields) {
|
|
12843
|
+
const col = await field.getAttribute('data-column');
|
|
12844
|
+
if (col !== null) {
|
|
12845
|
+
columns.push(col);
|
|
12846
|
+
}
|
|
12847
|
+
}
|
|
12848
|
+
return columns;
|
|
12849
|
+
}
|
|
12850
|
+
/**
|
|
12851
|
+
* Expands the "More fields" disclosure on a card to reveal lower-priority
|
|
12852
|
+
* fields. No-op if the card has no secondary fields.
|
|
12853
|
+
*
|
|
12854
|
+
* @param cardIndex Zero-based index of the card.
|
|
12855
|
+
*/
|
|
12856
|
+
async expandCardMoreFields(cardIndex) {
|
|
12857
|
+
await this.assertCardLayout('expandCardMoreFields');
|
|
12858
|
+
const disclosure = await this.locatorForOptional(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-more`)();
|
|
12859
|
+
if (!disclosure) {
|
|
12860
|
+
return;
|
|
12861
|
+
}
|
|
12862
|
+
// Idempotent, because the name and the doc promise an expand while `<summary>` toggles.
|
|
12863
|
+
// A second call used to collapse it — and under a real WebDriver, where `text()` returns
|
|
12864
|
+
// only visible text, `getCardFieldValue()` on a folded field would then come back empty.
|
|
12865
|
+
if (await disclosure.getProperty('open')) {
|
|
12866
|
+
return;
|
|
12867
|
+
}
|
|
12868
|
+
const summary = await this.locatorFor(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-more-summary`)();
|
|
12869
|
+
await summary.click();
|
|
12870
|
+
}
|
|
12871
|
+
/**
|
|
12872
|
+
* Toggles a card's detail section (card layout equivalent of row expansion).
|
|
12873
|
+
*
|
|
12874
|
+
* @param cardIndex Zero-based index of the card.
|
|
12875
|
+
*/
|
|
12876
|
+
async toggleCardDetail(cardIndex) {
|
|
12877
|
+
await this.assertCardLayout('toggleCardDetail');
|
|
12878
|
+
const toggle = await this.locatorFor(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-detail-toggle`)();
|
|
12879
|
+
await toggle.click();
|
|
12880
|
+
}
|
|
12881
|
+
/**
|
|
12882
|
+
* Gets the currently selected sort column in the card-layout sort menu, or
|
|
12883
|
+
* `''` when unsorted. Returns null if the sort menu isn't rendered.
|
|
12884
|
+
*
|
|
12885
|
+
* @returns Promise resolving to the selected column name, '', or null.
|
|
12886
|
+
*/
|
|
12887
|
+
async getCardSortColumn() {
|
|
12888
|
+
const select = await this.locatorForOptional('.tn-table__cards-sort-select')();
|
|
12889
|
+
if (!select) {
|
|
12890
|
+
return null;
|
|
12891
|
+
}
|
|
12892
|
+
return select.getProperty('value');
|
|
12893
|
+
}
|
|
12894
|
+
/**
|
|
12895
|
+
* Gets the active sort direction in the card layout, read from the direction toggle's
|
|
12896
|
+
* `data-sort-direction`. Deliberately not derived from the button's `aria-label`: matching
|
|
12897
|
+
* display text would silently report one direction forever if the wording or locale changed.
|
|
12898
|
+
*
|
|
12899
|
+
* @returns Promise resolving to 'asc' or 'desc'; `''` both when sorted by nothing and when the
|
|
12900
|
+
* active column isn't sortable (the toggle isn't rendered in either case, and this reads the
|
|
12901
|
+
* rendered control — so a non-sortable active column reports `''` even though the component's
|
|
12902
|
+
* `sortDirection()` holds a value); or null when the card sort menu isn't rendered at all
|
|
12903
|
+
* (table layout, or no sortable columns) — the same null-vs-empty distinction
|
|
12904
|
+
* {@link getCardSortColumn} makes.
|
|
12905
|
+
*/
|
|
12906
|
+
async getCardSortDirection() {
|
|
12907
|
+
const menu = await this.locatorForOptional('.tn-table__cards-sort')();
|
|
12908
|
+
if (!menu) {
|
|
12909
|
+
return null;
|
|
12910
|
+
}
|
|
12911
|
+
const button = await this.locatorForOptional('.tn-table__cards-sort-dir')();
|
|
12912
|
+
if (!button) {
|
|
12913
|
+
return '';
|
|
12914
|
+
}
|
|
12915
|
+
const direction = await button.getAttribute('data-sort-direction');
|
|
12916
|
+
// Report the empty direction rather than rounding it up to 'asc'. A column set with
|
|
12917
|
+
// no direction is not sorted, and claiming otherwise made this getter contradict its
|
|
12918
|
+
// own contract — and hid the fact that the button rendered a direction for it.
|
|
12919
|
+
if (direction === 'asc' || direction === 'desc') {
|
|
12920
|
+
return direction;
|
|
12921
|
+
}
|
|
12922
|
+
return '';
|
|
12923
|
+
}
|
|
12924
|
+
/**
|
|
12925
|
+
* Clicks the sort-direction toggle in the card-layout sort menu. No-op when the toggle
|
|
12926
|
+
* isn't rendered, which is three cases: the table layout is showing (the whole card sort
|
|
12927
|
+
* menu is absent — so a call made before the container narrowed does nothing and reports
|
|
12928
|
+
* success), no active sort column, or an active column that isn't `sortable()` — card mode
|
|
12929
|
+
* deliberately declines to reorder by a column whose table header ignores clicks.
|
|
12930
|
+
*/
|
|
12931
|
+
async toggleCardSortDirection() {
|
|
12932
|
+
const button = await this.locatorForOptional('.tn-table__cards-sort-dir')();
|
|
12933
|
+
if (button) {
|
|
12934
|
+
await button.click();
|
|
12935
|
+
}
|
|
12936
|
+
}
|
|
12937
|
+
// --- Card activation ---
|
|
12938
|
+
//
|
|
12939
|
+
// Mirrors of the four clickable-row methods. Without them the row methods' `@throws` sent a
|
|
12940
|
+
// card-mode spec to a card API that could not activate anything, and the only way through
|
|
12941
|
+
// was raw DOM — which is what the harness exists to spare a consumer.
|
|
12942
|
+
/**
|
|
12943
|
+
* Clicks a card body (card mirror of {@link clickRow}, for tables with `clickable`).
|
|
12944
|
+
*
|
|
12945
|
+
* Targets the card's title rather than the card box, because the card box's centre can sit
|
|
12946
|
+
* on a projected row action or the selection checkbox, and the activation guard ignores
|
|
12947
|
+
* clicks that originate on a control — the click would land and nothing would happen.
|
|
12948
|
+
*
|
|
12949
|
+
* @param cardIndex Zero-based index of the card.
|
|
12950
|
+
* @throws Error in the table layout; use {@link clickRow} there.
|
|
12951
|
+
*/
|
|
12952
|
+
async clickCard(cardIndex) {
|
|
12953
|
+
await this.assertCardLayout('clickCard');
|
|
12954
|
+
await this.assertIndexExists(cardIndex);
|
|
12955
|
+
const target = await this.cardActivationTarget(cardIndex);
|
|
12956
|
+
await target.click();
|
|
12957
|
+
}
|
|
12958
|
+
/**
|
|
12959
|
+
* Double-clicks a card, triggering `rowDoubleClick` (card mirror of
|
|
12960
|
+
* {@link doubleClickRow}). As there, only the `dblclick` event is dispatched — a real
|
|
12961
|
+
* double-click also fires two single clicks first.
|
|
12962
|
+
*
|
|
12963
|
+
* Dispatched on the card rather than on the title {@link clickCard} aims at, because a
|
|
12964
|
+
* synthesised event does not bubble: sent to the title it would never reach the card's
|
|
12965
|
+
* handler. There is no cost to it here — the target is set explicitly, so unlike a real
|
|
12966
|
+
* pointer click it cannot land on a projected control.
|
|
12967
|
+
*
|
|
12968
|
+
* @param cardIndex Zero-based index of the card.
|
|
12969
|
+
* @throws Error in the table layout; use {@link doubleClickRow} there.
|
|
12970
|
+
*/
|
|
12971
|
+
async doubleClickCard(cardIndex) {
|
|
12972
|
+
await this.assertCardLayout('doubleClickCard');
|
|
12973
|
+
await this.assertIndexExists(cardIndex);
|
|
12974
|
+
const card = await this.cardAt(cardIndex);
|
|
12975
|
+
await card.dispatchEvent('dblclick');
|
|
12976
|
+
}
|
|
12977
|
+
/**
|
|
12978
|
+
* Sends a keyboard event to a card (Enter/Space activate clickable cards). Card mirror of
|
|
12979
|
+
* {@link pressKeyOnRow}; the key goes to the card itself, which is the element that carries
|
|
12980
|
+
* `tabindex` and the keydown handler.
|
|
12981
|
+
*
|
|
12982
|
+
* @param cardIndex Zero-based index of the card.
|
|
12983
|
+
* @param key Which key to press — Enter or Space.
|
|
12984
|
+
* @throws Error in the table layout; use {@link pressKeyOnRow} there.
|
|
12985
|
+
*/
|
|
12986
|
+
async pressKeyOnCard(cardIndex, key) {
|
|
12987
|
+
await this.assertCardLayout('pressKeyOnCard');
|
|
12988
|
+
await this.assertIndexExists(cardIndex);
|
|
12989
|
+
const card = await this.cardAt(cardIndex);
|
|
12990
|
+
await card.focus();
|
|
12991
|
+
if (key === 'enter') {
|
|
12992
|
+
await card.sendKeys(TestKey.ENTER);
|
|
12993
|
+
}
|
|
12994
|
+
else {
|
|
12995
|
+
await card.sendKeys(' ');
|
|
12996
|
+
}
|
|
12997
|
+
}
|
|
12998
|
+
/**
|
|
12999
|
+
* Checks if a card is keyboard-focusable (tabindex=0). Card mirror of
|
|
13000
|
+
* {@link isRowFocusable}.
|
|
13001
|
+
*
|
|
13002
|
+
* @param cardIndex Zero-based index of the card.
|
|
13003
|
+
* @throws Error in the table layout; use {@link isRowFocusable} there.
|
|
13004
|
+
*/
|
|
13005
|
+
async isCardFocusable(cardIndex) {
|
|
13006
|
+
await this.assertCardLayout('isCardFocusable');
|
|
13007
|
+
await this.assertIndexExists(cardIndex);
|
|
13008
|
+
const card = await this.cardAt(cardIndex);
|
|
13009
|
+
return (await card.getAttribute('tabindex')) === '0';
|
|
13010
|
+
}
|
|
11942
13011
|
// --- Internal helpers ---
|
|
11943
|
-
|
|
13012
|
+
/** The card box itself — the element carrying `tabindex`, the handlers and the ARIA state. */
|
|
13013
|
+
async cardAt(cardIndex) {
|
|
13014
|
+
return this.locatorFor(`.tn-table__card[data-row-index="${cardIndex}"]`)();
|
|
13015
|
+
}
|
|
13016
|
+
/** The surface pointer activation is aimed at — see {@link clickCard} for why not the card. */
|
|
13017
|
+
async cardActivationTarget(cardIndex) {
|
|
13018
|
+
const title = await this.locatorForOptional(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-title`)();
|
|
13019
|
+
return title ?? (await this.cardAt(cardIndex));
|
|
13020
|
+
}
|
|
13021
|
+
/**
|
|
13022
|
+
* Bounds check for the layout-aware methods. Counts whatever the rendered layout shows, via
|
|
13023
|
+
* `getRowCount()` — unlike {@link assertRowExists}, which deliberately counts rows only,
|
|
13024
|
+
* because its callers refuse card mode outright.
|
|
13025
|
+
*/
|
|
13026
|
+
async assertIndexExists(index) {
|
|
11944
13027
|
const count = await this.getRowCount();
|
|
11945
|
-
if (
|
|
11946
|
-
|
|
13028
|
+
if (index >= count) {
|
|
13029
|
+
const noun = (await this.isCards()) ? 'cards' : 'rows';
|
|
13030
|
+
throw new Error(`Row index ${index} out of bounds (${count} ${noun})`);
|
|
13031
|
+
}
|
|
13032
|
+
}
|
|
13033
|
+
async assertRowExists(rowIndex) {
|
|
13034
|
+
// Counts `.tn-table__row` directly rather than calling `getRowCount()`, which is
|
|
13035
|
+
// layout-aware: in card mode that counts cards, so this guard passed for every valid
|
|
13036
|
+
// index and stopped catching the very mismatch it exists for. Row-only methods pair it
|
|
13037
|
+
// with `assertTableLayout()`, which reports the layout mismatch first.
|
|
13038
|
+
const rows = await this.locatorForAll('.tn-table__row')();
|
|
13039
|
+
if (rowIndex >= rows.length) {
|
|
13040
|
+
throw new Error(`Row index ${rowIndex} out of bounds (${rows.length} rows)`);
|
|
11947
13041
|
}
|
|
11948
13042
|
}
|
|
11949
13043
|
}
|
|
11950
13044
|
|
|
13045
|
+
/**
|
|
13046
|
+
* Stand-in for `ResizeObserver`, which jsdom does not implement — so a `tn-table` under
|
|
13047
|
+
* test can never measure its own container and never leaves the table layout on its own.
|
|
13048
|
+
* Installing this lets a spec push a width through the component's real callback path
|
|
13049
|
+
* rather than reaching for its private state.
|
|
13050
|
+
*
|
|
13051
|
+
* Module-private on purpose. `MockResizeObserver` is a name a consumer's own test helpers
|
|
13052
|
+
* plausibly already define — this repo's own `tree-virtual-scroll-view.component.spec.ts`
|
|
13053
|
+
* declares one — and a wildcard import of the library would collide with it. Everything a
|
|
13054
|
+
* caller needs is reachable through {@link TnTableTesting}.
|
|
13055
|
+
*/
|
|
13056
|
+
class MockResizeObserver {
|
|
13057
|
+
cb;
|
|
13058
|
+
static instances = [];
|
|
13059
|
+
constructor(cb) {
|
|
13060
|
+
this.cb = cb;
|
|
13061
|
+
MockResizeObserver.instances.push(this);
|
|
13062
|
+
}
|
|
13063
|
+
observe() { }
|
|
13064
|
+
unobserve() { }
|
|
13065
|
+
disconnect() { }
|
|
13066
|
+
/** Fires the observed callback with `width` as the element's content-box width. */
|
|
13067
|
+
emitWidth(width) {
|
|
13068
|
+
this.cb([{ contentRect: { width } }], this);
|
|
13069
|
+
}
|
|
13070
|
+
}
|
|
13071
|
+
/**
|
|
13072
|
+
* Testing utilities for `tn-table`'s container-driven responsive layout.
|
|
13073
|
+
*
|
|
13074
|
+
* Part of the public API because `TnTableHarness`'s layout-mismatch messages tell the caller
|
|
13075
|
+
* to widen or narrow the container past `cardBreakpoint`, and jsdom implements no
|
|
13076
|
+
* `ResizeObserver` — so without a stand-in a consumer's own spec has no supported way to
|
|
13077
|
+
* follow that advice.
|
|
13078
|
+
*
|
|
13079
|
+
* Namespaced rather than exported as loose functions, matching `TnIconTesting` and
|
|
13080
|
+
* `TnToastTesting`: these names live at the top level of the package, and `installObserver`
|
|
13081
|
+
* or `emitContainerWidth` on their own give no hint that they concern tables.
|
|
13082
|
+
*
|
|
13083
|
+
* @example
|
|
13084
|
+
* ```typescript
|
|
13085
|
+
* let restoreResizeObserver: () => void;
|
|
13086
|
+
*
|
|
13087
|
+
* beforeEach(() => { restoreResizeObserver = TnTableTesting.installResizeObserver(); });
|
|
13088
|
+
* afterEach(() => { restoreResizeObserver(); });
|
|
13089
|
+
*
|
|
13090
|
+
* // ...then, inside a test:
|
|
13091
|
+
* TnTableTesting.emitContainerWidth(320);
|
|
13092
|
+
* fixture.detectChanges();
|
|
13093
|
+
* ```
|
|
13094
|
+
*/
|
|
13095
|
+
const TnTableTesting = {
|
|
13096
|
+
/**
|
|
13097
|
+
* Swaps `globalThis.ResizeObserver` for a stand-in and clears any instances left by a
|
|
13098
|
+
* previous test.
|
|
13099
|
+
*
|
|
13100
|
+
* @returns A function that restores the original global — call it in `afterEach`.
|
|
13101
|
+
*/
|
|
13102
|
+
installResizeObserver() {
|
|
13103
|
+
const original = globalThis.ResizeObserver;
|
|
13104
|
+
MockResizeObserver.instances = [];
|
|
13105
|
+
globalThis.ResizeObserver = MockResizeObserver;
|
|
13106
|
+
return () => {
|
|
13107
|
+
globalThis.ResizeObserver = original;
|
|
13108
|
+
};
|
|
13109
|
+
},
|
|
13110
|
+
/**
|
|
13111
|
+
* Pushes `width` through every observer the component under test registered, as its
|
|
13112
|
+
* content-box width — the same quantity `measureContainer` reads on the initial pass.
|
|
13113
|
+
*
|
|
13114
|
+
* @param width Content-box width in px. A 0 reads as unmeasurable, not narrow.
|
|
13115
|
+
*/
|
|
13116
|
+
emitContainerWidth(width) {
|
|
13117
|
+
MockResizeObserver.instances.forEach((observer) => observer.emitWidth(width));
|
|
13118
|
+
},
|
|
13119
|
+
};
|
|
13120
|
+
|
|
11951
13121
|
/** English defaults used when no `TN_TABLE_PAGER_LABELS` provider is registered. */
|
|
11952
13122
|
const TN_TABLE_PAGER_DEFAULT_LABELS = {
|
|
11953
13123
|
itemsPerPage: 'Items per page',
|
|
@@ -19351,7 +20521,7 @@ class TnFilePickerPopupComponent {
|
|
|
19351
20521
|
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${timePart}`;
|
|
19352
20522
|
}
|
|
19353
20523
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
19354
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFilePickerPopupComponent, isStandalone: true, selector: "tn-file-picker-popup", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, createActions: { classPropertyName: "createActions", publicName: "createActions", isSignal: true, isRequired: false, transformFunction: null }, currentPath: { classPropertyName: "currentPath", publicName: "currentPath", isSignal: true, isRequired: false, transformFunction: null }, rootPath: { classPropertyName: "rootPath", publicName: "rootPath", isSignal: true, isRequired: false, transformFunction: null }, fileItems: { classPropertyName: "fileItems", publicName: "fileItems", isSignal: true, isRequired: false, transformFunction: null }, selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null }, testIdBase: { classPropertyName: "testIdBase", publicName: "testIdBase", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemDoubleClick: "itemDoubleClick", pathNavigate: "pathNavigate", createAction: "createAction", created: "created", clearSelection: "clearSelection", close: "close", submit: "submit", cancel: "cancel" }, host: { classAttribute: "tn-file-picker-popup" }, ngImport: i0, template: "<!-- Header with breadcrumb navigation -->\n<div class=\"tn-file-picker-header\">\n <nav class=\"tn-file-picker-breadcrumb\" aria-label=\"File path\">\n @for (segment of currentPath() | tnTruncatePath : rootPath(); track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n tnTestIdType=\"button\"\n [tnTestId]=\"breadcrumbTestId(segment.name)\"\n [class.current]=\"last\"\n [class.nameless]=\"!segment.name\"\n [disabled]=\"last\"\n [attr.aria-label]=\"segment.name === '\u2026' || !segment.name ? 'Navigate to ' + segment.path : null\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\n</div>\n\n<!-- Loading indicator -->\n@if (loading()) {\n <div class=\"tn-file-picker-loading\">\n <tn-icon name=\"loading\" library=\"mdi\" />\n <span>Loading...</span>\n </div>\n}\n\n<!-- File table -->\n@if (!loading()) {\n <div class=\"tn-file-picker-content\">\n\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\"\n [clickable]=\"true\"\n [activeWhen]=\"isRowSelected\"\n (rowClick)=\"onItemClick($event)\"\n (rowDoubleClick)=\"onItemDoubleClick($event)\">\n\n <!-- Selection column: explicit width so the auto table layout doesn't\n hand the near-empty column a share of the spare width -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\" width=\"20px\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n @if (!item.inlineCreate) {\n <tn-checkbox\n [hideLabel]=\"true\"\n [label]=\"'Select ' + item.name\"\n [testId]=\"itemTestId(item)\"\n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\" />\n }\n </ng-template>\n </ng-container>\n }\n\n <!-- Name column -->\n <ng-container tnColumnDef=\"name\">\n <ng-template tnHeaderCellDef>Name</ng-template>\n <ng-template let-item tnCellDef>\n\n <!-- Inline creation row for create actions with a `create` callback -->\n @if (item.inlineCreate) {\n @if (inlineCreation(); as creation) {\n <div class=\"inline-create-cell\">\n <div class=\"inline-create-row\" [class.has-error]=\"!!creation.error\">\n <tn-icon class=\"file-icon\" [name]=\"creation.action.icon || 'mdi-folder'\" />\n <input\n type=\"text\"\n class=\"inline-create-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n tnTestIdType=\"input\"\n [tnTestId]=\"inlineCreateTestId()\"\n [attr.aria-label]=\"creation.action.label + ' name'\"\n [attr.aria-invalid]=\"creation.error ? true : null\"\n [attr.aria-describedby]=\"creation.error ? inlineErrorId : null\"\n [class.error]=\"!!creation.error\"\n [disabled]=\"creation.loading\"\n [value]=\"creation.name\"\n (input)=\"onInlineCreationInput($event)\"\n (keydown)=\"onInlineCreationKeyDown($event)\"\n (blur)=\"onInlineCreationBlur()\">\n @if (creation.loading) {\n <tn-icon name=\"mdi-loading\" class=\"creation-loading-icon\" aria-hidden=\"true\" />\n }\n </div>\n @if (creation.error) {\n <div class=\"inline-create-error\" role=\"alert\" [id]=\"inlineErrorId\">\n <tn-icon name=\"mdi-alert-circle\" class=\"error-icon\" aria-hidden=\"true\" />\n <span class=\"error-text\">{{ creation.error }}</span>\n </div>\n }\n </div>\n }\n } @else {\n <div\n class=\"file-name-cell\"\n tnTestIdType=\"option\"\n [tnTestId]=\"itemTestId(item)\"\n [class.selected]=\"isSelected(item)\"\n [class.disabled]=\"!!item.dimmed\"\n [class.zfs-object]=\"isZfsObject(item)\">\n <tn-icon\n [name]=\"getItemIcon(item)\"\n [class]=\"'file-icon file-icon-' + item.type\" />\n <span class=\"file-name\">{{ item.name }}</span>\n\n <!-- ZFS badge -->\n @if (isZfsObject(item)) {\n <span\n [class]=\"'zfs-badge zfs-badge-' + item.type\">\n {{ getZfsBadge(item) }}\n </span>\n }\n\n <!-- Permission indicator -->\n @if (item.permissions === 'none') {\n <tn-icon\n name=\"lock\"\n library=\"mdi\"\n class=\"permission-icon\" />\n }\n\n <!-- Navigation affordance: single click opens the directory -->\n @if (isNavigatable(item)) {\n <button\n type=\"button\"\n class=\"navigate-button\"\n tnTestIdType=\"button\"\n [tnTestId]=\"navigateTestId(item)\"\n [attr.aria-label]=\"'Open ' + item.name\"\n [attr.title]=\"'Open ' + item.name\"\n (click)=\"onNavigateClick($event, item)\"\n (dblclick)=\"$event.stopPropagation()\">\n <tn-icon name=\"mdi-chevron-right\" size=\"sm\" />\n </button>\n }\n </div>\n }\n </ng-template>\n </ng-container>\n\n <!-- Size column -->\n <ng-container tnColumnDef=\"size\">\n <ng-template tnHeaderCellDef>Size</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.size !== undefined) {\n <span>{{ item.size | tnFileSize }}</span>\n }\n @if (item.size === undefined && item.type === 'folder') {\n <span class=\"folder-indicator\">--</span>\n }\n </ng-template>\n </ng-container>\n\n <!-- Modified column -->\n <ng-container tnColumnDef=\"modified\">\n <ng-template tnHeaderCellDef>Modified</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.modified) {\n <span>{{ formatDate(item.modified) }}</span>\n }\n </ng-template>\n </ng-container>\n\n\n </tn-table>\n\n <!-- Empty state -->\n @if (filteredFileItems().length === 0) {\n <div class=\"empty-state\">\n <tn-icon name=\"folder-open\" library=\"mdi\" customSize=\"48px\" />\n <p>No items found</p>\n </div>\n }\n </div>\n}\n\n<!-- Footer -->\n@if (!loading()) {\n <div class=\"tn-file-picker-footer\">\n <div class=\"footer-selection\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n <tn-button\n variant=\"outline\"\n label=\"Clear Selection\"\n [testId]=\"clearSelectionTestId()\"\n (onClick)=\"onClearSelection()\" />\n } @else if (allowCurrentDirectorySelection()) {\n <span class=\"selection-count current-directory-selection\">\n Current directory selected\n </span>\n } @else {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n </div>\n <div class=\"footer-actions\">\n @for (action of createActions(); track action.id) {\n <tn-button\n variant=\"outline\"\n [label]=\"action.label\"\n [testId]=\"createActionTestId(action)\"\n [disabled]=\"!!inlineCreation()\"\n (onClick)=\"onCreateAction(action)\" />\n }\n <!-- Disabled while the inline creation row is open: its blur auto-submit\n would race the selection submit -->\n <tn-button\n label=\"Select\"\n [testId]=\"selectTestId()\"\n [disabled]=\"(selectedItems().length === 0 && !allowCurrentDirectorySelection()) || !!inlineCreation()\"\n (onClick)=\"onSubmit()\" />\n </div>\n </div>\n}", styles: [":host{display:block;background:var(--tn-bg1, white);color:var(--tn-fg1, #333);padding:0;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014;border-radius:8px;border:1px solid var(--tn-lines, #e0e0e0);width:520px;max-width:calc(100vw - 32px);min-height:min(500px,100vh - 16px);max-height:min(600px,100vh - 16px);font-family:var(--tn-font-family-body);display:flex;flex-direction:column;overflow:hidden}.tn-file-picker-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tn-content-padding, 24px);padding-bottom:16px;border-bottom:1px solid var(--tn-lines)}.tn-file-picker-breadcrumb{display:flex;align-items:center;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 2px;border-radius:4px;font-size:1rem;white-space:nowrap;transition:background-color .15s ease-in-out}.tn-file-picker-breadcrumb .breadcrumb-segment:hover:not(:disabled){background:var(--tn-bg2)}.tn-file-picker-breadcrumb .breadcrumb-segment:disabled,.tn-file-picker-breadcrumb .breadcrumb-segment.current{color:var(--tn-fg1);cursor:default;font-weight:500}.tn-file-picker-breadcrumb .breadcrumb-segment:not(:last-child):after{content:\"/\";margin-left:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment:first-child:before{content:\"/\";margin-right:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment.nameless:not(:last-child):after{content:none}.tn-file-picker-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px;color:var(--tn-fg2)}.tn-file-picker-loading tn-icon{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tn-file-picker-content{flex:1;min-height:0;overflow-y:auto}.inline-create-cell{display:flex;flex-direction:column;gap:4px;flex:1;min-width:0}.inline-create-row{display:flex;align-items:center;gap:8px}.inline-create-row .inline-create-input{flex:1;padding:4px 8px;border:2px solid var(--tn-primary);border-radius:3px;background:var(--tn-bg1);color:var(--tn-fg1);font-size:inherit;font-family:inherit;outline:none;min-width:0}.inline-create-row .inline-create-input.error{border-color:var(--tn-error, #d32f2f)}.inline-create-row .inline-create-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2)}.inline-create-row .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary);flex-shrink:0}.inline-create-error{display:flex;align-items:center;gap:6px;margin-top:4px;padding:4px 8px;background:color-mix(in srgb,var(--tn-error, #d32f2f) 10%,transparent);border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.inline-create-error .error-icon{flex-shrink:0}.inline-create-error .error-text{flex:1}.file-list-viewport{width:100%;height:100%}.file-list-viewport .cdk-virtual-scroll-content-wrapper{width:100%}tn-table{width:100%}tn-table th,tn-table .tn-table__header-cell{font-weight:600;color:var(--tn-fg1);padding:12px 16px;border-bottom:2px solid var(--tn-lines)}tn-table td,tn-table .tn-table__cell{padding:8px 16px;border-bottom:1px solid var(--tn-lines)}.file-name-cell{display:flex;align-items:center;gap:8px;cursor:pointer}.file-name-cell.disabled{opacity:.5;color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-name{color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-icon{opacity:.6}.file-name-cell.disabled:has(.file-icon-folder),.file-name-cell.disabled:has(.file-icon-dataset),.file-name-cell.disabled:has(.file-icon-mountpoint){cursor:pointer}.file-name-cell.disabled:not(:has(.file-icon-folder)):not(:has(.file-icon-dataset)):not(:has(.file-icon-mountpoint)){cursor:not-allowed}.file-icon{display:flex;align-items:center;justify-content:center;font-size:var(--tn-icon-md, 20px);flex-shrink:0;line-height:1}.file-icon.file-icon-folder{color:var(--tn-primary)}.file-icon.file-icon-dataset{color:var(--tn-blue, #007db3)}.file-icon.file-icon-zvol{color:var(--tn-green, #71BF44)}.file-icon.file-icon-mountpoint{color:var(--tn-orange, #E68D37)}.file-name{flex:1;font-weight:500;line-height:1.4}.zfs-badge{display:inline-flex;align-items:center;background:var(--tn-alt-bg2);color:var(--tn-alt-fg2);font-size:.625rem;font-weight:600;padding:2px 6px;border-radius:12px;text-transform:uppercase;letter-spacing:.5px;line-height:1}.zfs-badge.zfs-badge-dataset{background:var(--tn-blue);color:#fff}.zfs-badge.zfs-badge-zvol{background:var(--tn-green);color:#fff}.zfs-badge.zfs-badge-mountpoint{background:var(--tn-orange);color:#fff}.permission-icon{display:flex;align-items:center;justify-content:center;color:var(--tn-red);font-size:var(--tn-icon-sm, 16px);line-height:1}.navigate-button{display:flex;align-items:center;justify-content:center;margin-left:auto;padding:2px;background:none;border:none;border-radius:4px;color:var(--tn-fg2);cursor:pointer}.navigate-button:hover,.navigate-button:focus-visible{background:var(--tn-bg3);color:var(--tn-primary)}.file-type{font-size:1rem;padding:2px 8px;border-radius:12px}.file-type.type-folder{background:var(--tn-alt-bg1);color:var(--tn-alt-fg2)}.file-type.type-file{background:var(--tn-bg2);color:var(--tn-fg2)}.file-type.type-dataset{background:#007db31a;color:var(--tn-blue)}.file-type.type-zvol{background:#71bf441a;color:var(--tn-green)}.file-type.type-mountpoint{background:#e68d371a;color:var(--tn-orange)}.folder-indicator{color:var(--tn-alt-fg1);font-style:italic}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--tn-alt-fg1);text-align:center}.empty-state tn-icon{margin-bottom:16px;opacity:.5}.empty-state p{margin:0;font-size:1rem}.tn-file-picker-footer{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px var(--tn-content-padding, 24px);border-top:1px solid var(--tn-lines);background:var(--tn-bg2);border-bottom-left-radius:8px;border-bottom-right-radius:8px}.tn-file-picker-footer .footer-selection{display:flex;align-items:center;gap:12px}.selection-count{font-size:1rem;color:var(--tn-fg2);font-weight:500}.footer-actions{display:flex;gap:8px}@media(prefers-reduced-motion:reduce){.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){:host{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnTableComponent, selector: "tn-table", inputs: ["dataSource", "displayedColumns", "trackBy", "emptyMessage", "emptyDescription", "emptyIcon", "selectable", "expandable", "bordered", "isRowExpandable", "activeRow", "activeWhen", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable", "expandOnRowClick", "singleExpand", "fixedLayout", "minColumnWidth", "minWidth", "sortColumn", "sortDirection"], outputs: ["sortChange", "selectionChange", "rowClick", "rowDoubleClick", "sortColumnChange", "sortDirectionChange"] }, { kind: "directive", type: TnTableColumnDirective, selector: "[tnColumnDef]", inputs: ["tnColumnDef", "sortable", "width"], exportAs: ["tnColumnDef"] }, { kind: "directive", type: TnHeaderCellDefDirective, selector: "[tnHeaderCellDef]" }, { kind: "directive", type: TnCellDefDirective, selector: "[tnCellDef]" }, { kind: "ngmodule", type: ScrollingModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
|
|
20524
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFilePickerPopupComponent, isStandalone: true, selector: "tn-file-picker-popup", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, createActions: { classPropertyName: "createActions", publicName: "createActions", isSignal: true, isRequired: false, transformFunction: null }, currentPath: { classPropertyName: "currentPath", publicName: "currentPath", isSignal: true, isRequired: false, transformFunction: null }, rootPath: { classPropertyName: "rootPath", publicName: "rootPath", isSignal: true, isRequired: false, transformFunction: null }, fileItems: { classPropertyName: "fileItems", publicName: "fileItems", isSignal: true, isRequired: false, transformFunction: null }, selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null }, testIdBase: { classPropertyName: "testIdBase", publicName: "testIdBase", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemDoubleClick: "itemDoubleClick", pathNavigate: "pathNavigate", createAction: "createAction", created: "created", clearSelection: "clearSelection", close: "close", submit: "submit", cancel: "cancel" }, host: { classAttribute: "tn-file-picker-popup" }, ngImport: i0, template: "<!-- Header with breadcrumb navigation -->\n<div class=\"tn-file-picker-header\">\n <nav class=\"tn-file-picker-breadcrumb\" aria-label=\"File path\">\n @for (segment of currentPath() | tnTruncatePath : rootPath(); track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n tnTestIdType=\"button\"\n [tnTestId]=\"breadcrumbTestId(segment.name)\"\n [class.current]=\"last\"\n [class.nameless]=\"!segment.name\"\n [disabled]=\"last\"\n [attr.aria-label]=\"segment.name === '\u2026' || !segment.name ? 'Navigate to ' + segment.path : null\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\n</div>\n\n<!-- Loading indicator -->\n@if (loading()) {\n <div class=\"tn-file-picker-loading\">\n <tn-icon name=\"loading\" library=\"mdi\" />\n <span>Loading...</span>\n </div>\n}\n\n<!-- File table -->\n@if (!loading()) {\n <div class=\"tn-file-picker-content\">\n\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\"\n [clickable]=\"true\"\n [activeWhen]=\"isRowSelected\"\n (rowClick)=\"onItemClick($event)\"\n (rowDoubleClick)=\"onItemDoubleClick($event)\">\n\n <!-- Selection column: explicit width so the auto table layout doesn't\n hand the near-empty column a share of the spare width -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\" width=\"20px\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n @if (!item.inlineCreate) {\n <tn-checkbox\n [hideLabel]=\"true\"\n [label]=\"'Select ' + item.name\"\n [testId]=\"itemTestId(item)\"\n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\" />\n }\n </ng-template>\n </ng-container>\n }\n\n <!-- Name column -->\n <ng-container tnColumnDef=\"name\">\n <ng-template tnHeaderCellDef>Name</ng-template>\n <ng-template let-item tnCellDef>\n\n <!-- Inline creation row for create actions with a `create` callback -->\n @if (item.inlineCreate) {\n @if (inlineCreation(); as creation) {\n <div class=\"inline-create-cell\">\n <div class=\"inline-create-row\" [class.has-error]=\"!!creation.error\">\n <tn-icon class=\"file-icon\" [name]=\"creation.action.icon || 'mdi-folder'\" />\n <input\n type=\"text\"\n class=\"inline-create-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n tnTestIdType=\"input\"\n [tnTestId]=\"inlineCreateTestId()\"\n [attr.aria-label]=\"creation.action.label + ' name'\"\n [attr.aria-invalid]=\"creation.error ? true : null\"\n [attr.aria-describedby]=\"creation.error ? inlineErrorId : null\"\n [class.error]=\"!!creation.error\"\n [disabled]=\"creation.loading\"\n [value]=\"creation.name\"\n (input)=\"onInlineCreationInput($event)\"\n (keydown)=\"onInlineCreationKeyDown($event)\"\n (blur)=\"onInlineCreationBlur()\">\n @if (creation.loading) {\n <tn-icon name=\"mdi-loading\" class=\"creation-loading-icon\" aria-hidden=\"true\" />\n }\n </div>\n @if (creation.error) {\n <div class=\"inline-create-error\" role=\"alert\" [id]=\"inlineErrorId\">\n <tn-icon name=\"mdi-alert-circle\" class=\"error-icon\" aria-hidden=\"true\" />\n <span class=\"error-text\">{{ creation.error }}</span>\n </div>\n }\n </div>\n }\n } @else {\n <div\n class=\"file-name-cell\"\n tnTestIdType=\"option\"\n [tnTestId]=\"itemTestId(item)\"\n [class.selected]=\"isSelected(item)\"\n [class.disabled]=\"!!item.dimmed\"\n [class.zfs-object]=\"isZfsObject(item)\">\n <tn-icon\n [name]=\"getItemIcon(item)\"\n [class]=\"'file-icon file-icon-' + item.type\" />\n <span class=\"file-name\">{{ item.name }}</span>\n\n <!-- ZFS badge -->\n @if (isZfsObject(item)) {\n <span\n [class]=\"'zfs-badge zfs-badge-' + item.type\">\n {{ getZfsBadge(item) }}\n </span>\n }\n\n <!-- Permission indicator -->\n @if (item.permissions === 'none') {\n <tn-icon\n name=\"lock\"\n library=\"mdi\"\n class=\"permission-icon\" />\n }\n\n <!-- Navigation affordance: single click opens the directory -->\n @if (isNavigatable(item)) {\n <button\n type=\"button\"\n class=\"navigate-button\"\n tnTestIdType=\"button\"\n [tnTestId]=\"navigateTestId(item)\"\n [attr.aria-label]=\"'Open ' + item.name\"\n [attr.title]=\"'Open ' + item.name\"\n (click)=\"onNavigateClick($event, item)\"\n (dblclick)=\"$event.stopPropagation()\">\n <tn-icon name=\"mdi-chevron-right\" size=\"sm\" />\n </button>\n }\n </div>\n }\n </ng-template>\n </ng-container>\n\n <!-- Size column -->\n <ng-container tnColumnDef=\"size\">\n <ng-template tnHeaderCellDef>Size</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.size !== undefined) {\n <span>{{ item.size | tnFileSize }}</span>\n }\n @if (item.size === undefined && item.type === 'folder') {\n <span class=\"folder-indicator\">--</span>\n }\n </ng-template>\n </ng-container>\n\n <!-- Modified column -->\n <ng-container tnColumnDef=\"modified\">\n <ng-template tnHeaderCellDef>Modified</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.modified) {\n <span>{{ formatDate(item.modified) }}</span>\n }\n </ng-template>\n </ng-container>\n\n\n </tn-table>\n\n <!-- Empty state -->\n @if (filteredFileItems().length === 0) {\n <div class=\"empty-state\">\n <tn-icon name=\"folder-open\" library=\"mdi\" customSize=\"48px\" />\n <p>No items found</p>\n </div>\n }\n </div>\n}\n\n<!-- Footer -->\n@if (!loading()) {\n <div class=\"tn-file-picker-footer\">\n <div class=\"footer-selection\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n <tn-button\n variant=\"outline\"\n label=\"Clear Selection\"\n [testId]=\"clearSelectionTestId()\"\n (onClick)=\"onClearSelection()\" />\n } @else if (allowCurrentDirectorySelection()) {\n <span class=\"selection-count current-directory-selection\">\n Current directory selected\n </span>\n } @else {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n </div>\n <div class=\"footer-actions\">\n @for (action of createActions(); track action.id) {\n <tn-button\n variant=\"outline\"\n [label]=\"action.label\"\n [testId]=\"createActionTestId(action)\"\n [disabled]=\"!!inlineCreation()\"\n (onClick)=\"onCreateAction(action)\" />\n }\n <!-- Disabled while the inline creation row is open: its blur auto-submit\n would race the selection submit -->\n <tn-button\n label=\"Select\"\n [testId]=\"selectTestId()\"\n [disabled]=\"(selectedItems().length === 0 && !allowCurrentDirectorySelection()) || !!inlineCreation()\"\n (onClick)=\"onSubmit()\" />\n </div>\n </div>\n}", styles: [":host{display:block;background:var(--tn-bg1, white);color:var(--tn-fg1, #333);padding:0;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014;border-radius:8px;border:1px solid var(--tn-lines, #e0e0e0);width:520px;max-width:calc(100vw - 32px);min-height:min(500px,100vh - 16px);max-height:min(600px,100vh - 16px);font-family:var(--tn-font-family-body);display:flex;flex-direction:column;overflow:hidden}.tn-file-picker-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tn-content-padding, 24px);padding-bottom:16px;border-bottom:1px solid var(--tn-lines)}.tn-file-picker-breadcrumb{display:flex;align-items:center;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 2px;border-radius:4px;font-size:1rem;white-space:nowrap;transition:background-color .15s ease-in-out}.tn-file-picker-breadcrumb .breadcrumb-segment:hover:not(:disabled){background:var(--tn-bg2)}.tn-file-picker-breadcrumb .breadcrumb-segment:disabled,.tn-file-picker-breadcrumb .breadcrumb-segment.current{color:var(--tn-fg1);cursor:default;font-weight:500}.tn-file-picker-breadcrumb .breadcrumb-segment:not(:last-child):after{content:\"/\";margin-left:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment:first-child:before{content:\"/\";margin-right:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment.nameless:not(:last-child):after{content:none}.tn-file-picker-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px;color:var(--tn-fg2)}.tn-file-picker-loading tn-icon{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tn-file-picker-content{flex:1;min-height:0;overflow-y:auto}.inline-create-cell{display:flex;flex-direction:column;gap:4px;flex:1;min-width:0}.inline-create-row{display:flex;align-items:center;gap:8px}.inline-create-row .inline-create-input{flex:1;padding:4px 8px;border:2px solid var(--tn-primary);border-radius:3px;background:var(--tn-bg1);color:var(--tn-fg1);font-size:inherit;font-family:inherit;outline:none;min-width:0}.inline-create-row .inline-create-input.error{border-color:var(--tn-error, #d32f2f)}.inline-create-row .inline-create-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2)}.inline-create-row .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary);flex-shrink:0}.inline-create-error{display:flex;align-items:center;gap:6px;margin-top:4px;padding:4px 8px;background:color-mix(in srgb,var(--tn-error, #d32f2f) 10%,transparent);border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.inline-create-error .error-icon{flex-shrink:0}.inline-create-error .error-text{flex:1}.file-list-viewport{width:100%;height:100%}.file-list-viewport .cdk-virtual-scroll-content-wrapper{width:100%}tn-table{width:100%}tn-table th,tn-table .tn-table__header-cell{font-weight:600;color:var(--tn-fg1);padding:12px 16px;border-bottom:2px solid var(--tn-lines)}tn-table td,tn-table .tn-table__cell{padding:8px 16px;border-bottom:1px solid var(--tn-lines)}.file-name-cell{display:flex;align-items:center;gap:8px;cursor:pointer}.file-name-cell.disabled{opacity:.5;color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-name{color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-icon{opacity:.6}.file-name-cell.disabled:has(.file-icon-folder),.file-name-cell.disabled:has(.file-icon-dataset),.file-name-cell.disabled:has(.file-icon-mountpoint){cursor:pointer}.file-name-cell.disabled:not(:has(.file-icon-folder)):not(:has(.file-icon-dataset)):not(:has(.file-icon-mountpoint)){cursor:not-allowed}.file-icon{display:flex;align-items:center;justify-content:center;font-size:var(--tn-icon-md, 20px);flex-shrink:0;line-height:1}.file-icon.file-icon-folder{color:var(--tn-primary)}.file-icon.file-icon-dataset{color:var(--tn-blue, #007db3)}.file-icon.file-icon-zvol{color:var(--tn-green, #71BF44)}.file-icon.file-icon-mountpoint{color:var(--tn-orange, #E68D37)}.file-name{flex:1;font-weight:500;line-height:1.4}.zfs-badge{display:inline-flex;align-items:center;background:var(--tn-alt-bg2);color:var(--tn-alt-fg2);font-size:.625rem;font-weight:600;padding:2px 6px;border-radius:12px;text-transform:uppercase;letter-spacing:.5px;line-height:1}.zfs-badge.zfs-badge-dataset{background:var(--tn-blue);color:#fff}.zfs-badge.zfs-badge-zvol{background:var(--tn-green);color:#fff}.zfs-badge.zfs-badge-mountpoint{background:var(--tn-orange);color:#fff}.permission-icon{display:flex;align-items:center;justify-content:center;color:var(--tn-red);font-size:var(--tn-icon-sm, 16px);line-height:1}.navigate-button{display:flex;align-items:center;justify-content:center;margin-left:auto;padding:2px;background:none;border:none;border-radius:4px;color:var(--tn-fg2);cursor:pointer}.navigate-button:hover,.navigate-button:focus-visible{background:var(--tn-bg3);color:var(--tn-primary)}.file-type{font-size:1rem;padding:2px 8px;border-radius:12px}.file-type.type-folder{background:var(--tn-alt-bg1);color:var(--tn-alt-fg2)}.file-type.type-file{background:var(--tn-bg2);color:var(--tn-fg2)}.file-type.type-dataset{background:#007db31a;color:var(--tn-blue)}.file-type.type-zvol{background:#71bf441a;color:var(--tn-green)}.file-type.type-mountpoint{background:#e68d371a;color:var(--tn-orange)}.folder-indicator{color:var(--tn-alt-fg1);font-style:italic}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--tn-alt-fg1);text-align:center}.empty-state tn-icon{margin-bottom:16px;opacity:.5}.empty-state p{margin:0;font-size:1rem}.tn-file-picker-footer{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px var(--tn-content-padding, 24px);border-top:1px solid var(--tn-lines);background:var(--tn-bg2);border-bottom-left-radius:8px;border-bottom-right-radius:8px}.tn-file-picker-footer .footer-selection{display:flex;align-items:center;gap:12px}.selection-count{font-size:1rem;color:var(--tn-fg2);font-weight:500}.footer-actions{display:flex;gap:8px}@media(prefers-reduced-motion:reduce){.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){:host{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnTableComponent, selector: "tn-table", inputs: ["dataSource", "displayedColumns", "trackBy", "emptyMessage", "emptyDescription", "emptyIcon", "selectable", "expandable", "bordered", "isRowExpandable", "activeRow", "activeWhen", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable", "expandOnRowClick", "singleExpand", "fixedLayout", "minColumnWidth", "minWidth", "mobileLayout", "cardBreakpoint", "cardPrimaryCount", "sortColumn", "sortDirection"], outputs: ["sortChange", "selectionChange", "rowClick", "rowDoubleClick", "sortColumnChange", "sortDirectionChange"] }, { kind: "directive", type: TnTableColumnDirective, selector: "[tnColumnDef]", inputs: ["tnColumnDef", "sortable", "width", "label", "cardPriority", "cardTitle", "cardHidden", "cardLabel"], exportAs: ["tnColumnDef"] }, { kind: "directive", type: TnHeaderCellDefDirective, selector: "[tnHeaderCellDef]" }, { kind: "directive", type: TnCellDefDirective, selector: "[tnCellDef]" }, { kind: "ngmodule", type: ScrollingModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
|
|
19355
20525
|
}
|
|
19356
20526
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, decorators: [{
|
|
19357
20527
|
type: Component,
|
|
@@ -21207,5 +22377,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
21207
22377
|
* Generated bundle index. Do not edit.
|
|
21208
22378
|
*/
|
|
21209
22379
|
|
|
21210
|
-
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, 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, 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 };
|
|
22380
|
+
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 };
|
|
21211
22381
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|