@truenas/ui-components 0.1.58 → 0.1.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, Renderer2, ElementRef, input, effect, Directive, output, viewChild, signal, computed, forwardRef, Component, model, afterNextRender, ChangeDetectionStrategy, Injectable, isDevMode, ViewEncapsulation, contentChildren, ViewContainerRef, contentChild, ChangeDetectorRef, HostListener, TemplateRef, DestroyRef, IterableDiffers, Pipe,
|
|
3
|
-
import * as
|
|
2
|
+
import { InjectionToken, inject, Renderer2, ElementRef, input, effect, Directive, output, viewChild, signal, computed, forwardRef, Component, model, afterNextRender, ChangeDetectionStrategy, Injectable, isDevMode, ViewEncapsulation, contentChildren, ViewContainerRef, contentChild, ChangeDetectorRef, HostListener, TemplateRef, DestroyRef, isSignal, untracked, IterableDiffers, Pipe, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
|
|
3
|
+
import * as i2 from '@angular/forms';
|
|
4
4
|
import { NG_VALUE_ACCESSOR, FormsModule, NgControl } from '@angular/forms';
|
|
5
5
|
import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
|
|
6
6
|
import * as i1 from '@angular/cdk/a11y';
|
|
@@ -20,7 +20,7 @@ import { trigger, state, transition, style, animate } from '@angular/animations'
|
|
|
20
20
|
import { SPACE, ENTER, END, HOME, DOWN_ARROW, UP_ARROW, RIGHT_ARROW, LEFT_ARROW } from '@angular/cdk/keycodes';
|
|
21
21
|
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
|
22
22
|
import { SelectionModel, DataSource } from '@angular/cdk/collections';
|
|
23
|
-
import * as i2 from '@angular/cdk/tree';
|
|
23
|
+
import * as i2$1 from '@angular/cdk/tree';
|
|
24
24
|
import { CdkTree, CdkTreeModule, CdkTreeNode, CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet, CdkNestedTreeNode } from '@angular/cdk/tree';
|
|
25
25
|
export { FlatTreeControl } from '@angular/cdk/tree';
|
|
26
26
|
import { map } from 'rxjs/operators';
|
|
@@ -5255,43 +5255,144 @@ class TnSelectComponent {
|
|
|
5255
5255
|
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
5256
5256
|
optionGroups = input([], ...(ngDevMode ? [{ debugName: "optionGroups" }] : []));
|
|
5257
5257
|
placeholder = input('Select an option', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
5258
|
+
/**
|
|
5259
|
+
* Accessible label for the select trigger. When set, this is used as the
|
|
5260
|
+
* trigger's `aria-label` instead of the visible `placeholder` — useful in
|
|
5261
|
+
* contexts (e.g. a pager's page-size dropdown) where the placeholder text
|
|
5262
|
+
* doesn't accurately describe the field's purpose to screen readers.
|
|
5263
|
+
*/
|
|
5264
|
+
ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
5265
|
+
/**
|
|
5266
|
+
* Message shown inside the dropdown when no options (and no option groups)
|
|
5267
|
+
* are available. Defaults to the English `'No options available'`; consumers
|
|
5268
|
+
* with i18n requirements can pass a translated string.
|
|
5269
|
+
*/
|
|
5270
|
+
noOptionsLabel = input('No options available', ...(ngDevMode ? [{ debugName: "noOptionsLabel" }] : []));
|
|
5258
5271
|
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
5259
5272
|
testId = input('', ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
5260
5273
|
multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
|
|
5274
|
+
/**
|
|
5275
|
+
* Custom comparator for matching option values against the selected value(s).
|
|
5276
|
+
*
|
|
5277
|
+
* When the option values are objects, **provide this** — the built-in
|
|
5278
|
+
* fallback uses `JSON.stringify`, which is key-order dependent and can
|
|
5279
|
+
* produce false negatives for structurally equal objects. For primitives the
|
|
5280
|
+
* default identity check is fine.
|
|
5281
|
+
*/
|
|
5261
5282
|
compareWith = input(...(ngDevMode ? [undefined, { debugName: "compareWith" }] : []));
|
|
5262
5283
|
selectionChange = output();
|
|
5263
5284
|
/** Emits the full array of selected values after each toggle in multiple mode. */
|
|
5264
5285
|
multiSelectionChange = output();
|
|
5265
5286
|
// Internal state signals
|
|
5266
5287
|
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
5288
|
+
dropdownPosition = signal('below', ...(ngDevMode ? [{ debugName: "dropdownPosition" }] : []));
|
|
5267
5289
|
selectedValue = signal(null, ...(ngDevMode ? [{ debugName: "selectedValue" }] : []));
|
|
5268
5290
|
selectedValues = signal([], ...(ngDevMode ? [{ debugName: "selectedValues" }] : []));
|
|
5291
|
+
/** Index into `flatOptions` of the keyboard-focused row (-1 when none). */
|
|
5292
|
+
focusedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "focusedIndex" }] : []));
|
|
5269
5293
|
formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
|
|
5294
|
+
// Name of the CSS custom property that defines the dropdown's max-height
|
|
5295
|
+
// (set in select.component.scss). Reading it via getComputedStyle keeps the
|
|
5296
|
+
// flip-up heuristic in sync with the stylesheet — no duplicated constant.
|
|
5297
|
+
static DROPDOWN_MAX_HEIGHT_VAR = '--tn-select-dropdown-max-height';
|
|
5298
|
+
// Fallback used when getComputedStyle can't resolve the variable (older
|
|
5299
|
+
// browsers, jsdom in some test configs).
|
|
5300
|
+
static DROPDOWN_MAX_HEIGHT_FALLBACK = 200;
|
|
5301
|
+
// Per-instance suffix used to namespace DOM ids when `testId` is empty.
|
|
5302
|
+
// Without this, two `<tn-select>` elements with no testId would emit
|
|
5303
|
+
// colliding option/dropdown/group ids, breaking aria-activedescendant.
|
|
5304
|
+
// A random suffix is preferred over a monotonic counter so id values stay
|
|
5305
|
+
// stable from test file to test file (a counter would grow unpredictably
|
|
5306
|
+
// across suites and break snapshot tests).
|
|
5307
|
+
fallbackId = `auto-${Math.random().toString(36).slice(2, 10)}`;
|
|
5308
|
+
uniqueId = computed(() => this.testId() || this.fallbackId, ...(ngDevMode ? [{ debugName: "uniqueId" }] : []));
|
|
5270
5309
|
// Computed disabled state (combines input and form state)
|
|
5271
5310
|
isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
5311
|
+
/**
|
|
5312
|
+
* Flattened option list (ungrouped + grouped, in render order). The keyboard
|
|
5313
|
+
* navigation walks this list — entries from disabled groups are kept but
|
|
5314
|
+
* marked disabled so the cursor skips over them correctly.
|
|
5315
|
+
*/
|
|
5316
|
+
flatOptions = computed(() => {
|
|
5317
|
+
const flat = [...this.options()];
|
|
5318
|
+
for (const group of this.optionGroups()) {
|
|
5319
|
+
for (const opt of group.options) {
|
|
5320
|
+
flat.push({ ...opt, disabled: opt.disabled || group.disabled });
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
return flat;
|
|
5324
|
+
}, ...(ngDevMode ? [{ debugName: "flatOptions" }] : []));
|
|
5325
|
+
/**
|
|
5326
|
+
* Starting flat-index of each option group, used by the template to
|
|
5327
|
+
* translate a (group, option) pair into the matching `flatOptions` index.
|
|
5328
|
+
*/
|
|
5329
|
+
groupOffsets = computed(() => {
|
|
5330
|
+
const offsets = [];
|
|
5331
|
+
let offset = this.options().length;
|
|
5332
|
+
for (const group of this.optionGroups()) {
|
|
5333
|
+
offsets.push(offset);
|
|
5334
|
+
offset += group.options.length;
|
|
5335
|
+
}
|
|
5336
|
+
return offsets;
|
|
5337
|
+
}, ...(ngDevMode ? [{ debugName: "groupOffsets" }] : []));
|
|
5338
|
+
/** `aria-activedescendant` id for the focused option (or null). */
|
|
5339
|
+
activeOptionId = computed(() => {
|
|
5340
|
+
const idx = this.focusedIndex();
|
|
5341
|
+
if (idx < 0 || !this.isOpen()) {
|
|
5342
|
+
return null;
|
|
5343
|
+
}
|
|
5344
|
+
return this.optionId(idx);
|
|
5345
|
+
}, ...(ngDevMode ? [{ debugName: "activeOptionId" }] : []));
|
|
5272
5346
|
onChange = (_value) => { };
|
|
5273
5347
|
onTouched = () => { };
|
|
5274
5348
|
elementRef = inject(ElementRef);
|
|
5275
5349
|
cdr = inject(ChangeDetectorRef);
|
|
5350
|
+
triggerEl = viewChild('trigger', ...(ngDevMode ? [{ debugName: "triggerEl" }] : []));
|
|
5276
5351
|
constructor() {
|
|
5277
|
-
// Click-outside detection
|
|
5352
|
+
// Click-outside detection. Cleanup is registered via the `onCleanup`
|
|
5353
|
+
// callback (returning a function from `effect()` does *not* register one) —
|
|
5354
|
+
// which fires both when the effect re-runs and when the component's
|
|
5355
|
+
// injector is destroyed, so the listener is removed even if the host is
|
|
5356
|
+
// torn down while the dropdown is still open. The `disposed` flag prevents
|
|
5357
|
+
// the listener from being attached at all if teardown races the deferred
|
|
5358
|
+
// setTimeout — without it, a cleanup that fires between the timeout firing
|
|
5359
|
+
// and addEventListener executing could leak a permanent listener.
|
|
5360
|
+
effect((onCleanup) => {
|
|
5361
|
+
if (!this.isOpen()) {
|
|
5362
|
+
return;
|
|
5363
|
+
}
|
|
5364
|
+
let disposed = false;
|
|
5365
|
+
const clickListener = (event) => {
|
|
5366
|
+
if (!this.elementRef.nativeElement.contains(event.target)) {
|
|
5367
|
+
// Click outside → close, but don't steal focus from whatever the
|
|
5368
|
+
// user clicked on.
|
|
5369
|
+
this.closeDropdown({ restoreFocus: false });
|
|
5370
|
+
}
|
|
5371
|
+
};
|
|
5372
|
+
// Deferred so the click that opened the dropdown doesn't immediately
|
|
5373
|
+
// close it on its bubble back up to the document.
|
|
5374
|
+
const timeoutId = setTimeout(() => {
|
|
5375
|
+
if (disposed) {
|
|
5376
|
+
return;
|
|
5377
|
+
}
|
|
5378
|
+
document.addEventListener('click', clickListener);
|
|
5379
|
+
}, 0);
|
|
5380
|
+
onCleanup(() => {
|
|
5381
|
+
disposed = true;
|
|
5382
|
+
clearTimeout(timeoutId);
|
|
5383
|
+
document.removeEventListener('click', clickListener);
|
|
5384
|
+
});
|
|
5385
|
+
});
|
|
5386
|
+
// When the dropdown opens, scroll the focused option into view.
|
|
5278
5387
|
effect(() => {
|
|
5279
|
-
if (this.isOpen()) {
|
|
5280
|
-
|
|
5281
|
-
if (!this.elementRef.nativeElement.contains(event.target)) {
|
|
5282
|
-
this.closeDropdown();
|
|
5283
|
-
}
|
|
5284
|
-
};
|
|
5285
|
-
// Add listener after a small delay to avoid immediate closure
|
|
5286
|
-
setTimeout(() => {
|
|
5287
|
-
document.addEventListener('click', clickListener);
|
|
5288
|
-
}, 0);
|
|
5289
|
-
// Cleanup function
|
|
5290
|
-
return () => {
|
|
5291
|
-
document.removeEventListener('click', clickListener);
|
|
5292
|
-
};
|
|
5388
|
+
if (!this.isOpen()) {
|
|
5389
|
+
return;
|
|
5293
5390
|
}
|
|
5294
|
-
|
|
5391
|
+
const idx = this.focusedIndex();
|
|
5392
|
+
if (idx < 0) {
|
|
5393
|
+
return;
|
|
5394
|
+
}
|
|
5395
|
+
queueMicrotask(() => this.scrollFocusedIntoView());
|
|
5295
5396
|
});
|
|
5296
5397
|
}
|
|
5297
5398
|
// ControlValueAccessor implementation
|
|
@@ -5322,14 +5423,107 @@ class TnSelectComponent {
|
|
|
5322
5423
|
if (this.isDisabled()) {
|
|
5323
5424
|
return;
|
|
5324
5425
|
}
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5426
|
+
if (this.isOpen()) {
|
|
5427
|
+
this.closeDropdown();
|
|
5428
|
+
}
|
|
5429
|
+
else {
|
|
5430
|
+
this.openDropdown();
|
|
5328
5431
|
}
|
|
5329
5432
|
}
|
|
5330
|
-
|
|
5433
|
+
/**
|
|
5434
|
+
* Open the dropdown, seed the keyboard cursor on the currently-selected
|
|
5435
|
+
* option (or the first focusable one), and decide whether to flip up.
|
|
5436
|
+
*/
|
|
5437
|
+
openDropdown() {
|
|
5438
|
+
if (this.isDisabled()) {
|
|
5439
|
+
return;
|
|
5440
|
+
}
|
|
5441
|
+
this.dropdownPosition.set(this.computeDropdownPosition());
|
|
5442
|
+
this.isOpen.set(true);
|
|
5443
|
+
this.focusedIndex.set(this.initialFocusIndex());
|
|
5444
|
+
}
|
|
5445
|
+
/**
|
|
5446
|
+
* Close the dropdown.
|
|
5447
|
+
*
|
|
5448
|
+
* @param restoreFocus When `true` (default), return focus to the trigger so
|
|
5449
|
+
* keyboard users land somewhere sensible. Pass `false` for click-outside
|
|
5450
|
+
* so we don't steal focus from the element the user just navigated to.
|
|
5451
|
+
*/
|
|
5452
|
+
closeDropdown(options = {}) {
|
|
5453
|
+
const restoreFocus = options.restoreFocus ?? true;
|
|
5454
|
+
if (!this.isOpen()) {
|
|
5455
|
+
return;
|
|
5456
|
+
}
|
|
5331
5457
|
this.isOpen.set(false);
|
|
5458
|
+
this.focusedIndex.set(-1);
|
|
5332
5459
|
this.onTouched();
|
|
5460
|
+
if (restoreFocus) {
|
|
5461
|
+
this.triggerEl()?.nativeElement.focus({ preventScroll: true });
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
/** Picks the initial focused-row index when the dropdown opens. */
|
|
5465
|
+
initialFocusIndex() {
|
|
5466
|
+
const flat = this.flatOptions();
|
|
5467
|
+
if (flat.length === 0) {
|
|
5468
|
+
return -1;
|
|
5469
|
+
}
|
|
5470
|
+
// Prefer the currently selected option (or first selected in multi mode).
|
|
5471
|
+
if (this.multiple()) {
|
|
5472
|
+
const values = this.selectedValues();
|
|
5473
|
+
if (values.length > 0) {
|
|
5474
|
+
const idx = flat.findIndex((opt) => values.some((v) => this.compareValues(v, opt.value)));
|
|
5475
|
+
if (idx >= 0 && !flat[idx].disabled) {
|
|
5476
|
+
return idx;
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
else {
|
|
5481
|
+
const value = this.selectedValue();
|
|
5482
|
+
if (value !== null && value !== undefined) {
|
|
5483
|
+
const idx = flat.findIndex((opt) => this.compareValues(opt.value, value));
|
|
5484
|
+
if (idx >= 0 && !flat[idx].disabled) {
|
|
5485
|
+
return idx;
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
}
|
|
5489
|
+
// Otherwise the first non-disabled option.
|
|
5490
|
+
return flat.findIndex((opt) => !opt.disabled);
|
|
5491
|
+
}
|
|
5492
|
+
/**
|
|
5493
|
+
* Decide whether the dropdown should open above or below the trigger.
|
|
5494
|
+
* Opens above when there isn't enough space below the trigger AND there is
|
|
5495
|
+
* more space above — otherwise stays below. Falls back to `'below'` when no
|
|
5496
|
+
* trigger element is found yet.
|
|
5497
|
+
*/
|
|
5498
|
+
computeDropdownPosition() {
|
|
5499
|
+
if (typeof window === 'undefined') {
|
|
5500
|
+
return 'below';
|
|
5501
|
+
}
|
|
5502
|
+
const trigger = this.elementRef.nativeElement.querySelector('.tn-select-trigger');
|
|
5503
|
+
if (!trigger) {
|
|
5504
|
+
return 'below';
|
|
5505
|
+
}
|
|
5506
|
+
const rect = trigger.getBoundingClientRect();
|
|
5507
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
5508
|
+
const spaceAbove = rect.top;
|
|
5509
|
+
if (spaceBelow < this.readDropdownMaxHeight(trigger) && spaceAbove > spaceBelow) {
|
|
5510
|
+
return 'above';
|
|
5511
|
+
}
|
|
5512
|
+
return 'below';
|
|
5513
|
+
}
|
|
5514
|
+
/**
|
|
5515
|
+
* Reads the dropdown's max-height from the CSS custom property set in
|
|
5516
|
+
* select.component.scss. Single source of truth for the flip-up threshold —
|
|
5517
|
+
* if the stylesheet changes, the heuristic follows automatically.
|
|
5518
|
+
*/
|
|
5519
|
+
readDropdownMaxHeight(trigger) {
|
|
5520
|
+
const raw = getComputedStyle(trigger)
|
|
5521
|
+
.getPropertyValue(TnSelectComponent.DROPDOWN_MAX_HEIGHT_VAR)
|
|
5522
|
+
.trim();
|
|
5523
|
+
const parsed = parseFloat(raw);
|
|
5524
|
+
return Number.isFinite(parsed) && parsed > 0
|
|
5525
|
+
? parsed
|
|
5526
|
+
: TnSelectComponent.DROPDOWN_MAX_HEIGHT_FALLBACK;
|
|
5333
5527
|
}
|
|
5334
5528
|
onOptionClick(option, groupDisabled = false) {
|
|
5335
5529
|
if (option.disabled || groupDisabled) {
|
|
@@ -5373,7 +5567,11 @@ class TnSelectComponent {
|
|
|
5373
5567
|
}
|
|
5374
5568
|
return this.compareValues(this.selectedValue(), option.value);
|
|
5375
5569
|
}
|
|
5376
|
-
|
|
5570
|
+
/** Build a stable DOM id for the option at `index` for aria-activedescendant. */
|
|
5571
|
+
optionId(index) {
|
|
5572
|
+
return `tn-select-${this.uniqueId()}-option-${index}`;
|
|
5573
|
+
}
|
|
5574
|
+
displayText = computed(() => {
|
|
5377
5575
|
if (this.multiple()) {
|
|
5378
5576
|
const values = this.selectedValues();
|
|
5379
5577
|
if (values.length === 0) {
|
|
@@ -5390,7 +5588,7 @@ class TnSelectComponent {
|
|
|
5390
5588
|
}
|
|
5391
5589
|
const option = this.findOptionByValue(value);
|
|
5392
5590
|
return option ? option.label : String(value);
|
|
5393
|
-
}, ...(ngDevMode ? [{ debugName: "
|
|
5591
|
+
}, ...(ngDevMode ? [{ debugName: "displayText" }] : []));
|
|
5394
5592
|
findOptionByValue(value) {
|
|
5395
5593
|
// Search in regular options first
|
|
5396
5594
|
const regularOption = this.options().find(opt => this.compareValues(opt.value, value));
|
|
@@ -5406,9 +5604,15 @@ class TnSelectComponent {
|
|
|
5406
5604
|
}
|
|
5407
5605
|
return undefined;
|
|
5408
5606
|
}
|
|
5409
|
-
|
|
5607
|
+
anyOptionsPresent = computed(() => {
|
|
5410
5608
|
return this.options().length > 0 || this.optionGroups().length > 0;
|
|
5411
|
-
}, ...(ngDevMode ? [{ debugName: "
|
|
5609
|
+
}, ...(ngDevMode ? [{ debugName: "anyOptionsPresent" }] : []));
|
|
5610
|
+
/**
|
|
5611
|
+
* Compares two option values for equality. Uses `compareWith` if provided,
|
|
5612
|
+
* otherwise identity (`===`). For object values it falls back to
|
|
5613
|
+
* `JSON.stringify`, which is key-order dependent — consumers with object
|
|
5614
|
+
* values should provide `compareWith` to avoid subtle bugs.
|
|
5615
|
+
*/
|
|
5412
5616
|
compareValues(a, b) {
|
|
5413
5617
|
const customCompare = this.compareWith();
|
|
5414
5618
|
if (customCompare) {
|
|
@@ -5422,17 +5626,31 @@ class TnSelectComponent {
|
|
|
5422
5626
|
}
|
|
5423
5627
|
return false;
|
|
5424
5628
|
}
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5629
|
+
/**
|
|
5630
|
+
* Keyboard handling on the trigger (focus stays on the trigger while the
|
|
5631
|
+
* dropdown is open — options use mousedown-preventDefault to avoid stealing
|
|
5632
|
+
* it). Implements the WAI-ARIA combobox pattern subset we need:
|
|
5633
|
+
*
|
|
5634
|
+
* - **Enter / Space**: open closed dropdown, or select the focused row
|
|
5635
|
+
* (toggle in multi-mode).
|
|
5636
|
+
* - **ArrowDown / ArrowUp**: move the focused row; opens the dropdown first
|
|
5637
|
+
* if it's closed.
|
|
5638
|
+
* - **Home / End**: jump to first / last focusable row (when open).
|
|
5639
|
+
* - **Escape**: close and restore focus to the trigger.
|
|
5640
|
+
* - **Tab**: close without preventing default so focus moves to the next
|
|
5641
|
+
* element naturally.
|
|
5642
|
+
*/
|
|
5428
5643
|
onKeydown(event) {
|
|
5429
5644
|
switch (event.key) {
|
|
5430
5645
|
case 'Enter':
|
|
5431
5646
|
case ' ':
|
|
5432
|
-
if (
|
|
5433
|
-
this.
|
|
5434
|
-
|
|
5647
|
+
if (this.isOpen()) {
|
|
5648
|
+
this.selectFocused();
|
|
5649
|
+
}
|
|
5650
|
+
else {
|
|
5651
|
+
this.openDropdown();
|
|
5435
5652
|
}
|
|
5653
|
+
event.preventDefault();
|
|
5436
5654
|
break;
|
|
5437
5655
|
case 'Escape':
|
|
5438
5656
|
if (this.isOpen()) {
|
|
@@ -5442,20 +5660,114 @@ class TnSelectComponent {
|
|
|
5442
5660
|
break;
|
|
5443
5661
|
case 'ArrowDown':
|
|
5444
5662
|
if (!this.isOpen()) {
|
|
5445
|
-
this.
|
|
5663
|
+
this.openDropdown();
|
|
5664
|
+
}
|
|
5665
|
+
else {
|
|
5666
|
+
this.moveFocus(1);
|
|
5446
5667
|
}
|
|
5447
5668
|
event.preventDefault();
|
|
5448
5669
|
break;
|
|
5670
|
+
case 'ArrowUp':
|
|
5671
|
+
if (!this.isOpen()) {
|
|
5672
|
+
this.openDropdown();
|
|
5673
|
+
}
|
|
5674
|
+
else {
|
|
5675
|
+
this.moveFocus(-1);
|
|
5676
|
+
}
|
|
5677
|
+
event.preventDefault();
|
|
5678
|
+
break;
|
|
5679
|
+
case 'Home':
|
|
5680
|
+
if (this.isOpen()) {
|
|
5681
|
+
this.moveFocusTo(0, 1);
|
|
5682
|
+
event.preventDefault();
|
|
5683
|
+
}
|
|
5684
|
+
break;
|
|
5685
|
+
case 'End':
|
|
5686
|
+
if (this.isOpen()) {
|
|
5687
|
+
this.moveFocusTo(this.flatOptions().length - 1, -1);
|
|
5688
|
+
event.preventDefault();
|
|
5689
|
+
}
|
|
5690
|
+
break;
|
|
5691
|
+
case 'Tab':
|
|
5692
|
+
// Let the browser move focus to the next element; just close.
|
|
5693
|
+
if (this.isOpen()) {
|
|
5694
|
+
this.closeDropdown({ restoreFocus: false });
|
|
5695
|
+
}
|
|
5696
|
+
break;
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
/** Step the focused row by ±1 (or more), skipping disabled options. */
|
|
5700
|
+
moveFocus(delta) {
|
|
5701
|
+
const flat = this.flatOptions();
|
|
5702
|
+
if (flat.length === 0) {
|
|
5703
|
+
return;
|
|
5704
|
+
}
|
|
5705
|
+
let idx = this.focusedIndex();
|
|
5706
|
+
for (let i = 0; i < flat.length; i++) {
|
|
5707
|
+
idx = (idx + delta + flat.length) % flat.length;
|
|
5708
|
+
if (!flat[idx].disabled) {
|
|
5709
|
+
this.focusedIndex.set(idx);
|
|
5710
|
+
this.scrollFocusedIntoView();
|
|
5711
|
+
return;
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
}
|
|
5715
|
+
/** Move focus to a specific index, scanning forward/backward to skip disabled. */
|
|
5716
|
+
moveFocusTo(start, step) {
|
|
5717
|
+
const flat = this.flatOptions();
|
|
5718
|
+
if (flat.length === 0) {
|
|
5719
|
+
return;
|
|
5720
|
+
}
|
|
5721
|
+
let idx = start;
|
|
5722
|
+
while (idx >= 0 && idx < flat.length) {
|
|
5723
|
+
if (!flat[idx].disabled) {
|
|
5724
|
+
this.focusedIndex.set(idx);
|
|
5725
|
+
this.scrollFocusedIntoView();
|
|
5726
|
+
return;
|
|
5727
|
+
}
|
|
5728
|
+
idx += step;
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
/** Select (or toggle, in multi-mode) the currently keyboard-focused row. */
|
|
5732
|
+
selectFocused() {
|
|
5733
|
+
const idx = this.focusedIndex();
|
|
5734
|
+
const flat = this.flatOptions();
|
|
5735
|
+
if (idx < 0 || idx >= flat.length) {
|
|
5736
|
+
return;
|
|
5737
|
+
}
|
|
5738
|
+
const opt = flat[idx];
|
|
5739
|
+
if (opt.disabled) {
|
|
5740
|
+
return;
|
|
5741
|
+
}
|
|
5742
|
+
if (this.multiple()) {
|
|
5743
|
+
this.toggleOption(opt);
|
|
5744
|
+
}
|
|
5745
|
+
else {
|
|
5746
|
+
this.selectOption(opt);
|
|
5449
5747
|
}
|
|
5450
5748
|
}
|
|
5749
|
+
/** Scrolls the keyboard-focused option into view if it's outside the dropdown's viewport. */
|
|
5750
|
+
scrollFocusedIntoView() {
|
|
5751
|
+
const idx = this.focusedIndex();
|
|
5752
|
+
if (idx < 0) {
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
const host = this.elementRef.nativeElement;
|
|
5756
|
+
// Use attribute-equality instead of #id selectors so we don't need
|
|
5757
|
+
// CSS.escape (unavailable in jsdom for tests) — option ids may contain
|
|
5758
|
+
// characters that require escaping in #id form.
|
|
5759
|
+
const el = host.querySelector(`[id="${this.optionId(idx)}"]`);
|
|
5760
|
+
// jsdom doesn't implement scrollIntoView — guard so tests don't crash.
|
|
5761
|
+
el?.scrollIntoView?.({ block: 'nearest' });
|
|
5762
|
+
}
|
|
5451
5763
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5452
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSelectComponent, isStandalone: true, selector: "tn-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionGroups: { classPropertyName: "optionGroups", publicName: "optionGroups", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", multiSelectionChange: "multiSelectionChange" }, providers: [
|
|
5764
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSelectComponent, isStandalone: true, selector: "tn-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionGroups: { classPropertyName: "optionGroups", publicName: "optionGroups", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, noOptionsLabel: { classPropertyName: "noOptionsLabel", publicName: "noOptionsLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", multiSelectionChange: "multiSelectionChange" }, providers: [
|
|
5453
5765
|
{
|
|
5454
5766
|
provide: NG_VALUE_ACCESSOR,
|
|
5455
5767
|
useExisting: forwardRef(() => TnSelectComponent),
|
|
5456
5768
|
multi: true
|
|
5457
5769
|
}
|
|
5458
|
-
], ngImport: i0, template: "<div class=\"tn-select-container\" [tnTestId]=\"testId()\">\n <!-- Select Trigger -->\n <div\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"
|
|
5770
|
+
], viewQueries: [{ propertyName: "triggerEl", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-select-container\" [tnTestId]=\"testId()\">\n <!-- Select Trigger -->\n <div\n #trigger\n class=\"tn-select-trigger\"\n role=\"combobox\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + uniqueId() : null\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [class.tn-select-dropdown--above]=\"dropdownPosition() === 'above'\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + uniqueId()\">\n\n <!-- Options List -->\n <!-- Options follow the WAI-ARIA combobox pattern: focus stays on the\n trigger, navigation is via the trigger's keydown handler and\n aria-activedescendant. Options handle click only, by design \u2014 they\n must not steal focus on mousedown. -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled\"\n [class.focused]=\"focusedIndex() === i\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let groupIdx = $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || options().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + uniqueId() + '-' + groupIdx\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + uniqueId() + '-' + groupIdx\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index; let optIdx = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [id]=\"optionId(groupOffsets()[groupIdx] + optIdx)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [class.focused]=\"focusedIndex() === groupOffsets()[groupIdx] + optIdx\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!anyOptionsPresent()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;--tn-select-dropdown-max-height: 200px}.tn-select-label{display:block;margin-bottom:.5rem;font-size:.875rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-select-dropdown-max-height);overflow:hidden}.tn-select-dropdown--above{top:auto;bottom:100%;margin-top:0;margin-bottom:.25rem}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg1, #f1f3f5);outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"], dependencies: [{ kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5459
5771
|
}
|
|
5460
5772
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, decorators: [{
|
|
5461
5773
|
type: Component,
|
|
@@ -5465,8 +5777,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
5465
5777
|
useExisting: forwardRef(() => TnSelectComponent),
|
|
5466
5778
|
multi: true
|
|
5467
5779
|
}
|
|
5468
|
-
], template: "<div class=\"tn-select-container\" [tnTestId]=\"testId()\">\n <!-- Select Trigger -->\n <div\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"
|
|
5469
|
-
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroups", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], multiSelectionChange: [{ type: i0.Output, args: ["multiSelectionChange"] }] } });
|
|
5780
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-select-container\" [tnTestId]=\"testId()\">\n <!-- Select Trigger -->\n <div\n #trigger\n class=\"tn-select-trigger\"\n role=\"combobox\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + uniqueId() : null\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [class.tn-select-dropdown--above]=\"dropdownPosition() === 'above'\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + uniqueId()\">\n\n <!-- Options List -->\n <!-- Options follow the WAI-ARIA combobox pattern: focus stays on the\n trigger, navigation is via the trigger's keydown handler and\n aria-activedescendant. Options handle click only, by design \u2014 they\n must not steal focus on mousedown. -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled\"\n [class.focused]=\"focusedIndex() === i\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let groupIdx = $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || options().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + uniqueId() + '-' + groupIdx\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + uniqueId() + '-' + groupIdx\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index; let optIdx = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [id]=\"optionId(groupOffsets()[groupIdx] + optIdx)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [class.focused]=\"focusedIndex() === groupOffsets()[groupIdx] + optIdx\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!anyOptionsPresent()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;--tn-select-dropdown-max-height: 200px}.tn-select-label{display:block;margin-bottom:.5rem;font-size:.875rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-select-dropdown-max-height);overflow:hidden}.tn-select-dropdown--above{top:auto;bottom:100%;margin-top:0;margin-bottom:.25rem}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg1, #f1f3f5);outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"] }]
|
|
5781
|
+
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroups", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], noOptionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "noOptionsLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], multiSelectionChange: [{ type: i0.Output, args: ["multiSelectionChange"] }], triggerEl: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
|
|
5470
5782
|
|
|
5471
5783
|
/**
|
|
5472
5784
|
* Harness for interacting with tn-select in tests.
|
|
@@ -6942,6 +7254,352 @@ class TnTableHarness extends ComponentHarness {
|
|
|
6942
7254
|
}
|
|
6943
7255
|
}
|
|
6944
7256
|
|
|
7257
|
+
/** English defaults used when no `TN_TABLE_PAGER_LABELS` provider is registered. */
|
|
7258
|
+
const TN_TABLE_PAGER_DEFAULT_LABELS = {
|
|
7259
|
+
itemsPerPage: 'Items per page',
|
|
7260
|
+
of: 'of',
|
|
7261
|
+
firstPage: 'First page',
|
|
7262
|
+
previousPage: 'Previous page',
|
|
7263
|
+
nextPage: 'Next page',
|
|
7264
|
+
lastPage: 'Last page',
|
|
7265
|
+
tablePagination: 'Table pagination',
|
|
7266
|
+
};
|
|
7267
|
+
/**
|
|
7268
|
+
* DI token for app-wide default labels. Provide either a static object or a
|
|
7269
|
+
* `Signal<TnTablePagerLabels>` — the latter lets the pager react to language
|
|
7270
|
+
* changes when the consumer wires it up to an i18n service.
|
|
7271
|
+
*
|
|
7272
|
+
* Explicit input bindings on `<tn-table-pager>` still win over these defaults.
|
|
7273
|
+
*/
|
|
7274
|
+
const TN_TABLE_PAGER_LABELS = new InjectionToken('TN_TABLE_PAGER_LABELS', { providedIn: 'root', factory: () => TN_TABLE_PAGER_DEFAULT_LABELS });
|
|
7275
|
+
/**
|
|
7276
|
+
* Pagination control for the `tn-table` (or any list view that paginates).
|
|
7277
|
+
*
|
|
7278
|
+
* Works in two modes:
|
|
7279
|
+
*
|
|
7280
|
+
* 1. **Dumb mode** — bind `[currentPage]`, `[pageSize]`, `[totalItems]` and listen
|
|
7281
|
+
* to `pageChange` / `pageSizeChange`. The component owns no provider state.
|
|
7282
|
+
* 2. **Data-provider mode** — bind `[dataProvider]` and the component drives
|
|
7283
|
+
* `setPagination()` on the provider, mirrors `totalRows`, and reacts to
|
|
7284
|
+
* `currentPage$` changes (with an internal guard against feedback loops).
|
|
7285
|
+
*
|
|
7286
|
+
* @example Dumb mode
|
|
7287
|
+
* ```html
|
|
7288
|
+
* <tn-table-pager
|
|
7289
|
+
* [(currentPage)]="page"
|
|
7290
|
+
* [(pageSize)]="size"
|
|
7291
|
+
* [totalItems]="total()"
|
|
7292
|
+
* (pageChange)="loadPage($event)" />
|
|
7293
|
+
* ```
|
|
7294
|
+
*
|
|
7295
|
+
* @example Data-provider mode (replaces the typical ix-table-pager wrapper)
|
|
7296
|
+
* ```html
|
|
7297
|
+
* <tn-table-pager
|
|
7298
|
+
* [dataProvider]="dataProvider()"
|
|
7299
|
+
* [pageSize]="dataProvider().pagination.pageSize ?? 50"
|
|
7300
|
+
* [currentPage]="dataProvider().pagination.pageNumber ?? 1"
|
|
7301
|
+
* [itemsPerPageLabel]="'Items per page' | translate" />
|
|
7302
|
+
* ```
|
|
7303
|
+
*/
|
|
7304
|
+
class TnTablePagerComponent {
|
|
7305
|
+
destroyRef = inject(DestroyRef);
|
|
7306
|
+
/**
|
|
7307
|
+
* Normalize the injected token into a Signal so consumers can supply either
|
|
7308
|
+
* a plain object or a reactive signal (e.g. derived from a TranslateService's
|
|
7309
|
+
* onLangChange) and the pager re-renders when labels change.
|
|
7310
|
+
*/
|
|
7311
|
+
defaultLabels;
|
|
7312
|
+
/** 1-based index of the currently displayed page. */
|
|
7313
|
+
currentPage = model(1, ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
|
|
7314
|
+
/** Number of items per page. */
|
|
7315
|
+
pageSize = model(50, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
|
|
7316
|
+
/** Selectable page-size values rendered in the dropdown. */
|
|
7317
|
+
pageSizeOptions = input([10, 20, 50, 100], ...(ngDevMode ? [{ debugName: "pageSizeOptions" }] : []));
|
|
7318
|
+
/**
|
|
7319
|
+
* Total item count across all pages — drives `totalPages` and the range
|
|
7320
|
+
* labels. Ignored when `dataProvider` is set (the provider's `totalRows`
|
|
7321
|
+
* wins, so consumers don't have to keep both in sync).
|
|
7322
|
+
*/
|
|
7323
|
+
totalItems = input(0, ...(ngDevMode ? [{ debugName: "totalItems" }] : []));
|
|
7324
|
+
/**
|
|
7325
|
+
* Optional data-provider integration. When supplied, the pager initializes
|
|
7326
|
+
* the provider's pagination on the first effect run, mirrors `totalRows`
|
|
7327
|
+
* into the displayed total, and listens to `currentPage$` to sync external
|
|
7328
|
+
* page changes. Page/size changes from user input are pushed back via
|
|
7329
|
+
* `setPagination()`.
|
|
7330
|
+
*/
|
|
7331
|
+
dataProvider = input(undefined, ...(ngDevMode ? [{ debugName: "dataProvider" }] : []));
|
|
7332
|
+
/**
|
|
7333
|
+
* Label inputs are nullable on purpose: the template reads the resolved
|
|
7334
|
+
* `*Label` computed signals below, which fall back to the DI-provided default
|
|
7335
|
+
* (a signal — so language changes propagate live). An explicit input binding
|
|
7336
|
+
* always wins.
|
|
7337
|
+
*/
|
|
7338
|
+
itemsPerPageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "itemsPerPageLabel" }] : []));
|
|
7339
|
+
ofLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ofLabel" }] : []));
|
|
7340
|
+
firstPageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "firstPageLabel" }] : []));
|
|
7341
|
+
previousPageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "previousPageLabel" }] : []));
|
|
7342
|
+
nextPageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "nextPageLabel" }] : []));
|
|
7343
|
+
lastPageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "lastPageLabel" }] : []));
|
|
7344
|
+
tablePaginationLabel = input(undefined, ...(ngDevMode ? [{ debugName: "tablePaginationLabel" }] : []));
|
|
7345
|
+
/** Resolved labels: explicit input takes precedence over the DI default. */
|
|
7346
|
+
resolvedItemsPerPageLabel = computed(() => this.itemsPerPageLabel() ?? this.defaultLabels().itemsPerPage, ...(ngDevMode ? [{ debugName: "resolvedItemsPerPageLabel" }] : []));
|
|
7347
|
+
resolvedOfLabel = computed(() => this.ofLabel() ?? this.defaultLabels().of, ...(ngDevMode ? [{ debugName: "resolvedOfLabel" }] : []));
|
|
7348
|
+
resolvedFirstPageLabel = computed(() => this.firstPageLabel() ?? this.defaultLabels().firstPage, ...(ngDevMode ? [{ debugName: "resolvedFirstPageLabel" }] : []));
|
|
7349
|
+
resolvedPreviousPageLabel = computed(() => this.previousPageLabel() ?? this.defaultLabels().previousPage, ...(ngDevMode ? [{ debugName: "resolvedPreviousPageLabel" }] : []));
|
|
7350
|
+
resolvedNextPageLabel = computed(() => this.nextPageLabel() ?? this.defaultLabels().nextPage, ...(ngDevMode ? [{ debugName: "resolvedNextPageLabel" }] : []));
|
|
7351
|
+
resolvedLastPageLabel = computed(() => this.lastPageLabel() ?? this.defaultLabels().lastPage, ...(ngDevMode ? [{ debugName: "resolvedLastPageLabel" }] : []));
|
|
7352
|
+
resolvedTablePaginationLabel = computed(() => this.tablePaginationLabel() ?? this.defaultLabels().tablePagination, ...(ngDevMode ? [{ debugName: "resolvedTablePaginationLabel" }] : []));
|
|
7353
|
+
/** Emits the new 1-based page number whenever the user navigates. */
|
|
7354
|
+
pageChange = output();
|
|
7355
|
+
/** Emits the new page-size value when the dropdown changes. */
|
|
7356
|
+
pageSizeChange = output();
|
|
7357
|
+
/**
|
|
7358
|
+
* Total items reported by the data provider (when one is bound). Falls back
|
|
7359
|
+
* to `totalItems` input otherwise — see `effectiveTotalItems`.
|
|
7360
|
+
*/
|
|
7361
|
+
providerTotalItems = signal(0, ...(ngDevMode ? [{ debugName: "providerTotalItems" }] : []));
|
|
7362
|
+
// The fields below are intentionally plain mutable state, not signals: they
|
|
7363
|
+
// back the provider binding's imperative lifecycle (current ref, current
|
|
7364
|
+
// subscription, last-pushed echo guard) and are only ever written from
|
|
7365
|
+
// inside the bind effect or the syncFromProvider/pushToProvider helpers.
|
|
7366
|
+
// Reading them never needs to participate in the reactive graph — exposing
|
|
7367
|
+
// them as signals would invite spurious re-evaluation without buying us
|
|
7368
|
+
// anything.
|
|
7369
|
+
/** The provider reference we're currently bound to (used to detect swaps). */
|
|
7370
|
+
currentProvider = null;
|
|
7371
|
+
/** Subscription to the current provider's `currentPage$` — torn down on swap. */
|
|
7372
|
+
providerSub = null;
|
|
7373
|
+
/**
|
|
7374
|
+
* Last pagination value we pushed to the provider. Used to recognize the
|
|
7375
|
+
* provider's resulting emission as our own echo and break the feedback loop
|
|
7376
|
+
* (setPagination → provider emits → syncFromProvider → setPagination …)
|
|
7377
|
+
* regardless of whether the provider emits synchronously or asynchronously,
|
|
7378
|
+
* and regardless of whether its stream replays on subscribe.
|
|
7379
|
+
*/
|
|
7380
|
+
lastPushedPagination = null;
|
|
7381
|
+
effectiveTotalItems = computed(() => this.dataProvider() ? this.providerTotalItems() : this.totalItems(), ...(ngDevMode ? [{ debugName: "effectiveTotalItems" }] : []));
|
|
7382
|
+
totalPages = computed(() => {
|
|
7383
|
+
const size = this.pageSize();
|
|
7384
|
+
if (size <= 0) {
|
|
7385
|
+
return 0;
|
|
7386
|
+
}
|
|
7387
|
+
return Math.ceil(this.effectiveTotalItems() / size);
|
|
7388
|
+
}, ...(ngDevMode ? [{ debugName: "totalPages" }] : []));
|
|
7389
|
+
firstItemOnPage = computed(() => {
|
|
7390
|
+
if (this.effectiveTotalItems() === 0) {
|
|
7391
|
+
return 0;
|
|
7392
|
+
}
|
|
7393
|
+
return (this.currentPage() - 1) * this.pageSize() + 1;
|
|
7394
|
+
}, ...(ngDevMode ? [{ debugName: "firstItemOnPage" }] : []));
|
|
7395
|
+
lastItemOnPage = computed(() => {
|
|
7396
|
+
const last = this.currentPage() * this.pageSize();
|
|
7397
|
+
return Math.min(last, this.effectiveTotalItems());
|
|
7398
|
+
}, ...(ngDevMode ? [{ debugName: "lastItemOnPage" }] : []));
|
|
7399
|
+
isFirstPageDisabled = computed(() => this.currentPage() <= 1, ...(ngDevMode ? [{ debugName: "isFirstPageDisabled" }] : []));
|
|
7400
|
+
isLastPageDisabled = computed(() => {
|
|
7401
|
+
const total = this.totalPages();
|
|
7402
|
+
return total === 0 || this.currentPage() >= total;
|
|
7403
|
+
}, ...(ngDevMode ? [{ debugName: "isLastPageDisabled" }] : []));
|
|
7404
|
+
pageSizeSelectOptions = computed(() => this.pageSizeOptions().map((value) => ({ value, label: String(value) })), ...(ngDevMode ? [{ debugName: "pageSizeSelectOptions" }] : []));
|
|
7405
|
+
constructor() {
|
|
7406
|
+
const provided = inject(TN_TABLE_PAGER_LABELS);
|
|
7407
|
+
this.defaultLabels = isSignal(provided) ? provided : signal(provided).asReadonly();
|
|
7408
|
+
// Re-bind when the dataProvider reference changes (including swap to a
|
|
7409
|
+
// different instance or clearing back to undefined). `untracked` keeps the
|
|
7410
|
+
// provider's imperative reads out of the reactive graph so this effect only
|
|
7411
|
+
// re-runs when the provider reference itself changes.
|
|
7412
|
+
this.destroyRef.onDestroy(() => this.providerSub?.unsubscribe());
|
|
7413
|
+
effect(() => {
|
|
7414
|
+
const provider = this.dataProvider() ?? null;
|
|
7415
|
+
if (provider === this.currentProvider) {
|
|
7416
|
+
return;
|
|
7417
|
+
}
|
|
7418
|
+
// Tear down any previous binding before attaching to the new provider so
|
|
7419
|
+
// a swap doesn't leave the old subscription running against destroyRef.
|
|
7420
|
+
this.providerSub?.unsubscribe();
|
|
7421
|
+
this.providerSub = null;
|
|
7422
|
+
this.lastPushedPagination = null;
|
|
7423
|
+
this.currentProvider = provider;
|
|
7424
|
+
if (!provider) {
|
|
7425
|
+
this.providerTotalItems.set(0);
|
|
7426
|
+
return;
|
|
7427
|
+
}
|
|
7428
|
+
untracked(() => {
|
|
7429
|
+
this.pushToProvider();
|
|
7430
|
+
this.providerTotalItems.set(provider.totalRows);
|
|
7431
|
+
});
|
|
7432
|
+
// No skip() here: if currentPage$ replays (BehaviorSubject), the replay
|
|
7433
|
+
// value matches what pushToProvider() just recorded in
|
|
7434
|
+
// lastPushedPagination, so syncFromProvider treats it as our own echo
|
|
7435
|
+
// and short-circuits. A plain Subject (no replay) still gets every real
|
|
7436
|
+
// emission — earlier we used skip(1), which silently dropped the first
|
|
7437
|
+
// real emission for non-replaying streams.
|
|
7438
|
+
this.providerSub = provider.currentPage$.subscribe(() => this.syncFromProvider());
|
|
7439
|
+
});
|
|
7440
|
+
}
|
|
7441
|
+
syncFromProvider() {
|
|
7442
|
+
const provider = this.dataProvider();
|
|
7443
|
+
if (!provider) {
|
|
7444
|
+
return;
|
|
7445
|
+
}
|
|
7446
|
+
this.providerTotalItems.set(provider.totalRows);
|
|
7447
|
+
const p = provider.pagination;
|
|
7448
|
+
const isEcho = this.lastPushedPagination !== null
|
|
7449
|
+
&& p.pageNumber === this.lastPushedPagination.pageNumber
|
|
7450
|
+
&& p.pageSize === this.lastPushedPagination.pageSize;
|
|
7451
|
+
if (!isEcho) {
|
|
7452
|
+
const providerPage = p.pageNumber;
|
|
7453
|
+
if (providerPage !== null && providerPage !== this.currentPage()) {
|
|
7454
|
+
this.currentPage.set(providerPage);
|
|
7455
|
+
return;
|
|
7456
|
+
}
|
|
7457
|
+
}
|
|
7458
|
+
// Whether or not this was an echo, an updated totalRows can leave us on an
|
|
7459
|
+
// out-of-range page — reset, emit pageChange (so consumers see the auto-
|
|
7460
|
+
// correction symmetrically with user-initiated navigation), and push the
|
|
7461
|
+
// corrected pagination back.
|
|
7462
|
+
if (this.currentPage() > this.totalPages() && this.currentPage() !== 1) {
|
|
7463
|
+
this.currentPage.set(1);
|
|
7464
|
+
this.pageChange.emit(1);
|
|
7465
|
+
this.pushToProvider();
|
|
7466
|
+
}
|
|
7467
|
+
}
|
|
7468
|
+
/**
|
|
7469
|
+
* Public navigation API: jump to a specific 1-based page. Used both by the
|
|
7470
|
+
* template (first/last buttons) and by consumers who want to drive the pager
|
|
7471
|
+
* programmatically. Out-of-range values and no-op transitions are silently
|
|
7472
|
+
* ignored. Sibling helpers `previousPage` / `nextPage` are template-only and
|
|
7473
|
+
* therefore `protected` — `goToPage` is intentionally part of the public API.
|
|
7474
|
+
*/
|
|
7475
|
+
goToPage(pageNumber) {
|
|
7476
|
+
const total = this.totalPages();
|
|
7477
|
+
if (pageNumber < 1 || (total > 0 && pageNumber > total)) {
|
|
7478
|
+
return;
|
|
7479
|
+
}
|
|
7480
|
+
if (pageNumber === this.currentPage()) {
|
|
7481
|
+
return;
|
|
7482
|
+
}
|
|
7483
|
+
this.currentPage.set(pageNumber);
|
|
7484
|
+
this.pageChange.emit(pageNumber);
|
|
7485
|
+
this.pushToProvider();
|
|
7486
|
+
}
|
|
7487
|
+
previousPage() {
|
|
7488
|
+
this.goToPage(this.currentPage() - 1);
|
|
7489
|
+
}
|
|
7490
|
+
nextPage() {
|
|
7491
|
+
this.goToPage(this.currentPage() + 1);
|
|
7492
|
+
}
|
|
7493
|
+
onPageSizeChange(value) {
|
|
7494
|
+
if (value === this.pageSize()) {
|
|
7495
|
+
return;
|
|
7496
|
+
}
|
|
7497
|
+
this.pageSize.set(value);
|
|
7498
|
+
this.pageSizeChange.emit(value);
|
|
7499
|
+
// Resetting to page 1 is a UX expectation when the page size changes —
|
|
7500
|
+
// otherwise the user might land on an out-of-range page.
|
|
7501
|
+
this.currentPage.set(1);
|
|
7502
|
+
this.pageChange.emit(1);
|
|
7503
|
+
this.pushToProvider();
|
|
7504
|
+
}
|
|
7505
|
+
/** Forwards the current page/size to the data provider, if one is bound. */
|
|
7506
|
+
pushToProvider() {
|
|
7507
|
+
const provider = this.dataProvider();
|
|
7508
|
+
if (!provider) {
|
|
7509
|
+
return;
|
|
7510
|
+
}
|
|
7511
|
+
// Recording the value before pushing lets syncFromProvider recognize the
|
|
7512
|
+
// resulting emission as our own echo (independent of whether the provider
|
|
7513
|
+
// emits synchronously from setPagination).
|
|
7514
|
+
const pagination = {
|
|
7515
|
+
pageNumber: this.currentPage(),
|
|
7516
|
+
pageSize: this.pageSize(),
|
|
7517
|
+
};
|
|
7518
|
+
this.lastPushedPagination = pagination;
|
|
7519
|
+
provider.setPagination(pagination);
|
|
7520
|
+
}
|
|
7521
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTablePagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7522
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTablePagerComponent, isStandalone: true, selector: "tn-table-pager", inputs: { currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: false, transformFunction: null }, dataProvider: { classPropertyName: "dataProvider", publicName: "dataProvider", isSignal: true, isRequired: false, transformFunction: null }, itemsPerPageLabel: { classPropertyName: "itemsPerPageLabel", publicName: "itemsPerPageLabel", isSignal: true, isRequired: false, transformFunction: null }, ofLabel: { classPropertyName: "ofLabel", publicName: "ofLabel", isSignal: true, isRequired: false, transformFunction: null }, firstPageLabel: { classPropertyName: "firstPageLabel", publicName: "firstPageLabel", isSignal: true, isRequired: false, transformFunction: null }, previousPageLabel: { classPropertyName: "previousPageLabel", publicName: "previousPageLabel", isSignal: true, isRequired: false, transformFunction: null }, nextPageLabel: { classPropertyName: "nextPageLabel", publicName: "nextPageLabel", isSignal: true, isRequired: false, transformFunction: null }, lastPageLabel: { classPropertyName: "lastPageLabel", publicName: "lastPageLabel", isSignal: true, isRequired: false, transformFunction: null }, tablePaginationLabel: { classPropertyName: "tablePaginationLabel", publicName: "tablePaginationLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", pageSize: "pageSizeChange", pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, host: { attributes: { "role": "navigation" }, properties: { "attr.aria-label": "resolvedTablePaginationLabel()" }, classAttribute: "tn-table-pager" }, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-table-pager__page-size\">\n <span class=\"tn-table-pager__label\">{{ resolvedItemsPerPageLabel() }}:</span>\n <tn-select\n class=\"tn-table-pager__size-select\"\n [options]=\"pageSizeSelectOptions()\"\n [ariaLabel]=\"resolvedItemsPerPageLabel()\"\n [ngModel]=\"pageSize()\"\n (ngModelChange)=\"onPageSizeChange($event)\" />\n</div>\n\n<span class=\"tn-table-pager__range\">\n @if (effectiveTotalItems() === 0) {\n \u2013 {{ resolvedOfLabel() }} 0\n } @else if (lastItemOnPage() > firstItemOnPage()) {\n {{ firstItemOnPage() }} \u2013 {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n } @else {\n {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n }\n</span>\n\n<div class=\"tn-table-pager__buttons\">\n <tn-icon-button\n name=\"page-first\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedFirstPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"goToPage(1)\" />\n <tn-icon-button\n name=\"chevron-left\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedPreviousPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"previousPage()\" />\n <tn-icon-button\n name=\"chevron-right\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedNextPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"nextPage()\" />\n <tn-icon-button\n name=\"page-last\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedLastPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"goToPage(totalPages())\" />\n</div>\n", styles: [":host{display:flex;align-items:center;justify-content:flex-end;gap:16px;padding:10px;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);color:var(--tn-fg2)}.tn-table-pager__page-size{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.tn-table-pager__label{white-space:nowrap}.tn-table-pager__size-select{min-width:84px}.tn-table-pager__range{white-space:nowrap}.tn-table-pager__buttons{display:flex;align-items:center;gap:4px}@media(max-width:600px){:host{gap:8px}.tn-table-pager__page-size{gap:4px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "ariaLabel", "testId", "name", "size", "color", "tooltip", "library"], outputs: ["onClick"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "ariaLabel", "noOptionsLabel", "disabled", "testId", "multiple", "compareWith"], outputs: ["selectionChange", "multiSelectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7523
|
+
}
|
|
7524
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTablePagerComponent, decorators: [{
|
|
7525
|
+
type: Component,
|
|
7526
|
+
args: [{ selector: 'tn-table-pager', standalone: true, imports: [FormsModule, TnIconButtonComponent, TnSelectComponent, TnTestIdDirective], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
7527
|
+
'class': 'tn-table-pager',
|
|
7528
|
+
'role': 'navigation',
|
|
7529
|
+
'[attr.aria-label]': 'resolvedTablePaginationLabel()',
|
|
7530
|
+
}, hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], template: "<div class=\"tn-table-pager__page-size\">\n <span class=\"tn-table-pager__label\">{{ resolvedItemsPerPageLabel() }}:</span>\n <tn-select\n class=\"tn-table-pager__size-select\"\n [options]=\"pageSizeSelectOptions()\"\n [ariaLabel]=\"resolvedItemsPerPageLabel()\"\n [ngModel]=\"pageSize()\"\n (ngModelChange)=\"onPageSizeChange($event)\" />\n</div>\n\n<span class=\"tn-table-pager__range\">\n @if (effectiveTotalItems() === 0) {\n \u2013 {{ resolvedOfLabel() }} 0\n } @else if (lastItemOnPage() > firstItemOnPage()) {\n {{ firstItemOnPage() }} \u2013 {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n } @else {\n {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n }\n</span>\n\n<div class=\"tn-table-pager__buttons\">\n <tn-icon-button\n name=\"page-first\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedFirstPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"goToPage(1)\" />\n <tn-icon-button\n name=\"chevron-left\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedPreviousPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"previousPage()\" />\n <tn-icon-button\n name=\"chevron-right\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedNextPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"nextPage()\" />\n <tn-icon-button\n name=\"page-last\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedLastPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"goToPage(totalPages())\" />\n</div>\n", styles: [":host{display:flex;align-items:center;justify-content:flex-end;gap:16px;padding:10px;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);color:var(--tn-fg2)}.tn-table-pager__page-size{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.tn-table-pager__label{white-space:nowrap}.tn-table-pager__size-select{min-width:84px}.tn-table-pager__range{white-space:nowrap}.tn-table-pager__buttons{display:flex;align-items:center;gap:4px}@media(max-width:600px){:host{gap:8px}.tn-table-pager__page-size{gap:4px}}\n"] }]
|
|
7531
|
+
}], ctorParameters: () => [], propDecorators: { currentPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPage", required: false }] }, { type: i0.Output, args: ["currentPageChange"] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }, { type: i0.Output, args: ["pageSizeChange"] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: false }] }], dataProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataProvider", required: false }] }], itemsPerPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsPerPageLabel", required: false }] }], ofLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ofLabel", required: false }] }], firstPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstPageLabel", required: false }] }], previousPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousPageLabel", required: false }] }], nextPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextPageLabel", required: false }] }], lastPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastPageLabel", required: false }] }], tablePaginationLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tablePaginationLabel", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], pageSizeChange: [{ type: i0.Output, args: ["pageSizeChange"] }] } });
|
|
7532
|
+
|
|
7533
|
+
/**
|
|
7534
|
+
* Harness for interacting with `tn-table-pager` in tests.
|
|
7535
|
+
*
|
|
7536
|
+
* @example
|
|
7537
|
+
* ```ts
|
|
7538
|
+
* const pager = await loader.getHarness(TnTablePagerHarness);
|
|
7539
|
+
* await pager.nextPage();
|
|
7540
|
+
* expect(await pager.getRangeText()).toBe('21 – 40 of 47');
|
|
7541
|
+
* ```
|
|
7542
|
+
*/
|
|
7543
|
+
class TnTablePagerHarness extends ComponentHarness {
|
|
7544
|
+
static hostSelector = 'tn-table-pager';
|
|
7545
|
+
static with(options = {}) {
|
|
7546
|
+
return new HarnessPredicate(TnTablePagerHarness, options);
|
|
7547
|
+
}
|
|
7548
|
+
firstButton = this.locatorFor(TnIconButtonHarness.with({ name: 'page-first' }));
|
|
7549
|
+
previousButton = this.locatorFor(TnIconButtonHarness.with({ name: 'chevron-left' }));
|
|
7550
|
+
nextButton = this.locatorFor(TnIconButtonHarness.with({ name: 'chevron-right' }));
|
|
7551
|
+
lastButton = this.locatorFor(TnIconButtonHarness.with({ name: 'page-last' }));
|
|
7552
|
+
pageSizeSelect = this.locatorFor(TnSelectHarness);
|
|
7553
|
+
rangeEl = this.locatorFor('.tn-table-pager__range');
|
|
7554
|
+
/**
|
|
7555
|
+
* Returns the rendered range text (e.g. `"1 – 20 of 47"`).
|
|
7556
|
+
*/
|
|
7557
|
+
async getRangeText() {
|
|
7558
|
+
const el = await this.rangeEl();
|
|
7559
|
+
return (await el.text()).replace(/\s+/g, ' ').trim();
|
|
7560
|
+
}
|
|
7561
|
+
/** Clicks the "first page" button. */
|
|
7562
|
+
async goToFirstPage() {
|
|
7563
|
+
const btn = await this.firstButton();
|
|
7564
|
+
await btn.click();
|
|
7565
|
+
}
|
|
7566
|
+
/** Clicks the "previous page" button. */
|
|
7567
|
+
async previousPage() {
|
|
7568
|
+
const btn = await this.previousButton();
|
|
7569
|
+
await btn.click();
|
|
7570
|
+
}
|
|
7571
|
+
/** Clicks the "next page" button. */
|
|
7572
|
+
async nextPage() {
|
|
7573
|
+
const btn = await this.nextButton();
|
|
7574
|
+
await btn.click();
|
|
7575
|
+
}
|
|
7576
|
+
/** Clicks the "last page" button. */
|
|
7577
|
+
async goToLastPage() {
|
|
7578
|
+
const btn = await this.lastButton();
|
|
7579
|
+
await btn.click();
|
|
7580
|
+
}
|
|
7581
|
+
async isFirstButtonDisabled() {
|
|
7582
|
+
const btn = await this.firstButton();
|
|
7583
|
+
return btn.isDisabled();
|
|
7584
|
+
}
|
|
7585
|
+
async isPreviousButtonDisabled() {
|
|
7586
|
+
const btn = await this.previousButton();
|
|
7587
|
+
return btn.isDisabled();
|
|
7588
|
+
}
|
|
7589
|
+
async isNextButtonDisabled() {
|
|
7590
|
+
const btn = await this.nextButton();
|
|
7591
|
+
return btn.isDisabled();
|
|
7592
|
+
}
|
|
7593
|
+
async isLastButtonDisabled() {
|
|
7594
|
+
const btn = await this.lastButton();
|
|
7595
|
+
return btn.isDisabled();
|
|
7596
|
+
}
|
|
7597
|
+
/** Returns the harness for the underlying page-size `tn-select`. */
|
|
7598
|
+
async getPageSizeSelect() {
|
|
7599
|
+
return this.pageSizeSelect();
|
|
7600
|
+
}
|
|
7601
|
+
}
|
|
7602
|
+
|
|
6945
7603
|
/**
|
|
6946
7604
|
* Tree flattener to convert normal type of node to node with children & level information.
|
|
6947
7605
|
*/
|
|
@@ -7025,7 +7683,7 @@ class TnTreeComponent extends CdkTree {
|
|
|
7025
7683
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7026
7684
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: TnTreeComponent, isStandalone: true, selector: "tn-tree", host: { attributes: { "role": "tree" }, classAttribute: "tn-tree" }, providers: [
|
|
7027
7685
|
{ provide: CdkTree, useExisting: TnTreeComponent }
|
|
7028
|
-
], exportAs: ["tnTree"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<ng-container cdkTreeNodeOutlet />", styles: [":host{display:block;width:100%}.tn-tree{width:100%;background-color:var(--tn-bg1);border:1px solid var(--tn-lines);border-radius:6px;overflow:hidden}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2.CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
7686
|
+
], exportAs: ["tnTree"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<ng-container cdkTreeNodeOutlet />", styles: [":host{display:block;width:100%}.tn-tree{width:100%;background-color:var(--tn-bg1);border:1px solid var(--tn-lines);border-radius:6px;overflow:hidden}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2$1.CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
7029
7687
|
}
|
|
7030
7688
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeComponent, decorators: [{
|
|
7031
7689
|
type: Component,
|
|
@@ -7056,7 +7714,7 @@ class TnTreeNodeComponent extends CdkTreeNode {
|
|
|
7056
7714
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7057
7715
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTreeNodeComponent, isStandalone: true, selector: "tn-tree-node", host: { attributes: { "role": "treeitem" }, properties: { "attr.aria-level": "level + 1", "attr.aria-expanded": "isExpandable ? isExpanded : null" }, classAttribute: "tn-tree-node-wrapper" }, providers: [
|
|
7058
7716
|
{ provide: CdkTreeNode, useExisting: TnTreeNodeComponent }
|
|
7059
|
-
], exportAs: ["tnTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-tree-node\"\n cdkTreeNodeToggle\n role=\"treeitem\"\n [class.tn-tree-node--expandable]=\"isExpandable\"\n [attr.aria-level]=\"level + 1\"\n [attr.aria-expanded]=\"isExpandable ? isExpanded : null\"\n [attr.aria-selected]=\"false\"\n [style.cursor]=\"isExpandable ? 'pointer' : 'default'\">\n \n <div class=\"tn-tree-node__content\">\n <!-- Arrow icon for expandable nodes -->\n @if (isExpandable) {\n <div\n class=\"tn-tree-node__toggle\"\n [class.tn-tree-node__toggle--expanded]=\"isExpanded\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </div>\n }\n\n <!-- Spacer for non-expandable nodes -->\n @if (!isExpandable) {\n <div class=\"tn-tree-node__spacer\"></div>\n }\n \n <!-- Node content -->\n <div class=\"tn-tree-node__text\">\n <ng-content />\n </div>\n </div>\n</div>", styles: [":host{display:block}.tn-tree-node{border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-tree-node:hover{background-color:var(--tn-alt-bg2)}.tn-tree-node:last-child{border-bottom:none}.tn-tree-node--expandable{cursor:pointer}.tn-tree-node--expandable:hover{background-color:var(--tn-alt-bg2)}.tn-tree-node--expandable:active{background-color:var(--tn-alt-bg1)}.tn-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px}.tn-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:none;color:var(--tn-fg2);cursor:pointer;border-radius:3px;transition:all .2s ease;flex-shrink:0}.tn-tree-node__toggle:hover{background-color:var(--tn-alt-bg2);color:var(--tn-fg1)}.tn-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:1px}.tn-tree-node__toggle svg{transition:transform .2s ease;transform:rotate(0)}.tn-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-tree-node__text{flex:1;min-width:0;color:var(--tn-fg1)}.tn-tree-node__children{padding-left:24px}.tn-tree-invisible{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2.CdkTreeNodeToggle, selector: "[cdkTreeNodeToggle]", inputs: ["cdkTreeNodeToggleRecursive"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7717
|
+
], exportAs: ["tnTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-tree-node\"\n cdkTreeNodeToggle\n role=\"treeitem\"\n [class.tn-tree-node--expandable]=\"isExpandable\"\n [attr.aria-level]=\"level + 1\"\n [attr.aria-expanded]=\"isExpandable ? isExpanded : null\"\n [attr.aria-selected]=\"false\"\n [style.cursor]=\"isExpandable ? 'pointer' : 'default'\">\n \n <div class=\"tn-tree-node__content\">\n <!-- Arrow icon for expandable nodes -->\n @if (isExpandable) {\n <div\n class=\"tn-tree-node__toggle\"\n [class.tn-tree-node__toggle--expanded]=\"isExpanded\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </div>\n }\n\n <!-- Spacer for non-expandable nodes -->\n @if (!isExpandable) {\n <div class=\"tn-tree-node__spacer\"></div>\n }\n \n <!-- Node content -->\n <div class=\"tn-tree-node__text\">\n <ng-content />\n </div>\n </div>\n</div>", styles: [":host{display:block}.tn-tree-node{border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-tree-node:hover{background-color:var(--tn-alt-bg2)}.tn-tree-node:last-child{border-bottom:none}.tn-tree-node--expandable{cursor:pointer}.tn-tree-node--expandable:hover{background-color:var(--tn-alt-bg2)}.tn-tree-node--expandable:active{background-color:var(--tn-alt-bg1)}.tn-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px}.tn-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:none;color:var(--tn-fg2);cursor:pointer;border-radius:3px;transition:all .2s ease;flex-shrink:0}.tn-tree-node__toggle:hover{background-color:var(--tn-alt-bg2);color:var(--tn-fg1)}.tn-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:1px}.tn-tree-node__toggle svg{transition:transform .2s ease;transform:rotate(0)}.tn-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-tree-node__text{flex:1;min-width:0;color:var(--tn-fg1)}.tn-tree-node__children{padding-left:24px}.tn-tree-invisible{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2$1.CdkTreeNodeToggle, selector: "[cdkTreeNodeToggle]", inputs: ["cdkTreeNodeToggleRecursive"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7060
7718
|
}
|
|
7061
7719
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeComponent, decorators: [{
|
|
7062
7720
|
type: Component,
|
|
@@ -7072,7 +7730,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
7072
7730
|
|
|
7073
7731
|
class TnTreeNodeOutletDirective {
|
|
7074
7732
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeOutletDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
7075
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnTreeNodeOutletDirective, isStandalone: true, selector: "[tnTreeNodeOutlet]", hostDirectives: [{ directive: i2.CdkTreeNodeOutlet }], ngImport: i0 });
|
|
7733
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnTreeNodeOutletDirective, isStandalone: true, selector: "[tnTreeNodeOutlet]", hostDirectives: [{ directive: i2$1.CdkTreeNodeOutlet }], ngImport: i0 });
|
|
7076
7734
|
}
|
|
7077
7735
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeOutletDirective, decorators: [{
|
|
7078
7736
|
type: Directive,
|
|
@@ -7137,7 +7795,7 @@ class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
|
|
|
7137
7795
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnNestedTreeNodeComponent, isStandalone: true, selector: "tn-nested-tree-node", host: { attributes: { "role": "treeitem" }, properties: { "attr.aria-level": "level + 1", "attr.aria-expanded": "isExpandable ? isExpanded : null" }, classAttribute: "tn-nested-tree-node-wrapper" }, providers: [
|
|
7138
7796
|
{ provide: CdkNestedTreeNode, useExisting: TnNestedTreeNodeComponent },
|
|
7139
7797
|
{ provide: CdkTreeNode, useExisting: TnNestedTreeNodeComponent }
|
|
7140
|
-
], exportAs: ["tnNestedTreeNode"], usesInheritance: true, ngImport: i0, template: "<div class=\"tn-nested-tree-node__content\">\n <!-- Toggle button for expandable nodes (provided by component) -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n cdkTreeNodeToggle\n type=\"button\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"'Toggle node'\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}", styles: [".tn-nested-tree-node-wrapper{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:.875rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2.CdkTreeNodeToggle, selector: "[cdkTreeNodeToggle]", inputs: ["cdkTreeNodeToggleRecursive"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7798
|
+
], exportAs: ["tnNestedTreeNode"], usesInheritance: true, ngImport: i0, template: "<div class=\"tn-nested-tree-node__content\">\n <!-- Toggle button for expandable nodes (provided by component) -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n cdkTreeNodeToggle\n type=\"button\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"'Toggle node'\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}", styles: [".tn-nested-tree-node-wrapper{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:.875rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2$1.CdkTreeNodeToggle, selector: "[cdkTreeNodeToggle]", inputs: ["cdkTreeNodeToggleRecursive"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7141
7799
|
}
|
|
7142
7800
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnNestedTreeNodeComponent, decorators: [{
|
|
7143
7801
|
type: Component,
|
|
@@ -9681,7 +10339,7 @@ class TnTimeInputComponent {
|
|
|
9681
10339
|
useExisting: forwardRef(() => TnTimeInputComponent),
|
|
9682
10340
|
multi: true
|
|
9683
10341
|
}
|
|
9684
|
-
], ngImport: i0, template: "<tn-select\n [options]=\"timeSelectOptions()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"testId()\"\n [ngModel]=\"_value\"\n (selectionChange)=\"onSelectionChange($event)\" />\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type:
|
|
10342
|
+
], ngImport: i0, template: "<tn-select\n [options]=\"timeSelectOptions()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"testId()\"\n [ngModel]=\"_value\"\n (selectionChange)=\"onSelectionChange($event)\" />\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "ariaLabel", "noOptionsLabel", "disabled", "testId", "multiple", "compareWith"], outputs: ["selectionChange", "multiSelectionChange"] }] });
|
|
9685
10343
|
}
|
|
9686
10344
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTimeInputComponent, decorators: [{
|
|
9687
10345
|
type: Component,
|
|
@@ -13307,5 +13965,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
13307
13965
|
* Generated bundle index. Do not edit.
|
|
13308
13966
|
*/
|
|
13309
13967
|
|
|
13310
|
-
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, 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, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
|
|
13968
|
+
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, 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, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, 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, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, 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, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
|
|
13311
13969
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|