@truenas/ui-components 0.4.12 → 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.
|
@@ -11244,10 +11244,51 @@ class TnTableColumnDirective {
|
|
|
11244
11244
|
name = input.required({ ...(ngDevMode ? { debugName: "name" } : {}), alias: 'tnColumnDef' });
|
|
11245
11245
|
sortable = input(false, ...(ngDevMode ? [{ debugName: "sortable" }] : []));
|
|
11246
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" }] : []));
|
|
11247
11288
|
headerTemplate = contentChild(TnHeaderCellDefDirective, { ...(ngDevMode ? { debugName: "headerTemplate" } : {}), read: TemplateRef });
|
|
11248
11289
|
cellTemplate = contentChild(TnCellDefDirective, { ...(ngDevMode ? { debugName: "cellTemplate" } : {}), read: TemplateRef });
|
|
11249
11290
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableColumnDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
11250
|
-
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 });
|
|
11251
11292
|
}
|
|
11252
11293
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableColumnDirective, decorators: [{
|
|
11253
11294
|
type: Directive,
|
|
@@ -11256,7 +11297,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11256
11297
|
standalone: true,
|
|
11257
11298
|
exportAs: 'tnColumnDef',
|
|
11258
11299
|
}]
|
|
11259
|
-
}], 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), { ...{
|
|
11260
11301
|
read: TemplateRef,
|
|
11261
11302
|
}, isSignal: true }] }], cellTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCellDefDirective), { ...{ read: TemplateRef }, isSignal: true }] }] } });
|
|
11262
11303
|
/**
|
|
@@ -11285,6 +11326,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11285
11326
|
standalone: true,
|
|
11286
11327
|
}]
|
|
11287
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
|
+
}] });
|
|
11288
11361
|
|
|
11289
11362
|
/**
|
|
11290
11363
|
* Animation duration for detail row expand/collapse.
|
|
@@ -11307,6 +11380,16 @@ function getExpandDuration() {
|
|
|
11307
11380
|
}
|
|
11308
11381
|
class TnTableComponent {
|
|
11309
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;
|
|
11310
11393
|
// --- Core inputs ---
|
|
11311
11394
|
dataSource = input([], ...(ngDevMode ? [{ debugName: "dataSource" }] : []));
|
|
11312
11395
|
displayedColumns = input([], ...(ngDevMode ? [{ debugName: "displayedColumns" }] : []));
|
|
@@ -11374,6 +11457,12 @@ class TnTableComponent {
|
|
|
11374
11457
|
* When true, shows a spinner overlay over the table. Existing rows remain
|
|
11375
11458
|
* visible (dimmed) so reloads don't cause layout jumps; if there are no rows
|
|
11376
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.
|
|
11377
11466
|
*/
|
|
11378
11467
|
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
11379
11468
|
/** Accessible label announced while loading. */
|
|
@@ -11382,6 +11471,16 @@ class TnTableComponent {
|
|
|
11382
11471
|
* When true, rows become keyboard-focusable (tabindex=0) and clicking or
|
|
11383
11472
|
* pressing Enter/Space emits `rowClick`. Use this for "click row to view
|
|
11384
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.
|
|
11385
11484
|
*/
|
|
11386
11485
|
clickable = input(false, ...(ngDevMode ? [{ debugName: "clickable" }] : []));
|
|
11387
11486
|
/**
|
|
@@ -11389,6 +11488,12 @@ class TnTableComponent {
|
|
|
11389
11488
|
* chevron. Requires `clickable` — that is what makes rows activatable at all — and `expandable`;
|
|
11390
11489
|
* rows the `isRowExpandable` predicate rejects are unaffected, since `toggleRowExpansion` gates
|
|
11391
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.
|
|
11392
11497
|
*/
|
|
11393
11498
|
expandOnRowClick = input(false, ...(ngDevMode ? [{ debugName: "expandOnRowClick" }] : []));
|
|
11394
11499
|
/**
|
|
@@ -11443,9 +11548,52 @@ class TnTableComponent {
|
|
|
11443
11548
|
if (!perColumn || !this.fixedLayout()) {
|
|
11444
11549
|
return null;
|
|
11445
11550
|
}
|
|
11446
|
-
|
|
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})`;
|
|
11447
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" }] : []));
|
|
11448
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
|
+
*/
|
|
11449
11597
|
sortChange = output();
|
|
11450
11598
|
selectionChange = output();
|
|
11451
11599
|
/** Emits the row when a clickable row is activated (click or Enter/Space). */
|
|
@@ -11460,6 +11608,11 @@ class TnTableComponent {
|
|
|
11460
11608
|
// --- Content queries ---
|
|
11461
11609
|
columnDefs = contentChildren(TnTableColumnDirective, ...(ngDevMode ? [{ debugName: "columnDefs" }] : []));
|
|
11462
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;
|
|
11463
11616
|
// --- Sort state ---
|
|
11464
11617
|
/**
|
|
11465
11618
|
* Sorted column and direction. Two-way bindable (`[(sortColumn)]`), so a consumer that owns the
|
|
@@ -11475,6 +11628,9 @@ class TnTableComponent {
|
|
|
11475
11628
|
* approach could address this.
|
|
11476
11629
|
*/
|
|
11477
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++}`;
|
|
11478
11634
|
// --- Selection state ---
|
|
11479
11635
|
selection = new SelectionModel(true, []);
|
|
11480
11636
|
selectionCount = signal(0, ...(ngDevMode ? [{ debugName: "selectionCount" }] : []));
|
|
@@ -11535,14 +11691,147 @@ class TnTableComponent {
|
|
|
11535
11691
|
this.expandedRows.set(next);
|
|
11536
11692
|
}
|
|
11537
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);
|
|
11538
11822
|
}
|
|
11539
11823
|
ngOnInit() {
|
|
11540
11824
|
this.initialized = true;
|
|
11541
|
-
// Clean up SelectionModel on destroy
|
|
11542
11825
|
this.destroyRef.onDestroy(() => {
|
|
11543
11826
|
this.selection.clear();
|
|
11827
|
+
this.resizeObserver?.disconnect();
|
|
11544
11828
|
});
|
|
11545
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" }] : []));
|
|
11546
11835
|
// --- Computed ---
|
|
11547
11836
|
data = computed(() => {
|
|
11548
11837
|
const source = this.dataSource();
|
|
@@ -11561,6 +11850,13 @@ class TnTableComponent {
|
|
|
11561
11850
|
}
|
|
11562
11851
|
return cols;
|
|
11563
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" }] : []));
|
|
11564
11860
|
isAllSelected = computed(() => {
|
|
11565
11861
|
const numSelected = this.selectionCount();
|
|
11566
11862
|
const numRows = this.data().length;
|
|
@@ -11583,13 +11879,15 @@ class TnTableComponent {
|
|
|
11583
11879
|
if (!colDef?.sortable()) {
|
|
11584
11880
|
return;
|
|
11585
11881
|
}
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
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()) {
|
|
11593
11891
|
this.sortColumn.set('');
|
|
11594
11892
|
}
|
|
11595
11893
|
}
|
|
@@ -11597,8 +11895,12 @@ class TnTableComponent {
|
|
|
11597
11895
|
this.sortColumn.set(column);
|
|
11598
11896
|
this.sortDirection.set('asc');
|
|
11599
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.
|
|
11600
11902
|
this.sortChange.emit({
|
|
11601
|
-
column: this.sortColumn()
|
|
11903
|
+
column: this.sortColumn(),
|
|
11602
11904
|
direction: this.sortDirection(),
|
|
11603
11905
|
});
|
|
11604
11906
|
}
|
|
@@ -11649,6 +11951,21 @@ class TnTableComponent {
|
|
|
11649
11951
|
isRowExpandTrigger(row) {
|
|
11650
11952
|
return this.expandOnRowClick() && this.clickable() && this.canExpandRow(row);
|
|
11651
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
|
+
}
|
|
11652
11969
|
// --- Active row ---
|
|
11653
11970
|
isRowActive(row) {
|
|
11654
11971
|
if (this.activeWhen()?.(row)) {
|
|
@@ -11658,25 +11975,47 @@ class TnTableComponent {
|
|
|
11658
11975
|
return active !== null && active === row;
|
|
11659
11976
|
}
|
|
11660
11977
|
// --- Row click ---
|
|
11661
|
-
|
|
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) {
|
|
11662
11985
|
if (!this.clickable()) {
|
|
11663
11986
|
return;
|
|
11664
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
|
+
}
|
|
11665
11997
|
if (this.expandOnRowClick()) {
|
|
11666
11998
|
this.toggleRowExpansion(row);
|
|
11667
11999
|
}
|
|
11668
12000
|
this.rowClick.emit(row);
|
|
11669
12001
|
}
|
|
11670
|
-
|
|
12002
|
+
/** @param event See {@link onRowClick} — same guard, same optionality. */
|
|
12003
|
+
onRowDoubleClick(row, event) {
|
|
11671
12004
|
if (!this.clickable()) {
|
|
11672
12005
|
return;
|
|
11673
12006
|
}
|
|
12007
|
+
if (event && this.isRowControlTarget(event)) {
|
|
12008
|
+
return;
|
|
12009
|
+
}
|
|
11674
12010
|
this.rowDoubleClick.emit(row);
|
|
11675
12011
|
}
|
|
11676
12012
|
onRowKeydown(event, row) {
|
|
11677
12013
|
if (!this.clickable()) {
|
|
11678
12014
|
return;
|
|
11679
12015
|
}
|
|
12016
|
+
if (this.isRowControlTarget(event)) {
|
|
12017
|
+
return;
|
|
12018
|
+
}
|
|
11680
12019
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
11681
12020
|
event.preventDefault();
|
|
11682
12021
|
if (this.expandOnRowClick()) {
|
|
@@ -11685,6 +12024,135 @@ class TnTableComponent {
|
|
|
11685
12024
|
this.rowClick.emit(row);
|
|
11686
12025
|
}
|
|
11687
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
|
+
}
|
|
11688
12156
|
// --- Selection methods ---
|
|
11689
12157
|
toggleSelectAll() {
|
|
11690
12158
|
if (this.isAllSelected()) {
|
|
@@ -11704,15 +12172,138 @@ class TnTableComponent {
|
|
|
11704
12172
|
isRowSelected(row) {
|
|
11705
12173
|
return this.selection.isSelected(row);
|
|
11706
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
|
+
}
|
|
11707
12290
|
// --- Column helpers ---
|
|
11708
12291
|
getColumnDef(columnName) {
|
|
11709
12292
|
return this.columnDefMap().get(columnName);
|
|
11710
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
|
+
}
|
|
11711
12302
|
getCellValue(row, column) {
|
|
11712
12303
|
return row[column];
|
|
11713
12304
|
}
|
|
11714
12305
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11715
|
-
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: [
|
|
11716
12307
|
trigger('detailExpand', [
|
|
11717
12308
|
state('collapsed,void', style({ height: '0px', minHeight: '0', overflow: 'hidden' })),
|
|
11718
12309
|
state('expanded', style({ height: '*' })),
|
|
@@ -11733,10 +12324,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11733
12324
|
'[class.tn-table--bordered]': 'bordered()',
|
|
11734
12325
|
'[class.tn-table--fixed-layout]': 'fixedLayout()',
|
|
11735
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()',
|
|
11736
12334
|
'[style.--tn-table-active-bg]': 'activeBg()',
|
|
11737
12335
|
'[style.--tn-table-active-indicator]': 'activeIndicator()',
|
|
11738
|
-
}, 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"] }]
|
|
11739
|
-
}], 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"] }] } });
|
|
11740
12338
|
|
|
11741
12339
|
/**
|
|
11742
12340
|
* Harness for interacting with `tn-table` in tests.
|
|
@@ -11770,18 +12368,59 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11770
12368
|
/**
|
|
11771
12369
|
* Gets the number of data rows (excludes header and detail rows).
|
|
11772
12370
|
*
|
|
11773
|
-
*
|
|
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.
|
|
11774
12375
|
*/
|
|
11775
12376
|
async getRowCount() {
|
|
11776
|
-
|
|
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)();
|
|
11777
12382
|
return rows.length;
|
|
11778
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
|
+
}
|
|
11779
12415
|
/**
|
|
11780
12416
|
* Gets the text content of header cells (excludes sort icons).
|
|
11781
12417
|
*
|
|
11782
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.
|
|
11783
12421
|
*/
|
|
11784
12422
|
async getHeaderTexts() {
|
|
12423
|
+
await this.assertTableLayout('getHeaderTexts');
|
|
11785
12424
|
const textEls = await this.locatorForAll('.tn-table__header-text')();
|
|
11786
12425
|
const texts = [];
|
|
11787
12426
|
for (const el of textEls) {
|
|
@@ -11794,8 +12433,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11794
12433
|
*
|
|
11795
12434
|
* @param rowIndex Zero-based index of the data row.
|
|
11796
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.
|
|
11797
12437
|
*/
|
|
11798
12438
|
async getRowTexts(rowIndex) {
|
|
12439
|
+
await this.assertTableLayout('getRowTexts');
|
|
11799
12440
|
await this.assertRowExists(rowIndex);
|
|
11800
12441
|
const cells = await this.locatorForAll(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__cell[data-column]`)();
|
|
11801
12442
|
const texts = [];
|
|
@@ -11810,8 +12451,11 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11810
12451
|
* @param rowIndex Zero-based index of the data row.
|
|
11811
12452
|
* @param columnName The column's data-column attribute value.
|
|
11812
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.
|
|
11813
12456
|
*/
|
|
11814
12457
|
async getCellText(rowIndex, columnName) {
|
|
12458
|
+
await this.assertTableLayout('getCellText');
|
|
11815
12459
|
await this.assertRowExists(rowIndex);
|
|
11816
12460
|
const cell = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"] [data-column="${columnName}"]`)();
|
|
11817
12461
|
return (await cell.text()).trim();
|
|
@@ -11820,8 +12464,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11820
12464
|
* Gets all row texts as a 2D string array (data-column cells only).
|
|
11821
12465
|
*
|
|
11822
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.
|
|
11823
12468
|
*/
|
|
11824
12469
|
async getAllRowTexts() {
|
|
12470
|
+
await this.assertTableLayout('getAllRowTexts');
|
|
11825
12471
|
const count = await this.getRowCount();
|
|
11826
12472
|
const result = [];
|
|
11827
12473
|
for (let i = 0; i < count; i++) {
|
|
@@ -11834,8 +12480,11 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11834
12480
|
* Clicks a sortable column header to cycle sort direction.
|
|
11835
12481
|
*
|
|
11836
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.
|
|
11837
12485
|
*/
|
|
11838
12486
|
async clickSortHeader(columnName) {
|
|
12487
|
+
await this.assertTableLayout('clickSortHeader');
|
|
11839
12488
|
const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
|
|
11840
12489
|
await header.click();
|
|
11841
12490
|
}
|
|
@@ -11844,59 +12493,105 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11844
12493
|
*
|
|
11845
12494
|
* @param columnName The column's data-column attribute value.
|
|
11846
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.
|
|
11847
12498
|
*/
|
|
11848
12499
|
async isSortable(columnName) {
|
|
12500
|
+
await this.assertTableLayout('isSortable');
|
|
11849
12501
|
const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
|
|
11850
12502
|
return header.hasClass('tn-table__header-cell--sortable');
|
|
11851
12503
|
}
|
|
11852
12504
|
/**
|
|
11853
12505
|
* Gets the current sort direction for a column via aria-sort.
|
|
11854
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
|
+
*
|
|
11855
12511
|
* @param columnName The column's data-column attribute value.
|
|
11856
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.
|
|
11857
12515
|
*/
|
|
11858
12516
|
async getSortDirection(columnName) {
|
|
12517
|
+
await this.assertTableLayout('getSortDirection');
|
|
11859
12518
|
const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
|
|
11860
|
-
|
|
12519
|
+
const sort = await header.getAttribute('aria-sort');
|
|
12520
|
+
return sort === 'none' ? null : sort;
|
|
11861
12521
|
}
|
|
11862
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
|
+
}
|
|
11863
12537
|
/**
|
|
11864
|
-
* 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.
|
|
11865
12540
|
*/
|
|
11866
12541
|
async toggleSelectAll() {
|
|
11867
|
-
const
|
|
11868
|
-
|
|
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();
|
|
11869
12547
|
}
|
|
11870
12548
|
/**
|
|
11871
|
-
* 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.
|
|
11872
12551
|
*
|
|
11873
12552
|
* @param rowIndex Zero-based index of the data row.
|
|
11874
12553
|
*/
|
|
11875
12554
|
async toggleRowSelection(rowIndex) {
|
|
11876
|
-
await this.
|
|
11877
|
-
const
|
|
11878
|
-
|
|
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();
|
|
11879
12561
|
}
|
|
11880
12562
|
/**
|
|
11881
|
-
* Checks if a specific row
|
|
12563
|
+
* Checks if a specific row (or its card) is selected.
|
|
11882
12564
|
*
|
|
11883
12565
|
* @param rowIndex Zero-based index of the data row.
|
|
11884
12566
|
* @returns Promise resolving to true if the row's checkbox is checked.
|
|
11885
12567
|
*/
|
|
11886
12568
|
async isRowSelected(rowIndex) {
|
|
11887
|
-
await this.
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
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 }))();
|
|
11891
12579
|
return checkbox.isChecked();
|
|
11892
12580
|
}
|
|
11893
12581
|
/**
|
|
11894
|
-
* Gets the count of currently selected rows.
|
|
12582
|
+
* Gets the count of currently selected rows, in either layout.
|
|
11895
12583
|
*
|
|
11896
12584
|
* @returns Promise resolving to the number of checked row checkboxes.
|
|
11897
12585
|
*/
|
|
11898
12586
|
async getSelectedRowCount() {
|
|
11899
|
-
|
|
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 }))();
|
|
11900
12595
|
let count = 0;
|
|
11901
12596
|
for (const cb of checkboxes) {
|
|
11902
12597
|
if (await cb.isChecked()) {
|
|
@@ -11910,8 +12605,11 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11910
12605
|
* Clicks the expand button for a specific row.
|
|
11911
12606
|
*
|
|
11912
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.
|
|
11913
12610
|
*/
|
|
11914
12611
|
async toggleRowExpansion(rowIndex) {
|
|
12612
|
+
await this.assertTableLayout('toggleRowExpansion');
|
|
11915
12613
|
await this.assertRowExists(rowIndex);
|
|
11916
12614
|
const button = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__expand-button`)();
|
|
11917
12615
|
await button.click();
|
|
@@ -11921,8 +12619,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11921
12619
|
*
|
|
11922
12620
|
* @param rowIndex Zero-based index of the data row.
|
|
11923
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.
|
|
11924
12623
|
*/
|
|
11925
12624
|
async isRowExpanded(rowIndex) {
|
|
12625
|
+
await this.assertTableLayout('isRowExpanded');
|
|
11926
12626
|
await this.assertRowExists(rowIndex);
|
|
11927
12627
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11928
12628
|
return row.hasClass('tn-table__row--expanded');
|
|
@@ -11933,8 +12633,11 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11933
12633
|
*
|
|
11934
12634
|
* @param rowIndex Zero-based index of the data row.
|
|
11935
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.
|
|
11936
12638
|
*/
|
|
11937
12639
|
async hasExpandControl(rowIndex) {
|
|
12640
|
+
await this.assertTableLayout('hasExpandControl');
|
|
11938
12641
|
await this.assertRowExists(rowIndex);
|
|
11939
12642
|
const button = await this.locatorForOptional(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__expand-button`)();
|
|
11940
12643
|
return button !== null;
|
|
@@ -11944,8 +12647,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11944
12647
|
* Clicks a row (for tables with `clickable` enabled).
|
|
11945
12648
|
*
|
|
11946
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.
|
|
11947
12651
|
*/
|
|
11948
12652
|
async clickRow(rowIndex) {
|
|
12653
|
+
await this.assertTableLayout('clickRow');
|
|
11949
12654
|
await this.assertRowExists(rowIndex);
|
|
11950
12655
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11951
12656
|
await row.click();
|
|
@@ -11956,8 +12661,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11956
12661
|
* clicks first; this helper dispatches only the `dblclick` event.
|
|
11957
12662
|
*
|
|
11958
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.
|
|
11959
12665
|
*/
|
|
11960
12666
|
async doubleClickRow(rowIndex) {
|
|
12667
|
+
await this.assertTableLayout('doubleClickRow');
|
|
11961
12668
|
await this.assertRowExists(rowIndex);
|
|
11962
12669
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11963
12670
|
await row.dispatchEvent('dblclick');
|
|
@@ -11967,8 +12674,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11967
12674
|
*
|
|
11968
12675
|
* @param rowIndex Zero-based index of the data row.
|
|
11969
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.
|
|
11970
12678
|
*/
|
|
11971
12679
|
async pressKeyOnRow(rowIndex, key) {
|
|
12680
|
+
await this.assertTableLayout('pressKeyOnRow');
|
|
11972
12681
|
await this.assertRowExists(rowIndex);
|
|
11973
12682
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11974
12683
|
await row.focus();
|
|
@@ -11983,8 +12692,10 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11983
12692
|
* Checks if a row is keyboard-focusable (tabindex=0).
|
|
11984
12693
|
*
|
|
11985
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.
|
|
11986
12696
|
*/
|
|
11987
12697
|
async isRowFocusable(rowIndex) {
|
|
12698
|
+
await this.assertTableLayout('isRowFocusable');
|
|
11988
12699
|
await this.assertRowExists(rowIndex);
|
|
11989
12700
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11990
12701
|
return (await row.getAttribute('tabindex')) === '0';
|
|
@@ -12003,10 +12714,17 @@ class TnTableHarness extends ComponentHarness {
|
|
|
12003
12714
|
/**
|
|
12004
12715
|
* Checks if a data row is currently marked active.
|
|
12005
12716
|
*
|
|
12006
|
-
*
|
|
12007
|
-
*
|
|
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.
|
|
12008
12722
|
*/
|
|
12009
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
|
+
}
|
|
12010
12728
|
await this.assertRowExists(rowIndex);
|
|
12011
12729
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
12012
12730
|
return row.hasClass('tn-table__row--active');
|
|
@@ -12017,11 +12735,19 @@ class TnTableHarness extends ComponentHarness {
|
|
|
12017
12735
|
* @returns Promise resolving to the active row index or null.
|
|
12018
12736
|
*/
|
|
12019
12737
|
async getActiveRowIndex() {
|
|
12020
|
-
|
|
12021
|
-
|
|
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) {
|
|
12022
12748
|
return null;
|
|
12023
12749
|
}
|
|
12024
|
-
const attr = await
|
|
12750
|
+
const attr = await active.getAttribute('data-row-index');
|
|
12025
12751
|
return attr === null ? null : Number(attr);
|
|
12026
12752
|
}
|
|
12027
12753
|
/**
|
|
@@ -12029,8 +12755,11 @@ class TnTableHarness extends ComponentHarness {
|
|
|
12029
12755
|
*
|
|
12030
12756
|
* @param detailIndex Zero-based index among currently visible detail rows.
|
|
12031
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.
|
|
12032
12760
|
*/
|
|
12033
12761
|
async getDetailRowContent(detailIndex) {
|
|
12762
|
+
await this.assertTableLayout('getDetailRowContent');
|
|
12034
12763
|
const detailRows = await this.locatorForAll('.tn-table__detail-row')();
|
|
12035
12764
|
if (detailIndex >= detailRows.length) {
|
|
12036
12765
|
throw new Error(`Detail row index ${detailIndex} out of bounds (${detailRows.length} detail rows)`);
|
|
@@ -12040,21 +12769,355 @@ class TnTableHarness extends ComponentHarness {
|
|
|
12040
12769
|
/**
|
|
12041
12770
|
* Gets the count of currently expanded detail rows.
|
|
12042
12771
|
*
|
|
12043
|
-
*
|
|
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.
|
|
12044
12777
|
*/
|
|
12045
12778
|
async getExpandedRowCount() {
|
|
12779
|
+
if (await this.isCards()) {
|
|
12780
|
+
const panels = await this.locatorForAll('.tn-table__card-detail')();
|
|
12781
|
+
return panels.length;
|
|
12782
|
+
}
|
|
12046
12783
|
const detailRows = await this.locatorForAll('.tn-table__detail-row')();
|
|
12047
12784
|
return detailRows.length;
|
|
12048
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
|
+
}
|
|
12049
13011
|
// --- Internal helpers ---
|
|
12050
|
-
|
|
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) {
|
|
12051
13027
|
const count = await this.getRowCount();
|
|
12052
|
-
if (
|
|
12053
|
-
|
|
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)`);
|
|
12054
13041
|
}
|
|
12055
13042
|
}
|
|
12056
13043
|
}
|
|
12057
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
|
+
|
|
12058
13121
|
/** English defaults used when no `TN_TABLE_PAGER_LABELS` provider is registered. */
|
|
12059
13122
|
const TN_TABLE_PAGER_DEFAULT_LABELS = {
|
|
12060
13123
|
itemsPerPage: 'Items per page',
|
|
@@ -19458,7 +20521,7 @@ class TnFilePickerPopupComponent {
|
|
|
19458
20521
|
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${timePart}`;
|
|
19459
20522
|
}
|
|
19460
20523
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
19461
|
-
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" }] });
|
|
19462
20525
|
}
|
|
19463
20526
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, decorators: [{
|
|
19464
20527
|
type: Component,
|
|
@@ -21314,5 +22377,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
21314
22377
|
* Generated bundle index. Do not edit.
|
|
21315
22378
|
*/
|
|
21316
22379
|
|
|
21317
|
-
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 };
|
|
21318
22381
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|