@truenas/ui-components 0.4.4 → 0.4.6
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,19 +1,19 @@
|
|
|
1
1
|
import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
|
|
2
2
|
import { TemplatePortal, ComponentPortal, PortalModule } from '@angular/cdk/portal';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { InjectionToken, inject, computed, input, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, TemplateRef, ChangeDetectorRef, DestroyRef, isSignal, IterableDiffers, booleanAttribute, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
|
|
4
|
+
import { InjectionToken, inject, computed, input, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, TemplateRef, ChangeDetectorRef, DestroyRef, isSignal, IterableDiffers, booleanAttribute, LOCALE_ID, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
|
|
5
5
|
import * as i1$3 from '@angular/forms';
|
|
6
6
|
import { NgControl, NG_VALUE_ACCESSOR, FormsModule, Validators } from '@angular/forms';
|
|
7
|
-
import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
|
|
7
|
+
import { ComponentHarness, HarnessPredicate, TestKey, parallel } from '@angular/cdk/testing';
|
|
8
8
|
import * as i1 from '@angular/cdk/a11y';
|
|
9
|
-
import { A11yModule, FocusMonitor } from '@angular/cdk/a11y';
|
|
9
|
+
import { A11yModule, FocusMonitor, _IdGenerator } from '@angular/cdk/a11y';
|
|
10
10
|
import * as i1$2 from '@angular/common';
|
|
11
11
|
import { DOCUMENT, NgTemplateOutlet, CommonModule, AsyncPipe, isPlatformBrowser } from '@angular/common';
|
|
12
12
|
import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiOpenInNew, mdiHelpCircle, mdiDotsVertical, mdiClose } from '@mdi/js';
|
|
13
13
|
import * as i1$1 from '@angular/platform-browser';
|
|
14
14
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
15
15
|
import { HttpClient } from '@angular/common/http';
|
|
16
|
-
import { firstValueFrom, BehaviorSubject, merge, animationFrameScheduler, asapScheduler,
|
|
16
|
+
import { firstValueFrom, BehaviorSubject, merge, animationFrameScheduler, asapScheduler, take, Subject } from 'rxjs';
|
|
17
17
|
import { RouterLink } from '@angular/router';
|
|
18
18
|
import { filesize } from 'filesize';
|
|
19
19
|
import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
|
|
@@ -2504,6 +2504,11 @@ class TnButtonComponent {
|
|
|
2504
2504
|
*/
|
|
2505
2505
|
iconPosition = input('left', ...(ngDevMode ? [{ debugName: "iconPosition" }] : []));
|
|
2506
2506
|
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
2507
|
+
/**
|
|
2508
|
+
* Stretches the button to the full width of its container instead of
|
|
2509
|
+
* shrink-wrapping the label. Use for buttons that head or foot a form column.
|
|
2510
|
+
*/
|
|
2511
|
+
fullWidth = input(false, ...(ngDevMode ? [{ debugName: "fullWidth" }] : []));
|
|
2507
2512
|
/**
|
|
2508
2513
|
* Native `type` of the rendered `<button>`. Defaults to `button` so stray
|
|
2509
2514
|
* clicks never submit an enclosing form. Set to `submit` for a form's save
|
|
@@ -2614,12 +2619,14 @@ class TnButtonComponent {
|
|
|
2614
2619
|
host.focus = (options) => inner.focus(options);
|
|
2615
2620
|
}
|
|
2616
2621
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2617
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnButtonComponent, isStandalone: true, selector: "tn-button", inputs: { primary: { classPropertyName: "primary", publicName: "primary", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconPosition: { classPropertyName: "iconPosition", publicName: "iconPosition", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, queryParams: { classPropertyName: "queryParams", publicName: "queryParams", isSignal: true, isRequired: false, transformFunction: null }, fragment: { classPropertyName: "fragment", publicName: "fragment", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, rel: { classPropertyName: "rel", publicName: "rel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClick: "onClick" }, viewQueries: [{ propertyName: "innerRef", first: true, predicate: ["button"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template #content>\n @if (icon() && iconPosition() === 'left') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n <span class=\"storybook-button__label\" [innerHTML]=\"label() | tnLabelMarkup\"></span>\n @if (icon() && iconPosition() === 'right') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n</ng-template>\n\n@if (isRouterLink()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [routerLink]=\"disabled() ? null : routerLink()\"\n [queryParams]=\"queryParams()\"\n [fragment]=\"fragment()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else if (isAnchor()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [attr.href]=\"disabled() ? null : href()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else {\n <button\n #button\n tnTestIdType=\"button\"\n [attr.type]=\"type()\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [tnTestId]=\"testId()\"\n (click)=\"onClick.emit($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </button>\n}\n", styles: [":host{display:inline-block;width:fit-content;justify-self:center}.storybook-button ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.storybook-button{display:inline-block;cursor:pointer;border:1px solid transparent;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif;white-space:nowrap}.storybook-button:disabled,.storybook-button.is-disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.storybook-button--has-icon{display:inline-flex;align-items:center;justify-content:center;gap:8px}.storybook-button__icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}a.storybook-button{text-decoration:none;text-align:center}.button-primary{background-color:var(--tn-primary);color:var(--tn-primary-txt)}.button-default{box-shadow:#00000026 0 0 0 1px inset;background-color:var(--tn-btn-default-bg);color:var(--tn-btn-default-txt)}.button-outline-primary{background-color:transparent;border:1px solid var(--tn-primary);color:var(--tn-primary);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-primary:hover{background-color:var(--tn-primary);border:1px solid var(--tn-primary);color:var(--tn-primary-txt)}.button-outline-default{background-color:transparent;border:1px solid var(--tn-lines, #e5e7eb);color:var(--tn-fg1, #000000);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-default:hover{background-color:var(--tn-btn-default-bg);border:1px solid var(--tn-btn-default-bg);color:var(--tn-btn-default-txt);box-shadow:#00000026 0 0 0 1px inset}.button-warn{background-color:var(--tn-red);color:#fff}.button-outline-warn{background-color:transparent;border:1px solid var(--tn-red);color:var(--tn-red);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-warn:hover{background-color:var(--tn-red);border:1px solid var(--tn-red);color:#fff}.storybook-button--small{padding:10px 16px;font-size:12px}.storybook-button--medium{padding:11px 20px;font-size:14px}.storybook-button--large{padding:12px 24px;font-size:1rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }] });
|
|
2622
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnButtonComponent, isStandalone: true, selector: "tn-button", inputs: { primary: { classPropertyName: "primary", publicName: "primary", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconPosition: { classPropertyName: "iconPosition", publicName: "iconPosition", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, queryParams: { classPropertyName: "queryParams", publicName: "queryParams", isSignal: true, isRequired: false, transformFunction: null }, fragment: { classPropertyName: "fragment", publicName: "fragment", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, rel: { classPropertyName: "rel", publicName: "rel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClick: "onClick" }, host: { properties: { "class.tn-button--full-width": "fullWidth()" } }, viewQueries: [{ propertyName: "innerRef", first: true, predicate: ["button"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template #content>\n @if (icon() && iconPosition() === 'left') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n <span class=\"storybook-button__label\" [innerHTML]=\"label() | tnLabelMarkup\"></span>\n @if (icon() && iconPosition() === 'right') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n</ng-template>\n\n@if (isRouterLink()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [routerLink]=\"disabled() ? null : routerLink()\"\n [queryParams]=\"queryParams()\"\n [fragment]=\"fragment()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else if (isAnchor()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [attr.href]=\"disabled() ? null : href()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else {\n <button\n #button\n tnTestIdType=\"button\"\n [attr.type]=\"type()\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [tnTestId]=\"testId()\"\n (click)=\"onClick.emit($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </button>\n}\n", styles: [":host{display:inline-block;width:fit-content;justify-self:center}:host(.tn-button--full-width){display:block;width:100%;justify-self:stretch}:host(.tn-button--full-width) .storybook-button{width:100%}.storybook-button ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.storybook-button{display:inline-block;cursor:pointer;border:1px solid transparent;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif;white-space:nowrap}.storybook-button:disabled,.storybook-button.is-disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.storybook-button--has-icon{display:inline-flex;align-items:center;justify-content:center;gap:8px}.storybook-button__icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}a.storybook-button{text-decoration:none;text-align:center}.button-primary{background-color:var(--tn-primary);color:var(--tn-primary-txt)}.button-default{box-shadow:#00000026 0 0 0 1px inset;background-color:var(--tn-btn-default-bg);color:var(--tn-btn-default-txt)}.button-outline-primary{background-color:transparent;border:1px solid var(--tn-primary);color:var(--tn-primary);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-primary:hover{background-color:var(--tn-primary);border:1px solid var(--tn-primary);color:var(--tn-primary-txt)}.button-outline-default{background-color:transparent;border:1px solid var(--tn-lines, #e5e7eb);color:var(--tn-fg1, #000000);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-default:hover{background-color:var(--tn-btn-default-bg);border:1px solid var(--tn-btn-default-bg);color:var(--tn-btn-default-txt);box-shadow:#00000026 0 0 0 1px inset}.button-warn{background-color:var(--tn-red);color:#fff}.button-outline-warn{background-color:transparent;border:1px solid var(--tn-red);color:var(--tn-red);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-warn:hover{background-color:var(--tn-red);border:1px solid var(--tn-red);color:#fff}.storybook-button--small{padding:10px 16px;font-size:12px}.storybook-button--medium{padding:11px 20px;font-size:14px}.storybook-button--large{padding:12px 24px;font-size:1rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }] });
|
|
2618
2623
|
}
|
|
2619
2624
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnButtonComponent, decorators: [{
|
|
2620
2625
|
type: Component,
|
|
2621
|
-
args: [{ selector: 'tn-button', standalone: true, imports: [CommonModule, RouterLink, TnTestIdDirective, LabelMarkupPipe, TnIconComponent],
|
|
2622
|
-
|
|
2626
|
+
args: [{ selector: 'tn-button', standalone: true, imports: [CommonModule, RouterLink, TnTestIdDirective, LabelMarkupPipe, TnIconComponent], host: {
|
|
2627
|
+
'[class.tn-button--full-width]': 'fullWidth()',
|
|
2628
|
+
}, template: "<ng-template #content>\n @if (icon() && iconPosition() === 'left') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n <span class=\"storybook-button__label\" [innerHTML]=\"label() | tnLabelMarkup\"></span>\n @if (icon() && iconPosition() === 'right') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n</ng-template>\n\n@if (isRouterLink()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [routerLink]=\"disabled() ? null : routerLink()\"\n [queryParams]=\"queryParams()\"\n [fragment]=\"fragment()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else if (isAnchor()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [attr.href]=\"disabled() ? null : href()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else {\n <button\n #button\n tnTestIdType=\"button\"\n [attr.type]=\"type()\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [tnTestId]=\"testId()\"\n (click)=\"onClick.emit($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </button>\n}\n", styles: [":host{display:inline-block;width:fit-content;justify-self:center}:host(.tn-button--full-width){display:block;width:100%;justify-self:stretch}:host(.tn-button--full-width) .storybook-button{width:100%}.storybook-button ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.storybook-button{display:inline-block;cursor:pointer;border:1px solid transparent;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif;white-space:nowrap}.storybook-button:disabled,.storybook-button.is-disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.storybook-button--has-icon{display:inline-flex;align-items:center;justify-content:center;gap:8px}.storybook-button__icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}a.storybook-button{text-decoration:none;text-align:center}.button-primary{background-color:var(--tn-primary);color:var(--tn-primary-txt)}.button-default{box-shadow:#00000026 0 0 0 1px inset;background-color:var(--tn-btn-default-bg);color:var(--tn-btn-default-txt)}.button-outline-primary{background-color:transparent;border:1px solid var(--tn-primary);color:var(--tn-primary);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-primary:hover{background-color:var(--tn-primary);border:1px solid var(--tn-primary);color:var(--tn-primary-txt)}.button-outline-default{background-color:transparent;border:1px solid var(--tn-lines, #e5e7eb);color:var(--tn-fg1, #000000);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-default:hover{background-color:var(--tn-btn-default-bg);border:1px solid var(--tn-btn-default-bg);color:var(--tn-btn-default-txt);box-shadow:#00000026 0 0 0 1px inset}.button-warn{background-color:var(--tn-red);color:#fff}.button-outline-warn{background-color:transparent;border:1px solid var(--tn-red);color:var(--tn-red);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-warn:hover{background-color:var(--tn-red);border:1px solid var(--tn-red);color:#fff}.storybook-button--small{padding:10px 16px;font-size:12px}.storybook-button--medium{padding:11px 20px;font-size:14px}.storybook-button--large{padding:12px 24px;font-size:1rem}\n"] }]
|
|
2629
|
+
}], propDecorators: { primary: [{ type: i0.Input, args: [{ isSignal: true, alias: "primary", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], iconPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconPosition", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], queryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParams", required: false }] }], fragment: [{ type: i0.Input, args: [{ isSignal: true, alias: "fragment", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], rel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], onClick: [{ type: i0.Output, args: ["onClick"] }], innerRef: [{ type: i0.ViewChild, args: ['button', { isSignal: true }] }] } });
|
|
2623
2630
|
|
|
2624
2631
|
/**
|
|
2625
2632
|
* Harness for interacting with tn-button in tests.
|
|
@@ -4500,7 +4507,7 @@ class TnFileInputComponent {
|
|
|
4500
4507
|
useExisting: forwardRef(() => TnFileInputComponent),
|
|
4501
4508
|
multi: true
|
|
4502
4509
|
}
|
|
4503
|
-
], viewQueries: [{ propertyName: "fileInputEl", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-file-input__container\" tnTestIdType=\"file-input\" [tnTestId]=\"testId()\" (focusout)=\"onBlur()\">\n <input\n #fileInput\n type=\"file\"\n class=\"tn-file-input__native\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onFilesSelected($event)\">\n\n <tn-button\n class=\"tn-file-input__button\"\n color=\"default\"\n [label]=\"buttonLabel()\"\n [disabled]=\"isDisabled()\"\n [ariaLabel]=\"ariaLabel()\"\n (onClick)=\"openFileDialog()\" />\n\n @if (showFileName()) {\n <span class=\"tn-file-input__filename\" [class.tn-file-input__filename--empty]=\"!hasFiles()\">\n {{ hasFiles() ? fileNames() : noFileText() }}\n </span>\n }\n\n <!--\n Always-present live region so screen readers are told what was picked even\n when the visible file name is suppressed (showFileName=false).\n -->\n <span class=\"tn-file-input__announcer\" aria-live=\"polite\">\n {{ hasFiles() ? fileNames() : '' }}\n </span>\n</div>\n", styles: [":host{display:inline-block}.tn-file-input__container{display:inline-flex;align-items:center;gap:12px}.tn-file-input__native,.tn-file-input__announcer{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.tn-file-input__button{flex-shrink:0}.tn-file-input__filename{font-size:14px;color:var(--tn-fg1);max-width:30ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-file-input__filename--empty{color:var(--tn-fg2)}\n"], dependencies: [{ kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
4510
|
+
], viewQueries: [{ propertyName: "fileInputEl", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-file-input__container\" tnTestIdType=\"file-input\" [tnTestId]=\"testId()\" (focusout)=\"onBlur()\">\n <input\n #fileInput\n type=\"file\"\n class=\"tn-file-input__native\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onFilesSelected($event)\">\n\n <tn-button\n class=\"tn-file-input__button\"\n color=\"default\"\n [label]=\"buttonLabel()\"\n [disabled]=\"isDisabled()\"\n [ariaLabel]=\"ariaLabel()\"\n (onClick)=\"openFileDialog()\" />\n\n @if (showFileName()) {\n <span class=\"tn-file-input__filename\" [class.tn-file-input__filename--empty]=\"!hasFiles()\">\n {{ hasFiles() ? fileNames() : noFileText() }}\n </span>\n }\n\n <!--\n Always-present live region so screen readers are told what was picked even\n when the visible file name is suppressed (showFileName=false).\n -->\n <span class=\"tn-file-input__announcer\" aria-live=\"polite\">\n {{ hasFiles() ? fileNames() : '' }}\n </span>\n</div>\n", styles: [":host{display:inline-block}.tn-file-input__container{display:inline-flex;align-items:center;gap:12px}.tn-file-input__native,.tn-file-input__announcer{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.tn-file-input__button{flex-shrink:0}.tn-file-input__filename{font-size:14px;color:var(--tn-fg1);max-width:30ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-file-input__filename--empty{color:var(--tn-fg2)}\n"], dependencies: [{ kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
4504
4511
|
}
|
|
4505
4512
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFileInputComponent, decorators: [{
|
|
4506
4513
|
type: Component,
|
|
@@ -6057,6 +6064,12 @@ class TnSlideToggleComponent {
|
|
|
6057
6064
|
toggleEl = viewChild.required('toggleEl');
|
|
6058
6065
|
labelPosition = input('after', ...(ngDevMode ? [{ debugName: "labelPosition" }] : []));
|
|
6059
6066
|
label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
6067
|
+
/**
|
|
6068
|
+
* Stretches the toggle to the full width of its container and pushes the label
|
|
6069
|
+
* and the track to opposite ends of the row, instead of shrink-wrapping them
|
|
6070
|
+
* side by side. Use for settings rows and option lists.
|
|
6071
|
+
*/
|
|
6072
|
+
fullWidth = input(false, ...(ngDevMode ? [{ debugName: "fullWidth" }] : []));
|
|
6060
6073
|
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
6061
6074
|
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
6062
6075
|
color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : []));
|
|
@@ -6137,6 +6150,9 @@ class TnSlideToggleComponent {
|
|
|
6137
6150
|
if (this.effectiveChecked()) {
|
|
6138
6151
|
classes.push('tn-slide-toggle--checked');
|
|
6139
6152
|
}
|
|
6153
|
+
if (this.fullWidth()) {
|
|
6154
|
+
classes.push('tn-slide-toggle--full-width');
|
|
6155
|
+
}
|
|
6140
6156
|
classes.push(`tn-slide-toggle--${this.color()}`);
|
|
6141
6157
|
classes.push(`tn-slide-toggle--label-${this.labelPosition()}`);
|
|
6142
6158
|
return classes;
|
|
@@ -6145,24 +6161,29 @@ class TnSlideToggleComponent {
|
|
|
6145
6161
|
return this.ariaLabel() || (this.label() ? undefined : 'Toggle');
|
|
6146
6162
|
}, ...(ngDevMode ? [{ debugName: "effectiveAriaLabel" }] : []));
|
|
6147
6163
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSlideToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6148
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSlideToggleComponent, isStandalone: true, selector: "tn-slide-toggle", inputs: { labelPosition: { classPropertyName: "labelPosition", publicName: "labelPosition", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change", toggleChange: "toggleChange" }, providers: [
|
|
6164
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSlideToggleComponent, isStandalone: true, selector: "tn-slide-toggle", inputs: { labelPosition: { classPropertyName: "labelPosition", publicName: "labelPosition", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change", toggleChange: "toggleChange" }, host: { properties: { "class.tn-slide-toggle-host--full-width": "fullWidth()" } }, providers: [
|
|
6149
6165
|
{
|
|
6150
6166
|
provide: NG_VALUE_ACCESSOR,
|
|
6151
6167
|
useExisting: forwardRef(() => TnSlideToggleComponent),
|
|
6152
6168
|
multi: true
|
|
6153
6169
|
}
|
|
6154
|
-
], viewQueries: [{ propertyName: "toggleEl", first: true, predicate: ["toggleEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-slide-toggle__label\" tnTestIdType=\"toggle\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n\n <!-- Label before toggle -->\n @if (label() && labelPosition() === 'before') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--before\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n <!-- Toggle track and thumb -->\n <div class=\"tn-slide-toggle__bar\">\n <input\n #toggleEl\n type=\"checkbox\"\n class=\"tn-slide-toggle__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledby()\"\n (change)=\"onToggleChange($event)\"\n />\n\n <div class=\"tn-slide-toggle__track\">\n <div class=\"tn-slide-toggle__track-fill\"></div>\n </div>\n\n <div class=\"tn-slide-toggle__thumb-container\">\n <div class=\"tn-slide-toggle__thumb\">\n <div class=\"tn-slide-toggle__ripple\"></div>\n <!-- State icon -->\n <div class=\"tn-slide-toggle__icon\">\n @if (effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__check-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n </svg>\n }\n @if (!effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__minus-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M19 13H5v-2h14v2z\" fill=\"currentColor\"/>\n </svg>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Label after toggle -->\n @if (label() && labelPosition() === 'after') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--after\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n </label>\n</div>", styles: [".tn-slide-toggle{display:inline-flex;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:1;font-family:inherit}.tn-slide-toggle__label{display:flex;align-items:center;cursor:inherit}.tn-slide-toggle__label-text ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.tn-slide-toggle__label-text{font-size:14px;line-height:1.4;color:var(--tn-fg1);transition:color .2s ease}.tn-slide-toggle__label-text--before{margin-right:8px}.tn-slide-toggle__label-text--after{margin-left:8px}.tn-slide-toggle__input{position:absolute;opacity:0;width:0;height:0;margin:0;pointer-events:none}.tn-slide-toggle__input:focus+.tn-slide-toggle__track{box-shadow:0 0 0 2px rgba(var(--tn-primary-rgb, 0, 123, 255),.2)}.tn-slide-toggle__bar{position:relative;display:flex;align-items:center;flex-shrink:0}.tn-slide-toggle__track{position:relative;width:52px;height:32px;border-radius:16px;background-color:var(--tn-lines);border:1px solid transparent;transition:background-color .2s ease,border-color .2s ease;overflow:hidden}.tn-slide-toggle__track-fill{position:absolute;inset:0;border-radius:inherit;background-color:var(--tn-primary, #007cba);opacity:0;transform:scaleX(0);transform-origin:left center;transition:opacity .2s ease,transform .2s ease}.tn-slide-toggle__thumb-container{position:absolute;top:50%;left:4px;transform:translateY(-50%);width:24px;height:24px;transition:transform .2s ease;z-index:1}.tn-slide-toggle__thumb{position:relative;width:24px;height:24px;border-radius:50%;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);box-shadow:0 2px 4px #0003;transition:background-color .2s ease,border-color .2s ease,box-shadow .2s ease;display:flex;align-items:center;justify-content:center}.tn-slide-toggle__ripple{position:absolute;width:40px;height:40px;border-radius:50%;background-color:transparent;top:50%;left:50%;transform:translate(-50%,-50%);opacity:0;transition:opacity .2s ease,background-color .2s ease}.tn-slide-toggle__icon{position:relative;display:flex;align-items:center;justify-content:center;z-index:2;pointer-events:none}.tn-slide-toggle__check-icon,.tn-slide-toggle__minus-icon{transition:opacity .2s ease,transform .2s ease;color:var(--tn-fg2)}.tn-slide-toggle:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.08}.tn-slide-toggle--checked .tn-slide-toggle__track{background-color:var(--tn-primary);opacity:.5}.tn-slide-toggle--checked .tn-slide-toggle__track-fill{opacity:1;transform:scaleX(1)}.tn-slide-toggle--checked .tn-slide-toggle__thumb-container{transform:translateY(-50%) translate(24px)}.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-bg2);border-color:var(--tn-primary)}.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-primary)}.tn-slide-toggle--checked:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.12}.tn-slide-toggle--disabled{cursor:not-allowed;opacity:.6}.tn-slide-toggle--disabled .tn-slide-toggle__label-text{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled .tn-slide-toggle__track{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1);box-shadow:none}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track,.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track-fill{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled:hover .tn-slide-toggle__ripple{opacity:0}.tn-slide-toggle--accent{--tn-primary: var(--tn-accent)}.tn-slide-toggle--warn{--tn-primary: var(--tn-red)}.tn-slide-toggle .tn-slide-toggle__input:focus-visible+.tn-slide-toggle__track{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-slide-toggle:active:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.16;transform:translate(-50%,-50%) scale(1.1)}@media(prefers-contrast:high){.tn-slide-toggle .tn-slide-toggle__track{border-color:var(--tn-fg1)}.tn-slide-toggle .tn-slide-toggle__thumb{border-width:2px}.tn-slide-toggle--disabled .tn-slide-toggle__track,.tn-slide-toggle--disabled .tn-slide-toggle__thumb{border-color:var(--tn-alt-fg1)}}@media(prefers-reduced-motion:reduce){.tn-slide-toggle .tn-slide-toggle__track,.tn-slide-toggle .tn-slide-toggle__track-fill,.tn-slide-toggle .tn-slide-toggle__thumb-container,.tn-slide-toggle .tn-slide-toggle__thumb,.tn-slide-toggle .tn-slide-toggle__ripple{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }] });
|
|
6170
|
+
], viewQueries: [{ propertyName: "toggleEl", first: true, predicate: ["toggleEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-slide-toggle__label\" tnTestIdType=\"toggle\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n\n <!-- Label before toggle -->\n @if (label() && labelPosition() === 'before') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--before\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n <!-- Toggle track and thumb -->\n <div class=\"tn-slide-toggle__bar\">\n <input\n #toggleEl\n type=\"checkbox\"\n class=\"tn-slide-toggle__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledby()\"\n (change)=\"onToggleChange($event)\"\n />\n\n <div class=\"tn-slide-toggle__track\">\n <div class=\"tn-slide-toggle__track-fill\"></div>\n </div>\n\n <div class=\"tn-slide-toggle__thumb-container\">\n <div class=\"tn-slide-toggle__thumb\">\n <div class=\"tn-slide-toggle__ripple\"></div>\n <!-- State icon -->\n <div class=\"tn-slide-toggle__icon\">\n @if (effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__check-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n </svg>\n }\n @if (!effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__minus-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M19 13H5v-2h14v2z\" fill=\"currentColor\"/>\n </svg>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Label after toggle -->\n @if (label() && labelPosition() === 'after') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--after\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n </label>\n</div>", styles: [":host(.tn-slide-toggle-host--full-width){display:block;width:100%}.tn-slide-toggle{display:inline-flex;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:1;font-family:inherit}.tn-slide-toggle__label{display:flex;align-items:center;cursor:inherit}.tn-slide-toggle--full-width{display:flex;width:100%}.tn-slide-toggle--full-width .tn-slide-toggle__label{justify-content:space-between;width:100%}.tn-slide-toggle__label-text ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.tn-slide-toggle__label-text{font-size:14px;line-height:1.4;color:var(--tn-fg1);transition:color .2s ease}.tn-slide-toggle__label-text--before{margin-right:8px}.tn-slide-toggle__label-text--after{margin-left:8px}.tn-slide-toggle__input{position:absolute;opacity:0;width:0;height:0;margin:0;pointer-events:none}.tn-slide-toggle__input:focus+.tn-slide-toggle__track{box-shadow:0 0 0 2px rgba(var(--tn-primary-rgb, 0, 123, 255),.2)}.tn-slide-toggle__bar{position:relative;display:flex;align-items:center;flex-shrink:0}.tn-slide-toggle__track{position:relative;width:52px;height:32px;border-radius:16px;background-color:var(--tn-lines);border:1px solid transparent;transition:background-color .2s ease,border-color .2s ease;overflow:hidden}.tn-slide-toggle__track-fill{position:absolute;inset:0;border-radius:inherit;background-color:var(--tn-primary, #007cba);opacity:0;transform:scaleX(0);transform-origin:left center;transition:opacity .2s ease,transform .2s ease}.tn-slide-toggle__thumb-container{position:absolute;top:50%;left:4px;transform:translateY(-50%);width:24px;height:24px;transition:transform .2s ease;z-index:1}.tn-slide-toggle__thumb{position:relative;width:24px;height:24px;border-radius:50%;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);box-shadow:0 2px 4px #0003;transition:background-color .2s ease,border-color .2s ease,box-shadow .2s ease;display:flex;align-items:center;justify-content:center}.tn-slide-toggle__ripple{position:absolute;width:40px;height:40px;border-radius:50%;background-color:transparent;top:50%;left:50%;transform:translate(-50%,-50%);opacity:0;transition:opacity .2s ease,background-color .2s ease}.tn-slide-toggle__icon{position:relative;display:flex;align-items:center;justify-content:center;z-index:2;pointer-events:none}.tn-slide-toggle__check-icon,.tn-slide-toggle__minus-icon{transition:opacity .2s ease,transform .2s ease;color:var(--tn-fg2)}.tn-slide-toggle:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.08}.tn-slide-toggle--checked .tn-slide-toggle__track{background-color:var(--tn-primary);opacity:.5}.tn-slide-toggle--checked .tn-slide-toggle__track-fill{opacity:1;transform:scaleX(1)}.tn-slide-toggle--checked .tn-slide-toggle__thumb-container{transform:translateY(-50%) translate(24px)}.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-bg2);border-color:var(--tn-primary)}.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-primary)}.tn-slide-toggle--checked:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.12}.tn-slide-toggle--disabled{cursor:not-allowed;opacity:.6}.tn-slide-toggle--disabled .tn-slide-toggle__label-text{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled .tn-slide-toggle__track{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1);box-shadow:none}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track,.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track-fill{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled:hover .tn-slide-toggle__ripple{opacity:0}.tn-slide-toggle--accent{--tn-primary: var(--tn-accent)}.tn-slide-toggle--warn{--tn-primary: var(--tn-red)}.tn-slide-toggle .tn-slide-toggle__input:focus-visible+.tn-slide-toggle__track{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-slide-toggle:active:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.16;transform:translate(-50%,-50%) scale(1.1)}@media(prefers-contrast:high){.tn-slide-toggle .tn-slide-toggle__track{border-color:var(--tn-fg1)}.tn-slide-toggle .tn-slide-toggle__thumb{border-width:2px}.tn-slide-toggle--disabled .tn-slide-toggle__track,.tn-slide-toggle--disabled .tn-slide-toggle__thumb{border-color:var(--tn-alt-fg1)}}@media(prefers-reduced-motion:reduce){.tn-slide-toggle .tn-slide-toggle__track,.tn-slide-toggle .tn-slide-toggle__track-fill,.tn-slide-toggle .tn-slide-toggle__thumb-container,.tn-slide-toggle .tn-slide-toggle__thumb,.tn-slide-toggle .tn-slide-toggle__ripple{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }] });
|
|
6155
6171
|
}
|
|
6156
6172
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSlideToggleComponent, decorators: [{
|
|
6157
6173
|
type: Component,
|
|
6158
|
-
args: [{ selector: 'tn-slide-toggle', standalone: true, imports: [CommonModule, FormsModule, A11yModule, TnTestIdDirective, LabelMarkupPipe],
|
|
6174
|
+
args: [{ selector: 'tn-slide-toggle', standalone: true, imports: [CommonModule, FormsModule, A11yModule, TnTestIdDirective, LabelMarkupPipe], host: {
|
|
6175
|
+
// The `.tn-slide-toggle` class sits on an inner <div>; the host element is
|
|
6176
|
+
// inline by default, so it has to be stretched too or the inner width has
|
|
6177
|
+
// nothing to fill.
|
|
6178
|
+
'[class.tn-slide-toggle-host--full-width]': 'fullWidth()',
|
|
6179
|
+
}, providers: [
|
|
6159
6180
|
{
|
|
6160
6181
|
provide: NG_VALUE_ACCESSOR,
|
|
6161
6182
|
useExisting: forwardRef(() => TnSlideToggleComponent),
|
|
6162
6183
|
multi: true
|
|
6163
6184
|
}
|
|
6164
|
-
], template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-slide-toggle__label\" tnTestIdType=\"toggle\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n\n <!-- Label before toggle -->\n @if (label() && labelPosition() === 'before') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--before\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n <!-- Toggle track and thumb -->\n <div class=\"tn-slide-toggle__bar\">\n <input\n #toggleEl\n type=\"checkbox\"\n class=\"tn-slide-toggle__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledby()\"\n (change)=\"onToggleChange($event)\"\n />\n\n <div class=\"tn-slide-toggle__track\">\n <div class=\"tn-slide-toggle__track-fill\"></div>\n </div>\n\n <div class=\"tn-slide-toggle__thumb-container\">\n <div class=\"tn-slide-toggle__thumb\">\n <div class=\"tn-slide-toggle__ripple\"></div>\n <!-- State icon -->\n <div class=\"tn-slide-toggle__icon\">\n @if (effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__check-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n </svg>\n }\n @if (!effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__minus-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M19 13H5v-2h14v2z\" fill=\"currentColor\"/>\n </svg>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Label after toggle -->\n @if (label() && labelPosition() === 'after') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--after\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n </label>\n</div>", styles: [".tn-slide-toggle{display:inline-flex;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:1;font-family:inherit}.tn-slide-toggle__label{display:flex;align-items:center;cursor:inherit}.tn-slide-toggle__label-text ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.tn-slide-toggle__label-text{font-size:14px;line-height:1.4;color:var(--tn-fg1);transition:color .2s ease}.tn-slide-toggle__label-text--before{margin-right:8px}.tn-slide-toggle__label-text--after{margin-left:8px}.tn-slide-toggle__input{position:absolute;opacity:0;width:0;height:0;margin:0;pointer-events:none}.tn-slide-toggle__input:focus+.tn-slide-toggle__track{box-shadow:0 0 0 2px rgba(var(--tn-primary-rgb, 0, 123, 255),.2)}.tn-slide-toggle__bar{position:relative;display:flex;align-items:center;flex-shrink:0}.tn-slide-toggle__track{position:relative;width:52px;height:32px;border-radius:16px;background-color:var(--tn-lines);border:1px solid transparent;transition:background-color .2s ease,border-color .2s ease;overflow:hidden}.tn-slide-toggle__track-fill{position:absolute;inset:0;border-radius:inherit;background-color:var(--tn-primary, #007cba);opacity:0;transform:scaleX(0);transform-origin:left center;transition:opacity .2s ease,transform .2s ease}.tn-slide-toggle__thumb-container{position:absolute;top:50%;left:4px;transform:translateY(-50%);width:24px;height:24px;transition:transform .2s ease;z-index:1}.tn-slide-toggle__thumb{position:relative;width:24px;height:24px;border-radius:50%;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);box-shadow:0 2px 4px #0003;transition:background-color .2s ease,border-color .2s ease,box-shadow .2s ease;display:flex;align-items:center;justify-content:center}.tn-slide-toggle__ripple{position:absolute;width:40px;height:40px;border-radius:50%;background-color:transparent;top:50%;left:50%;transform:translate(-50%,-50%);opacity:0;transition:opacity .2s ease,background-color .2s ease}.tn-slide-toggle__icon{position:relative;display:flex;align-items:center;justify-content:center;z-index:2;pointer-events:none}.tn-slide-toggle__check-icon,.tn-slide-toggle__minus-icon{transition:opacity .2s ease,transform .2s ease;color:var(--tn-fg2)}.tn-slide-toggle:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.08}.tn-slide-toggle--checked .tn-slide-toggle__track{background-color:var(--tn-primary);opacity:.5}.tn-slide-toggle--checked .tn-slide-toggle__track-fill{opacity:1;transform:scaleX(1)}.tn-slide-toggle--checked .tn-slide-toggle__thumb-container{transform:translateY(-50%) translate(24px)}.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-bg2);border-color:var(--tn-primary)}.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-primary)}.tn-slide-toggle--checked:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.12}.tn-slide-toggle--disabled{cursor:not-allowed;opacity:.6}.tn-slide-toggle--disabled .tn-slide-toggle__label-text{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled .tn-slide-toggle__track{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1);box-shadow:none}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track,.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track-fill{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled:hover .tn-slide-toggle__ripple{opacity:0}.tn-slide-toggle--accent{--tn-primary: var(--tn-accent)}.tn-slide-toggle--warn{--tn-primary: var(--tn-red)}.tn-slide-toggle .tn-slide-toggle__input:focus-visible+.tn-slide-toggle__track{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-slide-toggle:active:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.16;transform:translate(-50%,-50%) scale(1.1)}@media(prefers-contrast:high){.tn-slide-toggle .tn-slide-toggle__track{border-color:var(--tn-fg1)}.tn-slide-toggle .tn-slide-toggle__thumb{border-width:2px}.tn-slide-toggle--disabled .tn-slide-toggle__track,.tn-slide-toggle--disabled .tn-slide-toggle__thumb{border-color:var(--tn-alt-fg1)}}@media(prefers-reduced-motion:reduce){.tn-slide-toggle .tn-slide-toggle__track,.tn-slide-toggle .tn-slide-toggle__track-fill,.tn-slide-toggle .tn-slide-toggle__thumb-container,.tn-slide-toggle .tn-slide-toggle__thumb,.tn-slide-toggle .tn-slide-toggle__ripple{transition:none}}\n"] }]
|
|
6165
|
-
}], propDecorators: { toggleEl: [{ type: i0.ViewChild, args: ['toggleEl', { isSignal: true }] }], labelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPosition", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], toggleChange: [{ type: i0.Output, args: ["toggleChange"] }] } });
|
|
6185
|
+
], template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-slide-toggle__label\" tnTestIdType=\"toggle\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n\n <!-- Label before toggle -->\n @if (label() && labelPosition() === 'before') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--before\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n <!-- Toggle track and thumb -->\n <div class=\"tn-slide-toggle__bar\">\n <input\n #toggleEl\n type=\"checkbox\"\n class=\"tn-slide-toggle__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledby()\"\n (change)=\"onToggleChange($event)\"\n />\n\n <div class=\"tn-slide-toggle__track\">\n <div class=\"tn-slide-toggle__track-fill\"></div>\n </div>\n\n <div class=\"tn-slide-toggle__thumb-container\">\n <div class=\"tn-slide-toggle__thumb\">\n <div class=\"tn-slide-toggle__ripple\"></div>\n <!-- State icon -->\n <div class=\"tn-slide-toggle__icon\">\n @if (effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__check-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n </svg>\n }\n @if (!effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__minus-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M19 13H5v-2h14v2z\" fill=\"currentColor\"/>\n </svg>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Label after toggle -->\n @if (label() && labelPosition() === 'after') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--after\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n </label>\n</div>", styles: [":host(.tn-slide-toggle-host--full-width){display:block;width:100%}.tn-slide-toggle{display:inline-flex;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:1;font-family:inherit}.tn-slide-toggle__label{display:flex;align-items:center;cursor:inherit}.tn-slide-toggle--full-width{display:flex;width:100%}.tn-slide-toggle--full-width .tn-slide-toggle__label{justify-content:space-between;width:100%}.tn-slide-toggle__label-text ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.tn-slide-toggle__label-text{font-size:14px;line-height:1.4;color:var(--tn-fg1);transition:color .2s ease}.tn-slide-toggle__label-text--before{margin-right:8px}.tn-slide-toggle__label-text--after{margin-left:8px}.tn-slide-toggle__input{position:absolute;opacity:0;width:0;height:0;margin:0;pointer-events:none}.tn-slide-toggle__input:focus+.tn-slide-toggle__track{box-shadow:0 0 0 2px rgba(var(--tn-primary-rgb, 0, 123, 255),.2)}.tn-slide-toggle__bar{position:relative;display:flex;align-items:center;flex-shrink:0}.tn-slide-toggle__track{position:relative;width:52px;height:32px;border-radius:16px;background-color:var(--tn-lines);border:1px solid transparent;transition:background-color .2s ease,border-color .2s ease;overflow:hidden}.tn-slide-toggle__track-fill{position:absolute;inset:0;border-radius:inherit;background-color:var(--tn-primary, #007cba);opacity:0;transform:scaleX(0);transform-origin:left center;transition:opacity .2s ease,transform .2s ease}.tn-slide-toggle__thumb-container{position:absolute;top:50%;left:4px;transform:translateY(-50%);width:24px;height:24px;transition:transform .2s ease;z-index:1}.tn-slide-toggle__thumb{position:relative;width:24px;height:24px;border-radius:50%;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);box-shadow:0 2px 4px #0003;transition:background-color .2s ease,border-color .2s ease,box-shadow .2s ease;display:flex;align-items:center;justify-content:center}.tn-slide-toggle__ripple{position:absolute;width:40px;height:40px;border-radius:50%;background-color:transparent;top:50%;left:50%;transform:translate(-50%,-50%);opacity:0;transition:opacity .2s ease,background-color .2s ease}.tn-slide-toggle__icon{position:relative;display:flex;align-items:center;justify-content:center;z-index:2;pointer-events:none}.tn-slide-toggle__check-icon,.tn-slide-toggle__minus-icon{transition:opacity .2s ease,transform .2s ease;color:var(--tn-fg2)}.tn-slide-toggle:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.08}.tn-slide-toggle--checked .tn-slide-toggle__track{background-color:var(--tn-primary);opacity:.5}.tn-slide-toggle--checked .tn-slide-toggle__track-fill{opacity:1;transform:scaleX(1)}.tn-slide-toggle--checked .tn-slide-toggle__thumb-container{transform:translateY(-50%) translate(24px)}.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-bg2);border-color:var(--tn-primary)}.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-primary)}.tn-slide-toggle--checked:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.12}.tn-slide-toggle--disabled{cursor:not-allowed;opacity:.6}.tn-slide-toggle--disabled .tn-slide-toggle__label-text{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled .tn-slide-toggle__track{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1);box-shadow:none}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track,.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track-fill{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled:hover .tn-slide-toggle__ripple{opacity:0}.tn-slide-toggle--accent{--tn-primary: var(--tn-accent)}.tn-slide-toggle--warn{--tn-primary: var(--tn-red)}.tn-slide-toggle .tn-slide-toggle__input:focus-visible+.tn-slide-toggle__track{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-slide-toggle:active:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.16;transform:translate(-50%,-50%) scale(1.1)}@media(prefers-contrast:high){.tn-slide-toggle .tn-slide-toggle__track{border-color:var(--tn-fg1)}.tn-slide-toggle .tn-slide-toggle__thumb{border-width:2px}.tn-slide-toggle--disabled .tn-slide-toggle__track,.tn-slide-toggle--disabled .tn-slide-toggle__thumb{border-color:var(--tn-alt-fg1)}}@media(prefers-reduced-motion:reduce){.tn-slide-toggle .tn-slide-toggle__track,.tn-slide-toggle .tn-slide-toggle__track-fill,.tn-slide-toggle .tn-slide-toggle__thumb-container,.tn-slide-toggle .tn-slide-toggle__thumb,.tn-slide-toggle .tn-slide-toggle__ripple{transition:none}}\n"] }]
|
|
6186
|
+
}], propDecorators: { toggleEl: [{ type: i0.ViewChild, args: ['toggleEl', { isSignal: true }] }], labelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPosition", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], toggleChange: [{ type: i0.Output, args: ["toggleChange"] }] } });
|
|
6166
6187
|
|
|
6167
6188
|
class TnCardComponent {
|
|
6168
6189
|
iconRegistry = inject(TnIconRegistryService);
|
|
@@ -6271,7 +6292,7 @@ class TnCardComponent {
|
|
|
6271
6292
|
return type ? `tn-card__status--${type}` : 'tn-card__status--neutral';
|
|
6272
6293
|
}
|
|
6273
6294
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6274
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCardComponent, isStandalone: true, selector: "tn-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, titleLink: { classPropertyName: "titleLink", publicName: "titleLink", isSignal: true, isRequired: false, transformFunction: null }, titleRouterLink: { classPropertyName: "titleRouterLink", publicName: "titleRouterLink", isSignal: true, isRequired: false, transformFunction: null }, titleQueryParams: { classPropertyName: "titleQueryParams", publicName: "titleQueryParams", isSignal: true, isRequired: false, transformFunction: null }, titleTooltip: { classPropertyName: "titleTooltip", publicName: "titleTooltip", isSignal: true, isRequired: false, transformFunction: null }, elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, padContent: { classPropertyName: "padContent", publicName: "padContent", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null }, headerStatus: { classPropertyName: "headerStatus", publicName: "headerStatus", isSignal: true, isRequired: false, transformFunction: null }, headerControl: { classPropertyName: "headerControl", publicName: "headerControl", isSignal: true, isRequired: false, transformFunction: null }, headerMenu: { classPropertyName: "headerMenu", publicName: "headerMenu", isSignal: true, isRequired: false, transformFunction: null }, headerMenuTriggerTestId: { classPropertyName: "headerMenuTriggerTestId", publicName: "headerMenuTriggerTestId", isSignal: true, isRequired: false, transformFunction: null }, primaryAction: { classPropertyName: "primaryAction", publicName: "primaryAction", isSignal: true, isRequired: false, transformFunction: null }, secondaryAction: { classPropertyName: "secondaryAction", publicName: "secondaryAction", isSignal: true, isRequired: false, transformFunction: null }, footerLink: { classPropertyName: "footerLink", publicName: "footerLink", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "projectedHeader", first: true, predicate: TnCardHeaderDirective, descendants: true, isSignal: true }, { propertyName: "headerActions", first: true, predicate: TnCardHeaderActionsDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "footerActions", first: true, predicate: TnCardFooterActionsDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3 class=\"tn-card__title\">\n @if (isTitleRouterLink()) {\n <a\n class=\"tn-card__title-link\"\n [routerLink]=\"titleRouterLink()\"\n [queryParams]=\"titleQueryParams()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </a>\n } @else if (titleLink()) {\n <span\n class=\"tn-card__title-link\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick(); $event.preventDefault()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </span>\n } @else {\n {{ title() }}\n }\n\n @if (titleTooltip(); as tooltip) {\n <button\n type=\"button\"\n class=\"tn-card__title-tooltip\"\n aria-label=\"More information\"\n [tnTooltip]=\"tooltip\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </button>\n }\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Projected header actions (escape hatch for custom/permission-gated controls) -->\n @if (headerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n <!-- Projected footer actions (escape hatch for custom/permission-gated controls) -->\n @if (footerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n </div>\n </div>\n }\n</div>", styles: [".tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{display:inline-flex;align-items:center;gap:8px;margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title-link{display:inline-flex;align-items:center;gap:4px;color:inherit;cursor:pointer;text-decoration:none;transition:color .2s ease}.tn-card__title-link:hover{color:var(--tn-primary, #2563eb)}.tn-card__title-link-icon{flex-shrink:0}.tn-card__title-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-card__title-tooltip:hover,.tn-card__title-tooltip:focus-visible{color:var(--tn-fg1, #1f2937)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "component", type: TnSlideToggleComponent, selector: "tn-slide-toggle", inputs: ["labelPosition", "label", "disabled", "required", "color", "testId", "ariaLabel", "ariaLabelledby", "checked"], outputs: ["change", "toggleChange"] }, { kind: "component", type: TnMenuComponent, selector: "tn-menu", inputs: ["items", "contextMenu", "testId"], outputs: ["menuItemClick", "menuOpen", "menuClose"] }, { kind: "directive", type: TnMenuTriggerDirective, selector: "[tnMenuTriggerFor]", inputs: ["tnMenuTriggerFor", "tnMenuPosition"], exportAs: ["tnMenuTrigger"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "directive", type: TnTooltipDirective, selector: "[tnTooltip]", inputs: ["tnTooltip", "tnTooltipPosition", "tnTooltipDisabled", "tnTooltipShowDelay", "tnTooltipHideDelay", "tnTooltipClass"] }] });
|
|
6295
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCardComponent, isStandalone: true, selector: "tn-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, titleLink: { classPropertyName: "titleLink", publicName: "titleLink", isSignal: true, isRequired: false, transformFunction: null }, titleRouterLink: { classPropertyName: "titleRouterLink", publicName: "titleRouterLink", isSignal: true, isRequired: false, transformFunction: null }, titleQueryParams: { classPropertyName: "titleQueryParams", publicName: "titleQueryParams", isSignal: true, isRequired: false, transformFunction: null }, titleTooltip: { classPropertyName: "titleTooltip", publicName: "titleTooltip", isSignal: true, isRequired: false, transformFunction: null }, elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, padContent: { classPropertyName: "padContent", publicName: "padContent", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null }, headerStatus: { classPropertyName: "headerStatus", publicName: "headerStatus", isSignal: true, isRequired: false, transformFunction: null }, headerControl: { classPropertyName: "headerControl", publicName: "headerControl", isSignal: true, isRequired: false, transformFunction: null }, headerMenu: { classPropertyName: "headerMenu", publicName: "headerMenu", isSignal: true, isRequired: false, transformFunction: null }, headerMenuTriggerTestId: { classPropertyName: "headerMenuTriggerTestId", publicName: "headerMenuTriggerTestId", isSignal: true, isRequired: false, transformFunction: null }, primaryAction: { classPropertyName: "primaryAction", publicName: "primaryAction", isSignal: true, isRequired: false, transformFunction: null }, secondaryAction: { classPropertyName: "secondaryAction", publicName: "secondaryAction", isSignal: true, isRequired: false, transformFunction: null }, footerLink: { classPropertyName: "footerLink", publicName: "footerLink", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "projectedHeader", first: true, predicate: TnCardHeaderDirective, descendants: true, isSignal: true }, { propertyName: "headerActions", first: true, predicate: TnCardHeaderActionsDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "footerActions", first: true, predicate: TnCardFooterActionsDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3 class=\"tn-card__title\">\n @if (isTitleRouterLink()) {\n <a\n class=\"tn-card__title-link\"\n [routerLink]=\"titleRouterLink()\"\n [queryParams]=\"titleQueryParams()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </a>\n } @else if (titleLink()) {\n <span\n class=\"tn-card__title-link\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick(); $event.preventDefault()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </span>\n } @else {\n {{ title() }}\n }\n\n @if (titleTooltip(); as tooltip) {\n <button\n type=\"button\"\n class=\"tn-card__title-tooltip\"\n aria-label=\"More information\"\n [tnTooltip]=\"tooltip\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </button>\n }\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Projected header actions (escape hatch for custom/permission-gated controls) -->\n @if (headerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n <!-- Projected footer actions (escape hatch for custom/permission-gated controls) -->\n @if (footerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n </div>\n </div>\n }\n</div>", styles: [".tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{display:inline-flex;align-items:center;gap:8px;margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title-link{display:inline-flex;align-items:center;gap:4px;color:inherit;cursor:pointer;text-decoration:none;transition:color .2s ease}.tn-card__title-link:hover{color:var(--tn-primary, #2563eb)}.tn-card__title-link-icon{flex-shrink:0}.tn-card__title-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-card__title-tooltip:hover,.tn-card__title-tooltip:focus-visible{color:var(--tn-fg1, #1f2937)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "component", type: TnSlideToggleComponent, selector: "tn-slide-toggle", inputs: ["labelPosition", "label", "fullWidth", "disabled", "required", "color", "testId", "ariaLabel", "ariaLabelledby", "checked"], outputs: ["change", "toggleChange"] }, { kind: "component", type: TnMenuComponent, selector: "tn-menu", inputs: ["items", "contextMenu", "testId"], outputs: ["menuItemClick", "menuOpen", "menuClose"] }, { kind: "directive", type: TnMenuTriggerDirective, selector: "[tnMenuTriggerFor]", inputs: ["tnMenuTriggerFor", "tnMenuPosition"], exportAs: ["tnMenuTrigger"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "directive", type: TnTooltipDirective, selector: "[tnTooltip]", inputs: ["tnTooltip", "tnTooltipPosition", "tnTooltipDisabled", "tnTooltipShowDelay", "tnTooltipHideDelay", "tnTooltipClass"] }] });
|
|
6275
6296
|
}
|
|
6276
6297
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, decorators: [{
|
|
6277
6298
|
type: Component,
|
|
@@ -10151,77 +10172,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
10151
10172
|
}, template: "<ng-content />", styles: [".tn-list{display:block;padding:8px 0;margin:0;background-color:var(--tn-bg2);border-radius:4px}.tn-list--dense{padding:4px 0}.tn-list--dense ::ng-deep .tn-list-item{min-height:40px;padding:4px 16px}.tn-list--disabled{opacity:.6;pointer-events:none}::ng-deep .tn-divider{border-top:1px solid var(--tn-lines);margin:0;height:1px}\n"] }]
|
|
10152
10173
|
}], propDecorators: { dense: [{ type: i0.Input, args: [{ isSignal: true, alias: "dense", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
|
|
10153
10174
|
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
this.hasSecondaryTextContent.set(!!(element.querySelector('[tnListItemLine]') ||
|
|
10173
|
-
element.querySelector('[tnListItemSecondary]')));
|
|
10174
|
-
// Check for trailing content
|
|
10175
|
-
this.hasTrailingContent.set(!!element.querySelector('[tnListItemTrailing]'));
|
|
10176
|
-
// Check for primary text directive
|
|
10177
|
-
this.hasPrimaryTextDirective.set(!!(element.querySelector('[tnListItemTitle]') ||
|
|
10178
|
-
element.querySelector('[tnListItemPrimary]')));
|
|
10179
|
-
}
|
|
10180
|
-
hasSecondaryText = computed(() => {
|
|
10181
|
-
return this.hasSecondaryTextContent();
|
|
10182
|
-
}, ...(ngDevMode ? [{ debugName: "hasSecondaryText" }] : []));
|
|
10183
|
-
hasThirdText = computed(() => {
|
|
10184
|
-
// For now, we'll consider third line as having more than one secondary line
|
|
10185
|
-
const element = this.elementRef.nativeElement;
|
|
10186
|
-
const secondaryElements = element.querySelectorAll('[tnListItemLine], [tnListItemSecondary]');
|
|
10187
|
-
return secondaryElements.length > 1;
|
|
10188
|
-
}, ...(ngDevMode ? [{ debugName: "hasThirdText" }] : []));
|
|
10189
|
-
onClick(event) {
|
|
10190
|
-
if (!this.disabled() && this.clickable()) {
|
|
10191
|
-
this.itemClick.emit(event);
|
|
10192
|
-
}
|
|
10193
|
-
}
|
|
10194
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10195
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnListItemComponent, isStandalone: true, selector: "tn-list-item", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, host: { attributes: { "role": "listitem" }, listeners: { "click": "onClick($event)" }, properties: { "class.tn-list-item--disabled": "disabled()", "class.tn-list-item--clickable": "clickable()", "class.tn-list-item--two-line": "hasSecondaryText()", "class.tn-list-item--three-line": "hasThirdText()" }, classAttribute: "tn-list-item" }, ngImport: i0, template: "<div class=\"tn-list-item__content\">\n <!-- Leading icon/avatar slot -->\n @if (hasLeadingContent()) {\n <div class=\"tn-list-item__leading\">\n <ng-content select=\"[tnListIcon], [tnListAvatar]\" />\n </div>\n }\n\n <!-- Text content -->\n <div class=\"tn-list-item__text\">\n <!-- Primary text -->\n <div class=\"tn-list-item__primary-text\">\n <ng-content select=\"[tnListItemTitle], [tnListItemPrimary]\" />\n @if (!hasPrimaryTextDirective()) {\n <ng-content />\n }\n </div>\n\n <!-- Secondary text -->\n @if (hasSecondaryTextContent()) {\n <div class=\"tn-list-item__secondary-text\">\n <ng-content select=\"[tnListItemLine], [tnListItemSecondary]\" />\n </div>\n }\n </div>\n\n <!-- Trailing content slot -->\n @if (hasTrailingContent()) {\n <div class=\"tn-list-item__trailing\">\n <ng-content select=\"[tnListItemTrailing]\" />\n </div>\n }\n</div>", styles: [".tn-list-item{display:block;position:relative;min-height:48px;padding:0;cursor:default;text-decoration:none;color:var(--tn-fg1);transition:background-color .2s ease}.tn-list-item__content{display:flex;align-items:center;padding:8px 16px;min-height:inherit;box-sizing:border-box}.tn-list-item__leading{flex-shrink:0;margin-right:16px;display:flex;align-items:center;justify-content:center}.tn-list-item__text{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}.tn-list-item__primary-text{font-size:1rem;font-weight:400;line-height:1.5;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__secondary-text{font-size:14px;font-weight:400;line-height:1.4;color:var(--tn-fg2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__trailing{flex-shrink:0;margin-left:16px;display:flex;align-items:center}.tn-list-item--clickable{cursor:pointer}.tn-list-item--clickable:hover:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:focus{outline:none;background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:active:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg2)}.tn-list-item--two-line{min-height:64px}.tn-list-item--two-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--two-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3}.tn-list-item--three-line{min-height:88px}.tn-list-item--three-line .tn-list-item__text{align-items:flex-start;padding:8px 0}.tn-list-item--three-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--three-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.tn-list-item--disabled{opacity:.6;cursor:not-allowed;pointer-events:none}::ng-deep [ixListIcon]{width:24px;height:24px;font-size:24px;color:var(--tn-fg2);display:flex;align-items:center;justify-content:center}::ng-deep [ixListAvatar]{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:var(--tn-alt-bg1);color:var(--tn-fg1);display:flex;align-items:center;justify-content:center;font-weight:500}::ng-deep [ixListItemTitle]{font-weight:400}::ng-deep [ixListItemLine]{display:block;margin-top:4px;font-size:14px;color:var(--tn-fg2);line-height:1.4}::ng-deep [ixListItemTrailing]{color:var(--tn-fg2);font-size:14px}\n"] });
|
|
10196
|
-
}
|
|
10197
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemComponent, decorators: [{
|
|
10198
|
-
type: Component,
|
|
10199
|
-
args: [{ selector: 'tn-list-item', standalone: true, imports: [], host: {
|
|
10200
|
-
'class': 'tn-list-item',
|
|
10201
|
-
'[class.tn-list-item--disabled]': 'disabled()',
|
|
10202
|
-
'[class.tn-list-item--clickable]': 'clickable()',
|
|
10203
|
-
'[class.tn-list-item--two-line]': 'hasSecondaryText()',
|
|
10204
|
-
'[class.tn-list-item--three-line]': 'hasThirdText()',
|
|
10205
|
-
'role': 'listitem',
|
|
10206
|
-
'(click)': 'onClick($event)'
|
|
10207
|
-
}, template: "<div class=\"tn-list-item__content\">\n <!-- Leading icon/avatar slot -->\n @if (hasLeadingContent()) {\n <div class=\"tn-list-item__leading\">\n <ng-content select=\"[tnListIcon], [tnListAvatar]\" />\n </div>\n }\n\n <!-- Text content -->\n <div class=\"tn-list-item__text\">\n <!-- Primary text -->\n <div class=\"tn-list-item__primary-text\">\n <ng-content select=\"[tnListItemTitle], [tnListItemPrimary]\" />\n @if (!hasPrimaryTextDirective()) {\n <ng-content />\n }\n </div>\n\n <!-- Secondary text -->\n @if (hasSecondaryTextContent()) {\n <div class=\"tn-list-item__secondary-text\">\n <ng-content select=\"[tnListItemLine], [tnListItemSecondary]\" />\n </div>\n }\n </div>\n\n <!-- Trailing content slot -->\n @if (hasTrailingContent()) {\n <div class=\"tn-list-item__trailing\">\n <ng-content select=\"[tnListItemTrailing]\" />\n </div>\n }\n</div>", styles: [".tn-list-item{display:block;position:relative;min-height:48px;padding:0;cursor:default;text-decoration:none;color:var(--tn-fg1);transition:background-color .2s ease}.tn-list-item__content{display:flex;align-items:center;padding:8px 16px;min-height:inherit;box-sizing:border-box}.tn-list-item__leading{flex-shrink:0;margin-right:16px;display:flex;align-items:center;justify-content:center}.tn-list-item__text{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}.tn-list-item__primary-text{font-size:1rem;font-weight:400;line-height:1.5;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__secondary-text{font-size:14px;font-weight:400;line-height:1.4;color:var(--tn-fg2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__trailing{flex-shrink:0;margin-left:16px;display:flex;align-items:center}.tn-list-item--clickable{cursor:pointer}.tn-list-item--clickable:hover:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:focus{outline:none;background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:active:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg2)}.tn-list-item--two-line{min-height:64px}.tn-list-item--two-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--two-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3}.tn-list-item--three-line{min-height:88px}.tn-list-item--three-line .tn-list-item__text{align-items:flex-start;padding:8px 0}.tn-list-item--three-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--three-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.tn-list-item--disabled{opacity:.6;cursor:not-allowed;pointer-events:none}::ng-deep [ixListIcon]{width:24px;height:24px;font-size:24px;color:var(--tn-fg2);display:flex;align-items:center;justify-content:center}::ng-deep [ixListAvatar]{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:var(--tn-alt-bg1);color:var(--tn-fg1);display:flex;align-items:center;justify-content:center;font-weight:500}::ng-deep [ixListItemTitle]{font-weight:400}::ng-deep [ixListItemLine]{display:block;margin-top:4px;font-size:14px;color:var(--tn-fg2);line-height:1.4}::ng-deep [ixListItemTrailing]{color:var(--tn-fg2);font-size:14px}\n"] }]
|
|
10208
|
-
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
|
|
10209
|
-
|
|
10210
|
-
class TnListSubheaderComponent {
|
|
10211
|
-
inset = input(false, ...(ngDevMode ? [{ debugName: "inset" }] : []));
|
|
10212
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListSubheaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10213
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnListSubheaderComponent, isStandalone: true, selector: "tn-list-subheader", inputs: { inset: { classPropertyName: "inset", publicName: "inset", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "heading", "aria-level": "3" }, properties: { "class.tn-list-subheader--inset": "inset()" }, classAttribute: "tn-list-subheader" }, ngImport: i0, template: "<ng-content />", styles: [":host{display:block;margin:.75rem 16px;font-size:14px;font-weight:600;color:var(--tn-fg2);text-transform:uppercase;letter-spacing:.5px;line-height:1.4}:host-context(.tn-list--dense) :host{font-size:13px}\n"] });
|
|
10214
|
-
}
|
|
10215
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListSubheaderComponent, decorators: [{
|
|
10216
|
-
type: Component,
|
|
10217
|
-
args: [{ selector: 'tn-list-subheader', standalone: true, imports: [], host: {
|
|
10218
|
-
'class': 'tn-list-subheader',
|
|
10219
|
-
'[class.tn-list-subheader--inset]': 'inset()',
|
|
10220
|
-
'role': 'heading',
|
|
10221
|
-
'aria-level': '3'
|
|
10222
|
-
}, template: "<ng-content />", styles: [":host{display:block;margin:.75rem 16px;font-size:14px;font-weight:600;color:var(--tn-fg2);text-transform:uppercase;letter-spacing:.5px;line-height:1.4}:host-context(.tn-list--dense) :host{font-size:13px}\n"] }]
|
|
10223
|
-
}], propDecorators: { inset: [{ type: i0.Input, args: [{ isSignal: true, alias: "inset", required: false }] }] } });
|
|
10224
|
-
|
|
10175
|
+
/*
|
|
10176
|
+
* Content directives for `tn-list-item` / `tn-list-option`.
|
|
10177
|
+
*
|
|
10178
|
+
* Each of these must be imported by the component whose template writes the
|
|
10179
|
+
* attribute. `tn-list-item` renders its leading, secondary and trailing slots
|
|
10180
|
+
* only when a matching directive *instance* is present, and a directive applies
|
|
10181
|
+
* only in the template that declares it — importing the list-item component
|
|
10182
|
+
* does not bring these into scope. An attribute written without its directive
|
|
10183
|
+
* imported is inert: the slot stays closed and the content silently disappears.
|
|
10184
|
+
* `[tnListItemTitle]` and `[tnListItemPrimary]` are the exception; their slot is
|
|
10185
|
+
* ungated, so they project either way (they still need importing for the class
|
|
10186
|
+
* and styling).
|
|
10187
|
+
*/
|
|
10188
|
+
/**
|
|
10189
|
+
* Leading icon of a list row.
|
|
10190
|
+
*
|
|
10191
|
+
* Requires importing `TnListIconDirective`, or the leading slot never renders.
|
|
10192
|
+
*/
|
|
10225
10193
|
class TnListIconDirective {
|
|
10226
10194
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
10227
10195
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListIconDirective, isStandalone: true, selector: "[tnListIcon]", host: { classAttribute: "tn-list-icon" }, ngImport: i0 });
|
|
@@ -10236,6 +10204,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
10236
10204
|
}
|
|
10237
10205
|
}]
|
|
10238
10206
|
}] });
|
|
10207
|
+
/**
|
|
10208
|
+
* Leading avatar of a list row.
|
|
10209
|
+
*
|
|
10210
|
+
* Requires importing `TnListAvatarDirective`, or the leading slot never renders.
|
|
10211
|
+
*/
|
|
10239
10212
|
class TnListAvatarDirective {
|
|
10240
10213
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListAvatarDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
10241
10214
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListAvatarDirective, isStandalone: true, selector: "[tnListAvatar]", host: { classAttribute: "tn-list-avatar" }, ngImport: i0 });
|
|
@@ -10250,6 +10223,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
10250
10223
|
}
|
|
10251
10224
|
}]
|
|
10252
10225
|
}] });
|
|
10226
|
+
/**
|
|
10227
|
+
* Primary text of a list row. Projects whether or not this directive is
|
|
10228
|
+
* imported — the primary slot is ungated — but import it for the styling.
|
|
10229
|
+
*/
|
|
10253
10230
|
class TnListItemTitleDirective {
|
|
10254
10231
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
10255
10232
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListItemTitleDirective, isStandalone: true, selector: "[tnListItemTitle]", host: { classAttribute: "tn-list-item-title" }, ngImport: i0 });
|
|
@@ -10264,6 +10241,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
10264
10241
|
}
|
|
10265
10242
|
}]
|
|
10266
10243
|
}] });
|
|
10244
|
+
/**
|
|
10245
|
+
* Secondary line of a list row. Two or more make the row three-line.
|
|
10246
|
+
*
|
|
10247
|
+
* Requires importing `TnListItemLineDirective`, or the secondary slot never
|
|
10248
|
+
* renders and the row is not marked as multi-line.
|
|
10249
|
+
*/
|
|
10267
10250
|
class TnListItemLineDirective {
|
|
10268
10251
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemLineDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
10269
10252
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListItemLineDirective, isStandalone: true, selector: "[tnListItemLine]", host: { classAttribute: "tn-list-item-line" }, ngImport: i0 });
|
|
@@ -10278,6 +10261,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
10278
10261
|
}
|
|
10279
10262
|
}]
|
|
10280
10263
|
}] });
|
|
10264
|
+
/**
|
|
10265
|
+
* Primary text of a list row. Projects whether or not this directive is
|
|
10266
|
+
* imported — the primary slot is ungated — but import it for the styling.
|
|
10267
|
+
*/
|
|
10281
10268
|
class TnListItemPrimaryDirective {
|
|
10282
10269
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemPrimaryDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
10283
10270
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListItemPrimaryDirective, isStandalone: true, selector: "[tnListItemPrimary]", host: { classAttribute: "tn-list-item-primary" }, ngImport: i0 });
|
|
@@ -10292,6 +10279,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
10292
10279
|
}
|
|
10293
10280
|
}]
|
|
10294
10281
|
}] });
|
|
10282
|
+
/**
|
|
10283
|
+
* Secondary text of a list row.
|
|
10284
|
+
*
|
|
10285
|
+
* Requires importing `TnListItemSecondaryDirective`, or the secondary slot
|
|
10286
|
+
* never renders and the row is not marked as multi-line.
|
|
10287
|
+
*/
|
|
10295
10288
|
class TnListItemSecondaryDirective {
|
|
10296
10289
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemSecondaryDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
10297
10290
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListItemSecondaryDirective, isStandalone: true, selector: "[tnListItemSecondary]", host: { classAttribute: "tn-list-item-secondary" }, ngImport: i0 });
|
|
@@ -10306,6 +10299,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
10306
10299
|
}
|
|
10307
10300
|
}]
|
|
10308
10301
|
}] });
|
|
10302
|
+
/**
|
|
10303
|
+
* Trailing content of a list row — a control, a badge, a chevron.
|
|
10304
|
+
*
|
|
10305
|
+
* Requires importing `TnListItemTrailingDirective`, or the trailing slot never
|
|
10306
|
+
* renders.
|
|
10307
|
+
*/
|
|
10309
10308
|
class TnListItemTrailingDirective {
|
|
10310
10309
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemTrailingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
10311
10310
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListItemTrailingDirective, isStandalone: true, selector: "[tnListItemTrailing]", host: { classAttribute: "tn-list-item-trailing" }, ngImport: i0 });
|
|
@@ -10336,6 +10335,108 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
10336
10335
|
}]
|
|
10337
10336
|
}] });
|
|
10338
10337
|
|
|
10338
|
+
/**
|
|
10339
|
+
* A single row of a `tn-list`.
|
|
10340
|
+
*
|
|
10341
|
+
* **The content directives must be imported by the component that declares the
|
|
10342
|
+
* row.** Every slot but the primary text is rendered only when a matching
|
|
10343
|
+
* directive *instance* is found, and directives apply only in the template that
|
|
10344
|
+
* declares them — importing `TnListItemComponent` alone does not bring them
|
|
10345
|
+
* into scope. Write `<span tnListIcon>` without `TnListIconDirective` in that
|
|
10346
|
+
* component's `imports` and the attribute is inert: no instance, so the leading
|
|
10347
|
+
* slot never renders and the icon silently disappears. The same holds for
|
|
10348
|
+
* `[tnListAvatar]`, `[tnListItemLine]`, `[tnListItemSecondary]` and
|
|
10349
|
+
* `[tnListItemTrailing]`. Import them alongside the component:
|
|
10350
|
+
*
|
|
10351
|
+
* ```ts
|
|
10352
|
+
* import {
|
|
10353
|
+
* TnListItemComponent,
|
|
10354
|
+
* TnListIconDirective,
|
|
10355
|
+
* TnListItemLineDirective,
|
|
10356
|
+
* TnListItemTrailingDirective,
|
|
10357
|
+
* } from '@truenas/ui-components';
|
|
10358
|
+
* ```
|
|
10359
|
+
*
|
|
10360
|
+
* The primary-text slot is the deliberate exception — it has no gate, so
|
|
10361
|
+
* `[tnListItemTitle]` and `[tnListItemPrimary]` render either way. See the
|
|
10362
|
+
* comment in `list-item.component.html` for why the asymmetry is there.
|
|
10363
|
+
*/
|
|
10364
|
+
class TnListItemComponent {
|
|
10365
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
10366
|
+
clickable = input(false, ...(ngDevMode ? [{ debugName: "clickable" }] : []));
|
|
10367
|
+
/** Compact row: smaller minimum height and tighter vertical padding. */
|
|
10368
|
+
dense = input(false, ...(ngDevMode ? [{ debugName: "dense" }] : []));
|
|
10369
|
+
/**
|
|
10370
|
+
* Lets the primary and secondary text wrap onto multiple lines instead of
|
|
10371
|
+
* being truncated with an ellipsis. Use for rows whose content is a path, a
|
|
10372
|
+
* sentence, or anything else that should stay fully readable.
|
|
10373
|
+
*/
|
|
10374
|
+
wrap = input(false, ...(ngDevMode ? [{ debugName: "wrap" }] : []));
|
|
10375
|
+
itemClick = output();
|
|
10376
|
+
// Content queries, not a DOM query: projected content is instantiated in the
|
|
10377
|
+
// *declaring* view, so these resolve even while the matching <ng-content> slot
|
|
10378
|
+
// is still hidden behind the flag they feed. A `querySelector` cannot — the
|
|
10379
|
+
// element is not in the DOM until its slot renders, and its slot does not
|
|
10380
|
+
// render until the flag is set, so every gated slot stayed empty forever.
|
|
10381
|
+
//
|
|
10382
|
+
// These match directive *instances* while the slots they gate project by
|
|
10383
|
+
// *attribute*, so the two disagree for a consumer who writes the attribute
|
|
10384
|
+
// without importing the directive: nothing matches here, the slot stays
|
|
10385
|
+
// closed, and the content vanishes. The primary-text slot avoids this by not
|
|
10386
|
+
// gating at all — see the comment in the template. The side slots cannot
|
|
10387
|
+
// follow it: their wrapper elements carry their own gutters, so rendering
|
|
10388
|
+
// them unconditionally would leave empty ones spacing out every row, and the
|
|
10389
|
+
// --two-line/--three-line host classes need the counts regardless. So the
|
|
10390
|
+
// requirement is documented on the class and on each directive instead. It
|
|
10391
|
+
// cannot be checked at runtime: content that never projects is never in the
|
|
10392
|
+
// DOM to be found.
|
|
10393
|
+
leadingIcons = contentChildren(TnListIconDirective, { ...(ngDevMode ? { debugName: "leadingIcons" } : {}), descendants: true });
|
|
10394
|
+
leadingAvatars = contentChildren(TnListAvatarDirective, { ...(ngDevMode ? { debugName: "leadingAvatars" } : {}), descendants: true });
|
|
10395
|
+
secondaryLines = contentChildren(TnListItemLineDirective, { ...(ngDevMode ? { debugName: "secondaryLines" } : {}), descendants: true });
|
|
10396
|
+
secondaryTexts = contentChildren(TnListItemSecondaryDirective, { ...(ngDevMode ? { debugName: "secondaryTexts" } : {}), descendants: true });
|
|
10397
|
+
trailing = contentChildren(TnListItemTrailingDirective, { ...(ngDevMode ? { debugName: "trailing" } : {}), descendants: true });
|
|
10398
|
+
hasLeadingContent = computed(() => this.leadingIcons().length > 0 || this.leadingAvatars().length > 0, ...(ngDevMode ? [{ debugName: "hasLeadingContent" }] : []));
|
|
10399
|
+
hasTrailingContent = computed(() => this.trailing().length > 0, ...(ngDevMode ? [{ debugName: "hasTrailingContent" }] : []));
|
|
10400
|
+
hasSecondaryText = computed(() => this.secondaryLines().length > 0 || this.secondaryTexts().length > 0, ...(ngDevMode ? [{ debugName: "hasSecondaryText" }] : []));
|
|
10401
|
+
hasThirdText = computed(() => this.secondaryLines().length + this.secondaryTexts().length > 1, ...(ngDevMode ? [{ debugName: "hasThirdText" }] : []));
|
|
10402
|
+
onClick(event) {
|
|
10403
|
+
if (!this.disabled() && this.clickable()) {
|
|
10404
|
+
this.itemClick.emit(event);
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10407
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10408
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnListItemComponent, isStandalone: true, selector: "tn-list-item", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null }, dense: { classPropertyName: "dense", publicName: "dense", isSignal: true, isRequired: false, transformFunction: null }, wrap: { classPropertyName: "wrap", publicName: "wrap", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, host: { attributes: { "role": "listitem" }, listeners: { "click": "onClick($event)" }, properties: { "class.tn-list-item--disabled": "disabled()", "class.tn-list-item--clickable": "clickable()", "class.tn-list-item--dense": "dense()", "class.tn-list-item--wrap": "wrap()", "class.tn-list-item--two-line": "hasSecondaryText()", "class.tn-list-item--three-line": "hasThirdText()" }, classAttribute: "tn-list-item" }, queries: [{ propertyName: "leadingIcons", predicate: TnListIconDirective, descendants: true, isSignal: true }, { propertyName: "leadingAvatars", predicate: TnListAvatarDirective, descendants: true, isSignal: true }, { propertyName: "secondaryLines", predicate: TnListItemLineDirective, descendants: true, isSignal: true }, { propertyName: "secondaryTexts", predicate: TnListItemSecondaryDirective, descendants: true, isSignal: true }, { propertyName: "trailing", predicate: TnListItemTrailingDirective, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-list-item__content\">\n <!-- Leading icon/avatar slot -->\n @if (hasLeadingContent()) {\n <div class=\"tn-list-item__leading\">\n <ng-content select=\"[tnListIcon], [tnListAvatar]\" />\n </div>\n }\n\n <!-- Text content -->\n <div class=\"tn-list-item__text\">\n <!--\n Primary text. The catch-all slot needs no guard: a projected node is\n assigned to exactly one slot, so anything carrying [tnListItemTitle] or\n [tnListItemPrimary] lands in the slot above and can never reach the\n catch-all. Gating this on a directive query would instead key projection\n and rendering off two different things \u2014 the slot matches the attribute,\n a contentChildren query matches the directive instance \u2014 so an attribute\n written without its directive imported would toggle the guard while\n projecting exactly as before.\n\n The leading, secondary and trailing slots below are still gated that way,\n deliberately: their wrappers carry gutters that would space out every row\n if rendered empty. The cost is that they require the directive to be\n imported, which is documented on the component and on each directive.\n -->\n <div class=\"tn-list-item__primary-text\">\n <ng-content select=\"[tnListItemTitle], [tnListItemPrimary]\" />\n <ng-content />\n </div>\n\n <!-- Secondary text -->\n @if (hasSecondaryText()) {\n <div class=\"tn-list-item__secondary-text\">\n <ng-content select=\"[tnListItemLine], [tnListItemSecondary]\" />\n </div>\n }\n </div>\n\n <!-- Trailing content slot -->\n @if (hasTrailingContent()) {\n <div class=\"tn-list-item__trailing\">\n <ng-content select=\"[tnListItemTrailing]\" />\n </div>\n }\n</div>", styles: [".tn-list-item{display:block;position:relative;min-height:48px;padding:0;cursor:default;text-decoration:none;color:var(--tn-fg1);transition:background-color .2s ease}.tn-list-item__content{display:flex;align-items:center;padding:8px 16px;min-height:inherit;box-sizing:border-box}.tn-list-item__leading{flex-shrink:0;margin-right:16px;display:flex;align-items:center;justify-content:center}.tn-list-item__text{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}.tn-list-item__primary-text{font-size:1rem;font-weight:400;line-height:1.5;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__secondary-text{font-size:14px;font-weight:400;line-height:1.4;color:var(--tn-fg2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__trailing{flex-shrink:0;margin-left:16px;display:flex;align-items:center}.tn-list-item--clickable{cursor:pointer}.tn-list-item--clickable:hover:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:focus{outline:none;background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:active:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg2)}.tn-list-item--two-line{min-height:64px}.tn-list-item--two-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--two-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3}.tn-list-item--three-line{min-height:88px}.tn-list-item--three-line .tn-list-item__text{align-items:flex-start;padding:8px 0}.tn-list-item--three-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--three-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.tn-list-item--dense{min-height:32px}.tn-list-item--dense .tn-list-item__content{padding:4px 16px}.tn-list-item--dense .tn-list-item__leading{margin-right:8px}.tn-list-item--dense .tn-list-item__trailing{margin-left:8px}.tn-list-item--wrap .tn-list-item__primary-text,.tn-list-item--wrap .tn-list-item__secondary-text{overflow:visible;text-overflow:clip;white-space:normal}.tn-list-item--disabled{opacity:.6;cursor:not-allowed;pointer-events:none}::ng-deep [tnListIcon]{width:24px;height:24px;font-size:24px;color:var(--tn-fg2);display:flex;align-items:center;justify-content:center}::ng-deep [tnListAvatar]{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:var(--tn-alt-bg1);color:var(--tn-fg1);display:flex;align-items:center;justify-content:center;font-weight:500}::ng-deep [tnListItemTitle]{font-weight:400}::ng-deep [tnListItemLine]{display:block;margin-top:4px;font-size:14px;color:var(--tn-fg2);line-height:1.4}::ng-deep [tnListItemTrailing]{color:var(--tn-fg2);font-size:14px}\n"] });
|
|
10409
|
+
}
|
|
10410
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemComponent, decorators: [{
|
|
10411
|
+
type: Component,
|
|
10412
|
+
args: [{ selector: 'tn-list-item', standalone: true, imports: [], host: {
|
|
10413
|
+
'class': 'tn-list-item',
|
|
10414
|
+
'[class.tn-list-item--disabled]': 'disabled()',
|
|
10415
|
+
'[class.tn-list-item--clickable]': 'clickable()',
|
|
10416
|
+
'[class.tn-list-item--dense]': 'dense()',
|
|
10417
|
+
'[class.tn-list-item--wrap]': 'wrap()',
|
|
10418
|
+
'[class.tn-list-item--two-line]': 'hasSecondaryText()',
|
|
10419
|
+
'[class.tn-list-item--three-line]': 'hasThirdText()',
|
|
10420
|
+
'role': 'listitem',
|
|
10421
|
+
'(click)': 'onClick($event)'
|
|
10422
|
+
}, template: "<div class=\"tn-list-item__content\">\n <!-- Leading icon/avatar slot -->\n @if (hasLeadingContent()) {\n <div class=\"tn-list-item__leading\">\n <ng-content select=\"[tnListIcon], [tnListAvatar]\" />\n </div>\n }\n\n <!-- Text content -->\n <div class=\"tn-list-item__text\">\n <!--\n Primary text. The catch-all slot needs no guard: a projected node is\n assigned to exactly one slot, so anything carrying [tnListItemTitle] or\n [tnListItemPrimary] lands in the slot above and can never reach the\n catch-all. Gating this on a directive query would instead key projection\n and rendering off two different things \u2014 the slot matches the attribute,\n a contentChildren query matches the directive instance \u2014 so an attribute\n written without its directive imported would toggle the guard while\n projecting exactly as before.\n\n The leading, secondary and trailing slots below are still gated that way,\n deliberately: their wrappers carry gutters that would space out every row\n if rendered empty. The cost is that they require the directive to be\n imported, which is documented on the component and on each directive.\n -->\n <div class=\"tn-list-item__primary-text\">\n <ng-content select=\"[tnListItemTitle], [tnListItemPrimary]\" />\n <ng-content />\n </div>\n\n <!-- Secondary text -->\n @if (hasSecondaryText()) {\n <div class=\"tn-list-item__secondary-text\">\n <ng-content select=\"[tnListItemLine], [tnListItemSecondary]\" />\n </div>\n }\n </div>\n\n <!-- Trailing content slot -->\n @if (hasTrailingContent()) {\n <div class=\"tn-list-item__trailing\">\n <ng-content select=\"[tnListItemTrailing]\" />\n </div>\n }\n</div>", styles: [".tn-list-item{display:block;position:relative;min-height:48px;padding:0;cursor:default;text-decoration:none;color:var(--tn-fg1);transition:background-color .2s ease}.tn-list-item__content{display:flex;align-items:center;padding:8px 16px;min-height:inherit;box-sizing:border-box}.tn-list-item__leading{flex-shrink:0;margin-right:16px;display:flex;align-items:center;justify-content:center}.tn-list-item__text{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}.tn-list-item__primary-text{font-size:1rem;font-weight:400;line-height:1.5;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__secondary-text{font-size:14px;font-weight:400;line-height:1.4;color:var(--tn-fg2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__trailing{flex-shrink:0;margin-left:16px;display:flex;align-items:center}.tn-list-item--clickable{cursor:pointer}.tn-list-item--clickable:hover:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:focus{outline:none;background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:active:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg2)}.tn-list-item--two-line{min-height:64px}.tn-list-item--two-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--two-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3}.tn-list-item--three-line{min-height:88px}.tn-list-item--three-line .tn-list-item__text{align-items:flex-start;padding:8px 0}.tn-list-item--three-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--three-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.tn-list-item--dense{min-height:32px}.tn-list-item--dense .tn-list-item__content{padding:4px 16px}.tn-list-item--dense .tn-list-item__leading{margin-right:8px}.tn-list-item--dense .tn-list-item__trailing{margin-left:8px}.tn-list-item--wrap .tn-list-item__primary-text,.tn-list-item--wrap .tn-list-item__secondary-text{overflow:visible;text-overflow:clip;white-space:normal}.tn-list-item--disabled{opacity:.6;cursor:not-allowed;pointer-events:none}::ng-deep [tnListIcon]{width:24px;height:24px;font-size:24px;color:var(--tn-fg2);display:flex;align-items:center;justify-content:center}::ng-deep [tnListAvatar]{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:var(--tn-alt-bg1);color:var(--tn-fg1);display:flex;align-items:center;justify-content:center;font-weight:500}::ng-deep [tnListItemTitle]{font-weight:400}::ng-deep [tnListItemLine]{display:block;margin-top:4px;font-size:14px;color:var(--tn-fg2);line-height:1.4}::ng-deep [tnListItemTrailing]{color:var(--tn-fg2);font-size:14px}\n"] }]
|
|
10423
|
+
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], dense: [{ type: i0.Input, args: [{ isSignal: true, alias: "dense", required: false }] }], wrap: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrap", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], leadingIcons: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnListIconDirective), { ...{ descendants: true }, isSignal: true }] }], leadingAvatars: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnListAvatarDirective), { ...{ descendants: true }, isSignal: true }] }], secondaryLines: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnListItemLineDirective), { ...{ descendants: true }, isSignal: true }] }], secondaryTexts: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnListItemSecondaryDirective), { ...{ descendants: true }, isSignal: true }] }], trailing: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnListItemTrailingDirective), { ...{ descendants: true }, isSignal: true }] }] } });
|
|
10424
|
+
|
|
10425
|
+
class TnListSubheaderComponent {
|
|
10426
|
+
inset = input(false, ...(ngDevMode ? [{ debugName: "inset" }] : []));
|
|
10427
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListSubheaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10428
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnListSubheaderComponent, isStandalone: true, selector: "tn-list-subheader", inputs: { inset: { classPropertyName: "inset", publicName: "inset", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "heading", "aria-level": "3" }, properties: { "class.tn-list-subheader--inset": "inset()" }, classAttribute: "tn-list-subheader" }, ngImport: i0, template: "<ng-content />", styles: [":host{display:block;margin:.75rem 16px;font-size:14px;font-weight:600;color:var(--tn-fg2);text-transform:uppercase;letter-spacing:.5px;line-height:1.4}:host-context(.tn-list--dense) :host{font-size:13px}\n"] });
|
|
10429
|
+
}
|
|
10430
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListSubheaderComponent, decorators: [{
|
|
10431
|
+
type: Component,
|
|
10432
|
+
args: [{ selector: 'tn-list-subheader', standalone: true, imports: [], host: {
|
|
10433
|
+
'class': 'tn-list-subheader',
|
|
10434
|
+
'[class.tn-list-subheader--inset]': 'inset()',
|
|
10435
|
+
'role': 'heading',
|
|
10436
|
+
'aria-level': '3'
|
|
10437
|
+
}, template: "<ng-content />", styles: [":host{display:block;margin:.75rem 16px;font-size:14px;font-weight:600;color:var(--tn-fg2);text-transform:uppercase;letter-spacing:.5px;line-height:1.4}:host-context(.tn-list--dense) :host{font-size:13px}\n"] }]
|
|
10438
|
+
}], propDecorators: { inset: [{ type: i0.Input, args: [{ isSignal: true, alias: "inset", required: false }] }] } });
|
|
10439
|
+
|
|
10339
10440
|
class TnDividerComponent {
|
|
10340
10441
|
vertical = input(false, ...(ngDevMode ? [{ debugName: "vertical" }] : []));
|
|
10341
10442
|
inset = input(false, ...(ngDevMode ? [{ debugName: "inset" }] : []));
|
|
@@ -10556,7 +10657,7 @@ class TnEmptyComponent {
|
|
|
10556
10657
|
return this.size() === 'compact' ? 'lg' : 'xl';
|
|
10557
10658
|
}, ...(ngDevMode ? [{ debugName: "iconSizePreset" }] : []));
|
|
10558
10659
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnEmptyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10559
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnEmptyComponent, isStandalone: true, selector: "tn-empty", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconLibrary: { classPropertyName: "iconLibrary", publicName: "iconLibrary", isSignal: true, isRequired: false, transformFunction: null }, iconSize: { classPropertyName: "iconSize", publicName: "iconSize", isSignal: true, isRequired: false, transformFunction: null }, actionText: { classPropertyName: "actionText", publicName: "actionText", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onAction: "onAction" }, host: { attributes: { "role": "status" }, properties: { "class.tn-empty--compact": "size() === \"compact\"", "class.tn-empty--bordered": "bordered()" }, classAttribute: "tn-empty" }, ngImport: i0, template: "@if (icon()) {\n <div class=\"tn-empty__icon\">\n <tn-icon\n aria-hidden=\"true\"\n [name]=\"icon()!\"\n [library]=\"iconLibrary()\"\n [size]=\"iconSizePreset()\"\n [customSize]=\"iconSize()\"\n />\n </div>\n}\n\n<div class=\"tn-empty__title\">\n {{ title() }}\n</div>\n\n@if (description()) {\n <div class=\"tn-empty__description\">\n {{ description() }}\n </div>\n}\n\n@if (hasAction()) {\n <div class=\"tn-empty__action\">\n <tn-button\n color=\"primary\"\n variant=\"outline\"\n [label]=\"actionText()!\"\n (onClick)=\"onAction.emit()\"\n />\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:8px;padding:48px 24px;border-radius:8px}:host(.tn-empty--bordered){border:1px solid var(--tn-lines)}:host(.tn-empty--compact){padding:24px 16px}:host(.tn-empty--compact) .tn-empty__title{font-size:1rem}:host(.tn-empty--compact) .tn-empty__description{font-size:.8125rem}:host(.tn-empty--compact) .tn-empty__icon{margin-bottom:4px}:host(.tn-empty--compact) .tn-empty__action{margin-top:4px}.tn-empty__icon{color:var(--tn-fg2, #6b7280);margin-bottom:8px}.tn-empty__title{font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #e5e7eb);line-height:1.4}.tn-empty__description{font-size:1rem;color:var(--tn-fg2, #6b7280);line-height:1.5;max-width:420px}.tn-empty__action{margin-top:8px}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }] });
|
|
10660
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnEmptyComponent, isStandalone: true, selector: "tn-empty", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconLibrary: { classPropertyName: "iconLibrary", publicName: "iconLibrary", isSignal: true, isRequired: false, transformFunction: null }, iconSize: { classPropertyName: "iconSize", publicName: "iconSize", isSignal: true, isRequired: false, transformFunction: null }, actionText: { classPropertyName: "actionText", publicName: "actionText", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onAction: "onAction" }, host: { attributes: { "role": "status" }, properties: { "class.tn-empty--compact": "size() === \"compact\"", "class.tn-empty--bordered": "bordered()" }, classAttribute: "tn-empty" }, ngImport: i0, template: "@if (icon()) {\n <div class=\"tn-empty__icon\">\n <tn-icon\n aria-hidden=\"true\"\n [name]=\"icon()!\"\n [library]=\"iconLibrary()\"\n [size]=\"iconSizePreset()\"\n [customSize]=\"iconSize()\"\n />\n </div>\n}\n\n<div class=\"tn-empty__title\">\n {{ title() }}\n</div>\n\n@if (description()) {\n <div class=\"tn-empty__description\">\n {{ description() }}\n </div>\n}\n\n@if (hasAction()) {\n <div class=\"tn-empty__action\">\n <tn-button\n color=\"primary\"\n variant=\"outline\"\n [label]=\"actionText()!\"\n (onClick)=\"onAction.emit()\"\n />\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:8px;padding:48px 24px;border-radius:8px}:host(.tn-empty--bordered){border:1px solid var(--tn-lines)}:host(.tn-empty--compact){padding:24px 16px}:host(.tn-empty--compact) .tn-empty__title{font-size:1rem}:host(.tn-empty--compact) .tn-empty__description{font-size:.8125rem}:host(.tn-empty--compact) .tn-empty__icon{margin-bottom:4px}:host(.tn-empty--compact) .tn-empty__action{margin-top:4px}.tn-empty__icon{color:var(--tn-fg2, #6b7280);margin-bottom:8px}.tn-empty__title{font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #e5e7eb);line-height:1.4}.tn-empty__description{font-size:1rem;color:var(--tn-fg2, #6b7280);line-height:1.5;max-width:420px}.tn-empty__action{margin-top:8px}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }] });
|
|
10560
10661
|
}
|
|
10561
10662
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnEmptyComponent, decorators: [{
|
|
10562
10663
|
type: Component,
|
|
@@ -13830,43 +13931,251 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
13830
13931
|
}, template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\n <defs>\n <linearGradient id=\"fadeToPrimary\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\">\n <stop offset=\"0%\" stop-color=\"transparent\" />\n <stop stop-color=\"transparent\" [attr.offset]=\"gradientTransitionStart() + '%'\" />\n <stop offset=\"100%\" [attr.stop-color]=\"progressBarColor()\" />\n </linearGradient>\n </defs>\n\n <!-- Background bar -->\n <rect x=\"50\" fill=\"rgba(0,0,0,0.1)\" [attr.y]=\"height() / 2 - height() / 4\" [attr.width]=\"width() - 100\" [attr.height]=\"height() / 2\" [attr.rx]=\"height() / 4\" />\n\n <!-- Fill bar -->\n <rect\n x=\"50\"\n fill=\"url(#fadeToPrimary)\"\n [attr.y]=\"height() / 2 - height() / 4\"\n [attr.width]=\"fill()\"\n [attr.height]=\"height() / 2\"\n [attr.rx]=\"height() / 4\"\n />\n\n <!-- Particle canvas -->\n <foreignObject x=\"0\" y=\"0\" [attr.width]=\"width()\" [attr.height]=\"height()\">\n <canvas #canvas style=\"pointer-events: none;\" [attr.width]=\"width()\" [attr.height]=\"height()\"></canvas>\n </foreignObject>\n</svg>\n", styles: [":host{display:block}\n"] }]
|
|
13831
13932
|
}], propDecorators: { speed: [{ type: i0.Input, args: [{ isSignal: true, alias: "speed", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], canvasRef: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }] } });
|
|
13832
13933
|
|
|
13934
|
+
/**
|
|
13935
|
+
* Day-granularity date helpers shared by the calendar views.
|
|
13936
|
+
*
|
|
13937
|
+
* The calendar deals in calendar days, but the dates a caller binds are ordinary
|
|
13938
|
+
* `Date`s and routinely carry a time of day — `new Date()` for a default, or a value
|
|
13939
|
+
* round-tripped through an API. Comparing those against the midnight dates the grid
|
|
13940
|
+
* builds gives answers that are off by up to a day, so every comparison the calendar
|
|
13941
|
+
* makes goes through here.
|
|
13942
|
+
*/
|
|
13943
|
+
/**
|
|
13944
|
+
* Orders two dates by calendar day: negative when `a` falls on an earlier day than `b`,
|
|
13945
|
+
* zero on the same day, positive when later. Time of day is ignored on both sides.
|
|
13946
|
+
*/
|
|
13947
|
+
function compareDays(a, b) {
|
|
13948
|
+
return (a.getFullYear() - b.getFullYear()
|
|
13949
|
+
|| a.getMonth() - b.getMonth()
|
|
13950
|
+
|| a.getDate() - b.getDate());
|
|
13951
|
+
}
|
|
13952
|
+
/** Whether two dates fall on the same calendar day, ignoring time of day. */
|
|
13953
|
+
function isSameDay(a, b) {
|
|
13954
|
+
return compareDays(a, b) === 0;
|
|
13955
|
+
}
|
|
13956
|
+
/** Stable per-day key, for lookups and change tracking. */
|
|
13957
|
+
function dateKey(date) {
|
|
13958
|
+
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
13959
|
+
}
|
|
13960
|
+
/**
|
|
13961
|
+
* `YYYY-MM-DD` for the day this date falls on, built from its local parts.
|
|
13962
|
+
*
|
|
13963
|
+
* Rendered onto each day cell as `data-tn-date`, which gives tests a handle on a day
|
|
13964
|
+
* that survives translation — cell text is formatted for the locale, right down to the
|
|
13965
|
+
* numerals, so matching on it only works in English.
|
|
13966
|
+
*
|
|
13967
|
+
* Deliberately not `toISOString()`: that converts to UTC first, so anywhere east or west
|
|
13968
|
+
* of Greenwich it can name the day before or after the one shown.
|
|
13969
|
+
*/
|
|
13970
|
+
function isoDateString(date) {
|
|
13971
|
+
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
|
13972
|
+
const day = `${date.getDate()}`.padStart(2, '0');
|
|
13973
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
13974
|
+
}
|
|
13975
|
+
/**
|
|
13976
|
+
* Adds whole months, clamping to the last day when the target month is shorter, so
|
|
13977
|
+
* navigating never silently lands in the month after the one asked for. `new Date` rolls
|
|
13978
|
+
* 31 January + 1 month over into early March; this returns 28 (or 29) February.
|
|
13979
|
+
*/
|
|
13980
|
+
function addMonths(date, delta) {
|
|
13981
|
+
return atMonthAndDay(date, date.getFullYear(), date.getMonth() + delta);
|
|
13982
|
+
}
|
|
13983
|
+
/**
|
|
13984
|
+
* The same month and day in another year, clamped the same way — 29 February in a
|
|
13985
|
+
* non-leap year is 28 February, not 1 March. Without the clamp, paging the year grid off
|
|
13986
|
+
* a leap day quietly changes the month as well as the year.
|
|
13987
|
+
*/
|
|
13988
|
+
function withYear(date, year) {
|
|
13989
|
+
return atMonthAndDay(date, year, date.getMonth());
|
|
13990
|
+
}
|
|
13991
|
+
/** Keeps `date`'s day-of-month where the target month is long enough to hold it. */
|
|
13992
|
+
function atMonthAndDay(date, year, month) {
|
|
13993
|
+
const lastDay = new Date(year, month + 1, 0).getDate();
|
|
13994
|
+
return new Date(year, month, Math.min(date.getDate(), lastDay));
|
|
13995
|
+
}
|
|
13996
|
+
/** How many years one page of the year grid shows. */
|
|
13997
|
+
const YEARS_PER_PAGE = 24;
|
|
13998
|
+
/**
|
|
13999
|
+
* The first year of the page containing `year`.
|
|
14000
|
+
*
|
|
14001
|
+
* Pages are fixed blocks counted from year 0, not windows centred on the year asked
|
|
14002
|
+
* about — 2020 sits near the top of 2016–2039 rather than in the middle of 2008–2031.
|
|
14003
|
+
* The year view and the header both need this answer and must agree on it, or the
|
|
14004
|
+
* header labels a span the grid isn't showing.
|
|
14005
|
+
*/
|
|
14006
|
+
function yearPageStart(year) {
|
|
14007
|
+
return Math.floor(year / YEARS_PER_PAGE) * YEARS_PER_PAGE;
|
|
14008
|
+
}
|
|
14009
|
+
/**
|
|
14010
|
+
* The day a week starts on in the given locale, as a `Date.getDay()` index — 0 for
|
|
14011
|
+
* Sunday through 6 for Saturday. Sunday in the US, Monday across most of Europe,
|
|
14012
|
+
* Saturday in much of the Middle East.
|
|
14013
|
+
*
|
|
14014
|
+
* Read from `Intl`, which reports it ISO-style (1 Monday … 7 Sunday). Not every engine
|
|
14015
|
+
* exposes it — Firefox notably lagged — so anything that cannot answer falls back to
|
|
14016
|
+
* Sunday, which is what the grid did for every locale before.
|
|
14017
|
+
*/
|
|
14018
|
+
function firstDayOfWeek(locale) {
|
|
14019
|
+
try {
|
|
14020
|
+
const info = new Intl.Locale(locale);
|
|
14021
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay;
|
|
14022
|
+
// ISO counts Sunday as 7; `Date.getDay()` counts it as 0.
|
|
14023
|
+
return firstDay === undefined ? 0 : firstDay % 7;
|
|
14024
|
+
}
|
|
14025
|
+
catch {
|
|
14026
|
+
// A malformed locale tag shouldn't take the calendar down with it.
|
|
14027
|
+
return 0;
|
|
14028
|
+
}
|
|
14029
|
+
}
|
|
14030
|
+
|
|
14031
|
+
/** The cell holding the grid's roving tabindex — the one cell focus belongs on. */
|
|
14032
|
+
const ACTIVE_CELL = '.tn-calendar-body-cell[tabindex="0"]';
|
|
14033
|
+
/**
|
|
14034
|
+
* Follows the roving tabindex with real focus, once the grid it names has rendered.
|
|
14035
|
+
*
|
|
14036
|
+
* Two situations need it, and both leave focus somewhere wrong until it runs. Arrowing
|
|
14037
|
+
* within a grid re-renders cells that persist as elements, so the browser keeps focus on
|
|
14038
|
+
* the cell just left. Switching between the day and year grids destroys the cell that
|
|
14039
|
+
* was focused outright, which drops focus to `<body>` and restarts the tab order.
|
|
14040
|
+
*
|
|
14041
|
+
* Tied to the render rather than a zero timeout: the target cell doesn't exist until the
|
|
14042
|
+
* new grid is painted.
|
|
14043
|
+
*
|
|
14044
|
+
* Does nothing when no cell can take focus — a month or page with every day disabled has
|
|
14045
|
+
* nothing worth focusing.
|
|
14046
|
+
*/
|
|
14047
|
+
function focusActiveCellAfterRender(host, injector) {
|
|
14048
|
+
afterNextRender(() => {
|
|
14049
|
+
host.nativeElement.querySelector(ACTIVE_CELL)?.focus();
|
|
14050
|
+
}, { injector });
|
|
14051
|
+
}
|
|
14052
|
+
|
|
14053
|
+
/** The wording used when an app provides none of its own. */
|
|
14054
|
+
const TN_CALENDAR_INTL_DEFAULTS = {
|
|
14055
|
+
marked: '(marked)',
|
|
14056
|
+
rangeStart: '(range start)',
|
|
14057
|
+
rangeEnd: '(range end)',
|
|
14058
|
+
inRange: '(in range)',
|
|
14059
|
+
currentYear: '(current year)',
|
|
14060
|
+
monthGridLabel: (period) => period,
|
|
14061
|
+
yearGridLabel: (startYear, endYear) => `Years ${startYear} to ${endYear}`,
|
|
14062
|
+
chooseMonthAndYear: 'Choose month and year',
|
|
14063
|
+
previousMonth: 'Previous month',
|
|
14064
|
+
nextMonth: 'Next month',
|
|
14065
|
+
previousYears: `Previous ${YEARS_PER_PAGE} years`,
|
|
14066
|
+
nextYears: `Next ${YEARS_PER_PAGE} years`,
|
|
14067
|
+
};
|
|
14068
|
+
/**
|
|
14069
|
+
* Injection token for the calendar's wording.
|
|
14070
|
+
*
|
|
14071
|
+
* Because the library ships no localized strings, this is the recommended hook for
|
|
14072
|
+
* wiring a translation service so every `tn-calendar` reads the same way. Anything left
|
|
14073
|
+
* out falls back to {@link TN_CALENDAR_INTL_DEFAULTS}, so an app can translate one
|
|
14074
|
+
* string without restating the rest.
|
|
14075
|
+
*
|
|
14076
|
+
* Dates themselves are not here — they follow `LOCALE_ID`, or the calendar's `locale`
|
|
14077
|
+
* input. See `TnCalendarComponent.locale`.
|
|
14078
|
+
*
|
|
14079
|
+
* @example
|
|
14080
|
+
* ```ts
|
|
14081
|
+
* // Fixed for the lifetime of the app — the language is chosen at build or bootstrap.
|
|
14082
|
+
* providers: [
|
|
14083
|
+
* {
|
|
14084
|
+
* provide: TN_CALENDAR_INTL,
|
|
14085
|
+
* useFactory: (translate: TranslateService): TnCalendarIntlInput => ({
|
|
14086
|
+
* marked: translate.instant('calendar.marked'),
|
|
14087
|
+
* previousMonth: translate.instant('calendar.previousMonth'),
|
|
14088
|
+
* yearGridLabel: (from, to) => translate.instant('calendar.years', { from, to }),
|
|
14089
|
+
* }),
|
|
14090
|
+
* deps: [TranslateService],
|
|
14091
|
+
* },
|
|
14092
|
+
* ];
|
|
14093
|
+
* ```
|
|
14094
|
+
*
|
|
14095
|
+
* @example
|
|
14096
|
+
* ```ts
|
|
14097
|
+
* // Switchable at runtime: a signal, so calendars already on screen follow along.
|
|
14098
|
+
* providers: [
|
|
14099
|
+
* {
|
|
14100
|
+
* provide: TN_CALENDAR_INTL,
|
|
14101
|
+
* useFactory: (): TnCalendarIntlInput => {
|
|
14102
|
+
* const translate = inject(TranslateService);
|
|
14103
|
+
* const lang = toSignal(translate.onLangChange, { initialValue: null });
|
|
14104
|
+
* return computed(() => {
|
|
14105
|
+
* lang(); // Re-read every string when the language changes.
|
|
14106
|
+
* return { marked: translate.instant('calendar.marked') };
|
|
14107
|
+
* });
|
|
14108
|
+
* },
|
|
14109
|
+
* },
|
|
14110
|
+
* ];
|
|
14111
|
+
* ```
|
|
14112
|
+
*/
|
|
14113
|
+
const TN_CALENDAR_INTL = new InjectionToken('TN_CALENDAR_INTL');
|
|
14114
|
+
/**
|
|
14115
|
+
* Resolves the wording for a calendar component: whatever the app provided, laid over
|
|
14116
|
+
* the built-in defaults.
|
|
14117
|
+
*
|
|
14118
|
+
* Returns a signal rather than a plain object so a language switch reaches calendars that
|
|
14119
|
+
* are already on screen. Read it inside a `computed` — as every caller here does — and
|
|
14120
|
+
* the labels recompute on their own.
|
|
14121
|
+
*/
|
|
14122
|
+
function injectTnCalendarIntl() {
|
|
14123
|
+
const provided = inject(TN_CALENDAR_INTL, { optional: true });
|
|
14124
|
+
return computed(() => {
|
|
14125
|
+
// Signals are functions; a plain object of wording is not.
|
|
14126
|
+
const wording = typeof provided === 'function' ? provided() : provided;
|
|
14127
|
+
return { ...TN_CALENDAR_INTL_DEFAULTS, ...(wording ?? {}) };
|
|
14128
|
+
});
|
|
14129
|
+
}
|
|
14130
|
+
|
|
13833
14131
|
class TnCalendarHeaderComponent {
|
|
13834
14132
|
currentDate = input(new Date(), ...(ngDevMode ? [{ debugName: "currentDate" }] : []));
|
|
13835
14133
|
currentView = input('month', ...(ngDevMode ? [{ debugName: "currentView" }] : []));
|
|
13836
|
-
|
|
13837
|
-
|
|
14134
|
+
/**
|
|
14135
|
+
* Locale for the month abbreviation. Falls back to the app's `LOCALE_ID`;
|
|
14136
|
+
* `tn-calendar` binds it from its own `locale` input.
|
|
14137
|
+
*/
|
|
14138
|
+
locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
13838
14139
|
viewChanged = output();
|
|
13839
14140
|
previousClicked = output();
|
|
13840
14141
|
nextClicked = output();
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
14142
|
+
appLocale = inject(LOCALE_ID);
|
|
14143
|
+
intl = injectTnCalendarIntl();
|
|
14144
|
+
resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
|
|
14145
|
+
/**
|
|
14146
|
+
* Ties the period button to its live region through `aria-describedby`. Counted rather
|
|
14147
|
+
* than drawn at random: two calendars on one page — a start and an end picker, say —
|
|
14148
|
+
* had a real chance of minting the same id out of only 10,000, and a duplicate id
|
|
14149
|
+
* silently unhooks the association with nothing to show for it.
|
|
14150
|
+
*/
|
|
14151
|
+
periodLabelId = inject(_IdGenerator).getId('tn-calendar-period-label-');
|
|
13846
14152
|
periodLabel = computed(() => {
|
|
13847
14153
|
const date = this.currentDate();
|
|
13848
14154
|
if (!date) {
|
|
13849
14155
|
return '';
|
|
13850
14156
|
}
|
|
14157
|
+
const locale = this.resolvedLocale();
|
|
13851
14158
|
if (this.currentView() === 'month') {
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
14159
|
+
// Formatted as one unit so the locale decides the order and the numerals, not a
|
|
14160
|
+
// template string. Upper-cased after the fact: no locale offers a short month name
|
|
14161
|
+
// already capitalised the way this header wants it.
|
|
14162
|
+
return date.toLocaleDateString(locale, { month: 'short', year: 'numeric' }).toUpperCase();
|
|
13855
14163
|
}
|
|
13856
14164
|
else {
|
|
13857
|
-
//
|
|
13858
|
-
|
|
13859
|
-
const startYear =
|
|
13860
|
-
const
|
|
13861
|
-
return `${startYear} – ${
|
|
14165
|
+
// The span the year grid is showing. Shared with the view so the two cannot
|
|
14166
|
+
// disagree about which page is on screen.
|
|
14167
|
+
const startYear = yearPageStart(date.getFullYear());
|
|
14168
|
+
const yearFormat = new Intl.NumberFormat(locale, { useGrouping: false });
|
|
14169
|
+
return `${yearFormat.format(startYear)} – ${yearFormat.format(startYear + YEARS_PER_PAGE - 1)}`;
|
|
13862
14170
|
}
|
|
13863
14171
|
}, ...(ngDevMode ? [{ debugName: "periodLabel" }] : []));
|
|
13864
14172
|
previousLabel = computed(() => {
|
|
13865
|
-
return this.currentView() === 'month' ?
|
|
14173
|
+
return this.currentView() === 'month' ? this.intl().previousMonth : this.intl().previousYears;
|
|
13866
14174
|
}, ...(ngDevMode ? [{ debugName: "previousLabel" }] : []));
|
|
13867
14175
|
nextLabel = computed(() => {
|
|
13868
|
-
return this.currentView() === 'month' ?
|
|
14176
|
+
return this.currentView() === 'month' ? this.intl().nextMonth : this.intl().nextYears;
|
|
13869
14177
|
}, ...(ngDevMode ? [{ debugName: "nextLabel" }] : []));
|
|
14178
|
+
periodButtonLabel = computed(() => this.intl().chooseMonthAndYear, ...(ngDevMode ? [{ debugName: "periodButtonLabel" }] : []));
|
|
13870
14179
|
toggleView() {
|
|
13871
14180
|
const newView = this.currentView() === 'month' ? 'year' : 'month';
|
|
13872
14181
|
this.viewChanged.emit(newView);
|
|
@@ -13878,12 +14187,12 @@ class TnCalendarHeaderComponent {
|
|
|
13878
14187
|
this.nextClicked.emit();
|
|
13879
14188
|
}
|
|
13880
14189
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13881
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnCalendarHeaderComponent, isStandalone: true, selector: "tn-calendar-header", inputs: { currentDate: { classPropertyName: "currentDate", publicName: "currentDate", isSignal: true, isRequired: false, transformFunction: null }, currentView: { classPropertyName: "currentView", publicName: "currentView", isSignal: true, isRequired: false, transformFunction: null }
|
|
14190
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnCalendarHeaderComponent, isStandalone: true, selector: "tn-calendar-header", inputs: { currentDate: { classPropertyName: "currentDate", publicName: "currentDate", isSignal: true, isRequired: false, transformFunction: null }, currentView: { classPropertyName: "currentView", publicName: "currentView", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewChanged: "viewChanged", previousClicked: "previousClicked", nextClicked: "nextClicked" }, ngImport: i0, template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"periodButtonLabel()\"\n [attr.aria-describedby]=\"periodLabelId\"\n (click)=\"toggleView()\">\n <span [attr.aria-hidden]=\"true\">{{ periodLabel() }}</span>\n <svg viewBox=\"0 0 10 5\" focusable=\"false\" aria-hidden=\"true\" class=\"tn-calendar-arrow\">\n <polygon points=\"0,0 5,5 10,0\" />\n </svg>\n </button>\n\n <!-- Spacer -->\n <div class=\"tn-calendar-spacer\"></div>\n\n <!-- Previous button -->\n <button\n type=\"button\"\n class=\"tn-calendar-previous-button\"\n [attr.aria-label]=\"previousLabel()\"\n (click)=\"onPreviousClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"tn-calendar-next-button\"\n [attr.aria-label]=\"nextLabel()\"\n (click)=\"onNextClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".tn-calendar-header{display:flex;padding:16px}.tn-calendar-controls{display:flex;align-items:center;width:100%}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-calendar-period-button{background:none;border:none;font-weight:600;font-size:1rem;color:var(--tn-fg1, #333);padding:8px 12px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;transition:background-color .2s ease}.tn-calendar-period-button:hover{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-period-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-arrow{width:10px;height:5px;fill:currentColor}.tn-calendar-spacer{flex:1}.tn-calendar-previous-button,.tn-calendar-next-button{background:none;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--tn-fg1, #333);transition:background-color .2s ease}.tn-calendar-previous-button svg,.tn-calendar-next-button svg{width:24px;height:24px;fill:currentColor}.tn-calendar-previous-button:hover:not(:disabled),.tn-calendar-next-button:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:focus,.tn-calendar-next-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:disabled,.tn-calendar-next-button:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:not-allowed}\n"] });
|
|
13882
14191
|
}
|
|
13883
14192
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarHeaderComponent, decorators: [{
|
|
13884
14193
|
type: Component,
|
|
13885
|
-
args: [{ selector: 'tn-calendar-header', standalone: true, imports: [], template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"
|
|
13886
|
-
}], propDecorators: { currentDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentDate", required: false }] }], currentView: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentView", required: false }] }],
|
|
14194
|
+
args: [{ selector: 'tn-calendar-header', standalone: true, imports: [], template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"periodButtonLabel()\"\n [attr.aria-describedby]=\"periodLabelId\"\n (click)=\"toggleView()\">\n <span [attr.aria-hidden]=\"true\">{{ periodLabel() }}</span>\n <svg viewBox=\"0 0 10 5\" focusable=\"false\" aria-hidden=\"true\" class=\"tn-calendar-arrow\">\n <polygon points=\"0,0 5,5 10,0\" />\n </svg>\n </button>\n\n <!-- Spacer -->\n <div class=\"tn-calendar-spacer\"></div>\n\n <!-- Previous button -->\n <button\n type=\"button\"\n class=\"tn-calendar-previous-button\"\n [attr.aria-label]=\"previousLabel()\"\n (click)=\"onPreviousClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"tn-calendar-next-button\"\n [attr.aria-label]=\"nextLabel()\"\n (click)=\"onNextClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".tn-calendar-header{display:flex;padding:16px}.tn-calendar-controls{display:flex;align-items:center;width:100%}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-calendar-period-button{background:none;border:none;font-weight:600;font-size:1rem;color:var(--tn-fg1, #333);padding:8px 12px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;transition:background-color .2s ease}.tn-calendar-period-button:hover{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-period-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-arrow{width:10px;height:5px;fill:currentColor}.tn-calendar-spacer{flex:1}.tn-calendar-previous-button,.tn-calendar-next-button{background:none;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--tn-fg1, #333);transition:background-color .2s ease}.tn-calendar-previous-button svg,.tn-calendar-next-button svg{width:24px;height:24px;fill:currentColor}.tn-calendar-previous-button:hover:not(:disabled),.tn-calendar-next-button:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:focus,.tn-calendar-next-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:disabled,.tn-calendar-next-button:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:not-allowed}\n"] }]
|
|
14195
|
+
}], propDecorators: { currentDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentDate", required: false }] }], currentView: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentView", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], viewChanged: [{ type: i0.Output, args: ["viewChanged"] }], previousClicked: [{ type: i0.Output, args: ["previousClicked"] }], nextClicked: [{ type: i0.Output, args: ["nextClicked"] }] } });
|
|
13887
14196
|
|
|
13888
14197
|
class TnMonthViewComponent {
|
|
13889
14198
|
activeDate = input(new Date(), ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
|
|
@@ -13891,25 +14200,114 @@ class TnMonthViewComponent {
|
|
|
13891
14200
|
minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
|
|
13892
14201
|
maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
|
|
13893
14202
|
dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
|
|
14203
|
+
/**
|
|
14204
|
+
* Dates to flag as noteworthy — days a task runs, days with events, and the like.
|
|
14205
|
+
* Order and time-of-day are ignored; only the calendar day is compared. The
|
|
14206
|
+
* calendar owns how a marked day looks, so callers pass dates rather than styles.
|
|
14207
|
+
*/
|
|
14208
|
+
markedDates = input(undefined, ...(ngDevMode ? [{ debugName: "markedDates" }] : []));
|
|
13894
14209
|
// Range mode inputs
|
|
13895
14210
|
rangeMode = input(false, ...(ngDevMode ? [{ debugName: "rangeMode" }] : []));
|
|
13896
14211
|
selectedRange = input(undefined, ...(ngDevMode ? [{ debugName: "selectedRange" }] : []));
|
|
13897
14212
|
selectedChange = output();
|
|
13898
14213
|
activeDateChange = output();
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
13906
|
-
|
|
13907
|
-
|
|
14214
|
+
/**
|
|
14215
|
+
* Locale for dates, weekday names and the first day of the week. Falls back to the
|
|
14216
|
+
* app's `LOCALE_ID`; `tn-calendar` binds it from its own `locale` input.
|
|
14217
|
+
*/
|
|
14218
|
+
locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
14219
|
+
host = inject(ElementRef);
|
|
14220
|
+
injector = inject(Injector);
|
|
14221
|
+
appLocale = inject(LOCALE_ID);
|
|
14222
|
+
intl = injectTnCalendarIntl();
|
|
14223
|
+
resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
|
|
14224
|
+
/**
|
|
14225
|
+
* Which day the week starts on in this locale, as a `Date.getDay()` index. Everything
|
|
14226
|
+
* about the grid's shape hangs off it: the order of the headings and how many blank
|
|
14227
|
+
* cells precede the 1st.
|
|
14228
|
+
*/
|
|
14229
|
+
weekStart = computed(() => firstDayOfWeek(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "weekStart" }] : []));
|
|
14230
|
+
/** Day numbers in the locale's own numerals, without a thousands separator. */
|
|
14231
|
+
dayFormat = computed(() => {
|
|
14232
|
+
return new Intl.NumberFormat(this.resolvedLocale(), { useGrouping: false });
|
|
14233
|
+
}, ...(ngDevMode ? [{ debugName: "dayFormat" }] : []));
|
|
14234
|
+
/**
|
|
14235
|
+
* Column headings, named and ordered by the locale — Sunday first in the US, Monday
|
|
14236
|
+
* across most of Europe. The dates are an arbitrary week, used only to name the days.
|
|
14237
|
+
*/
|
|
14238
|
+
weekdays = computed(() => {
|
|
14239
|
+
const locale = this.resolvedLocale();
|
|
14240
|
+
const long = new Intl.DateTimeFormat(locale, { weekday: 'long' });
|
|
14241
|
+
const narrow = new Intl.DateTimeFormat(locale, { weekday: 'narrow' });
|
|
14242
|
+
const weekStart = this.weekStart();
|
|
14243
|
+
return Array.from({ length: 7 }, (_, column) => {
|
|
14244
|
+
// 7 Jan 2024 was a Sunday, so adding a getDay() index lands on that weekday.
|
|
14245
|
+
const date = new Date(2024, 0, 7 + ((weekStart + column) % 7));
|
|
14246
|
+
return { long: long.format(date), short: narrow.format(date) };
|
|
14247
|
+
});
|
|
14248
|
+
}, ...(ngDevMode ? [{ debugName: "weekdays" }] : []));
|
|
13908
14249
|
// Cell sizing now controlled via CSS custom properties in the SCSS file
|
|
14250
|
+
markedDateKeys = computed(() => {
|
|
14251
|
+
return new Set((this.markedDates() ?? []).map((date) => dateKey(date)));
|
|
14252
|
+
}, ...(ngDevMode ? [{ debugName: "markedDateKeys" }] : []));
|
|
14253
|
+
/**
|
|
14254
|
+
* Names the grid after the month it shows. Without it a screen-reader user arrowing
|
|
14255
|
+
* around hears each day but never which month they are in — the period only appears in
|
|
14256
|
+
* the header, outside the grid.
|
|
14257
|
+
*/
|
|
14258
|
+
gridLabel = computed(() => {
|
|
14259
|
+
const period = this.activeDate()
|
|
14260
|
+
.toLocaleDateString(this.resolvedLocale(), { month: 'long', year: 'numeric' });
|
|
14261
|
+
return this.intl().monthGridLabel(period);
|
|
14262
|
+
}, ...(ngDevMode ? [{ debugName: "gridLabel" }] : []));
|
|
14263
|
+
/**
|
|
14264
|
+
* The day of the month that carries `tabindex="0"` — the grid's single keyboard entry
|
|
14265
|
+
* point, per the roving tabindex pattern.
|
|
14266
|
+
*
|
|
14267
|
+
* It follows `activeDate`, but falls back to the nearest enabled day when that day
|
|
14268
|
+
* can't take focus. Without the fallback a month whose active day is disabled (say,
|
|
14269
|
+
* anything before `minDate`) would have no tabbable cell at all, leaving the grid
|
|
14270
|
+
* unreachable by keyboard. `null` only when the whole month is disabled, where there
|
|
14271
|
+
* is nothing worth focusing anyway.
|
|
14272
|
+
*
|
|
14273
|
+
* Derived from the month rather than from `calendarRows()`, which would be circular.
|
|
14274
|
+
*/
|
|
14275
|
+
activeDay = computed(() => {
|
|
14276
|
+
const activeDate = this.activeDate();
|
|
14277
|
+
if (!activeDate) {
|
|
14278
|
+
return null;
|
|
14279
|
+
}
|
|
14280
|
+
const year = activeDate.getFullYear();
|
|
14281
|
+
const month = activeDate.getMonth();
|
|
14282
|
+
const lastDay = new Date(year, month + 1, 0).getDate();
|
|
14283
|
+
const isEnabled = (day) => this.isDateEnabled(new Date(year, month, day));
|
|
14284
|
+
// Always within the rendered month: both come off the same Date, and `Date` has
|
|
14285
|
+
// already rolled over any overflow the caller handed us.
|
|
14286
|
+
const wanted = activeDate.getDate();
|
|
14287
|
+
if (isEnabled(wanted)) {
|
|
14288
|
+
return wanted;
|
|
14289
|
+
}
|
|
14290
|
+
// Spiral outwards from the day we wanted, forward first.
|
|
14291
|
+
for (let offset = 1; offset < lastDay; offset++) {
|
|
14292
|
+
if (wanted + offset <= lastDay && isEnabled(wanted + offset)) {
|
|
14293
|
+
return wanted + offset;
|
|
14294
|
+
}
|
|
14295
|
+
if (wanted - offset >= 1 && isEnabled(wanted - offset)) {
|
|
14296
|
+
return wanted - offset;
|
|
14297
|
+
}
|
|
14298
|
+
}
|
|
14299
|
+
return null;
|
|
14300
|
+
}, ...(ngDevMode ? [{ debugName: "activeDay" }] : []));
|
|
14301
|
+
activeCellDate = computed(() => {
|
|
14302
|
+
const day = this.activeDay();
|
|
14303
|
+
if (day === null) {
|
|
14304
|
+
return null;
|
|
14305
|
+
}
|
|
14306
|
+
const activeDate = this.activeDate();
|
|
14307
|
+
return new Date(activeDate.getFullYear(), activeDate.getMonth(), day);
|
|
14308
|
+
}, ...(ngDevMode ? [{ debugName: "activeCellDate" }] : []));
|
|
13909
14309
|
calendarRows = computed(() => {
|
|
13910
14310
|
const activeDate = this.activeDate();
|
|
13911
|
-
// Track selectedRange signal so computed recalculates when range changes
|
|
13912
|
-
this.selectedRange();
|
|
13913
14311
|
if (!activeDate) {
|
|
13914
14312
|
return [];
|
|
13915
14313
|
}
|
|
@@ -13917,7 +14315,13 @@ class TnMonthViewComponent {
|
|
|
13917
14315
|
const month = activeDate.getMonth();
|
|
13918
14316
|
const firstDate = new Date(year, month, 1);
|
|
13919
14317
|
const lastDate = new Date(year, month + 1, 0);
|
|
13920
|
-
|
|
14318
|
+
// How far into the week the 1st falls, counted from wherever this locale's week
|
|
14319
|
+
// begins — that many blanks lead the grid.
|
|
14320
|
+
const startDayOfWeek = (firstDate.getDay() - this.weekStart() + 7) % 7;
|
|
14321
|
+
// Read once for the whole grid rather than per cell, so every day is measured
|
|
14322
|
+
// against the same instant as well as allocating a fortieth as much.
|
|
14323
|
+
const today = new Date();
|
|
14324
|
+
const dayFormat = this.dayFormat();
|
|
13921
14325
|
const rows = [];
|
|
13922
14326
|
let currentRow = [];
|
|
13923
14327
|
// Add empty cells for days before month starts
|
|
@@ -13927,7 +14331,7 @@ class TnMonthViewComponent {
|
|
|
13927
14331
|
// Add all days of the month
|
|
13928
14332
|
for (let day = 1; day <= lastDate.getDate(); day++) {
|
|
13929
14333
|
const date = new Date(year, month, day);
|
|
13930
|
-
currentRow.push(this.createCell(date, day));
|
|
14334
|
+
currentRow.push(this.createCell(date, day, today, dayFormat));
|
|
13931
14335
|
// If we have 7 cells, complete the row
|
|
13932
14336
|
if (currentRow.length === 7) {
|
|
13933
14337
|
rows.push(currentRow);
|
|
@@ -13943,10 +14347,10 @@ class TnMonthViewComponent {
|
|
|
13943
14347
|
}
|
|
13944
14348
|
return rows;
|
|
13945
14349
|
}, ...(ngDevMode ? [{ debugName: "calendarRows" }] : []));
|
|
13946
|
-
createCell(date, value) {
|
|
13947
|
-
const
|
|
13948
|
-
const
|
|
13949
|
-
const
|
|
14350
|
+
createCell(date, value, today, dayFormat) {
|
|
14351
|
+
const isToday = isSameDay(date, today);
|
|
14352
|
+
const isSelected = this.selected() ? isSameDay(date, this.selected()) : false;
|
|
14353
|
+
const isMarked = this.markedDateKeys().has(dateKey(date));
|
|
13950
14354
|
const enabled = this.isDateEnabled(date);
|
|
13951
14355
|
// Range mode calculations
|
|
13952
14356
|
let rangeStart = false;
|
|
@@ -13955,24 +14359,36 @@ class TnMonthViewComponent {
|
|
|
13955
14359
|
const currentRange = this.selectedRange();
|
|
13956
14360
|
if (this.rangeMode() && currentRange) {
|
|
13957
14361
|
const { start, end } = currentRange;
|
|
13958
|
-
if (start &&
|
|
14362
|
+
if (start && isSameDay(date, start)) {
|
|
13959
14363
|
rangeStart = true;
|
|
13960
14364
|
}
|
|
13961
|
-
if (end &&
|
|
14365
|
+
if (end && isSameDay(date, end)) {
|
|
13962
14366
|
rangeEnd = true;
|
|
13963
14367
|
}
|
|
13964
|
-
|
|
14368
|
+
// Strictly between the ends. Compared by day so a start or end carrying a time of
|
|
14369
|
+
// day can't come out as both a cap and an in-between day.
|
|
14370
|
+
if (start && end && compareDays(date, start) > 0 && compareDays(date, end) < 0) {
|
|
13965
14371
|
inRange = true;
|
|
13966
14372
|
}
|
|
13967
14373
|
}
|
|
14374
|
+
// Selection and the range caps share one solid fill; marking sits below them and
|
|
14375
|
+
// shows only where neither applies. A cap is a selection too, so `today` must not
|
|
14376
|
+
// paint its outline over it.
|
|
14377
|
+
const isPrimary = isSelected || rangeStart || rangeEnd;
|
|
14378
|
+
const fill = isPrimary ? 'primary' : isMarked ? 'marked' : 'none';
|
|
13968
14379
|
return {
|
|
13969
14380
|
value,
|
|
13970
14381
|
date: new Date(date),
|
|
13971
|
-
|
|
13972
|
-
|
|
14382
|
+
isoDate: isoDateString(date),
|
|
14383
|
+
label: dayFormat.format(value),
|
|
14384
|
+
ariaLabel: this.formatAriaLabel(date, isMarked, rangeStart, rangeEnd, inRange),
|
|
13973
14385
|
enabled,
|
|
13974
14386
|
selected: isSelected,
|
|
13975
14387
|
today: isToday,
|
|
14388
|
+
marked: isMarked,
|
|
14389
|
+
inSelection: isSelected || rangeStart || rangeEnd || inRange,
|
|
14390
|
+
fill,
|
|
14391
|
+
todayOutline: isToday && !isPrimary,
|
|
13976
14392
|
rangeStart,
|
|
13977
14393
|
rangeEnd,
|
|
13978
14394
|
inRange,
|
|
@@ -13982,21 +14398,28 @@ class TnMonthViewComponent {
|
|
|
13982
14398
|
return {
|
|
13983
14399
|
value: 0,
|
|
13984
14400
|
date: new Date(),
|
|
14401
|
+
isoDate: '',
|
|
13985
14402
|
label: '',
|
|
13986
14403
|
ariaLabel: '',
|
|
13987
14404
|
enabled: false,
|
|
13988
14405
|
selected: false,
|
|
13989
14406
|
today: false,
|
|
14407
|
+
marked: false,
|
|
14408
|
+
inSelection: false,
|
|
14409
|
+
fill: 'none',
|
|
14410
|
+
todayOutline: false,
|
|
13990
14411
|
};
|
|
13991
14412
|
}
|
|
13992
14413
|
isDateEnabled(date) {
|
|
13993
14414
|
const minDate = this.minDate();
|
|
13994
14415
|
const maxDate = this.maxDate();
|
|
13995
14416
|
const dateFilter = this.dateFilter();
|
|
13996
|
-
|
|
14417
|
+
// Bounds are inclusive of their own day: a `minDate` of "today, 14:30" means today
|
|
14418
|
+
// is selectable, not that today is half past the deadline.
|
|
14419
|
+
if (minDate && compareDays(date, minDate) < 0) {
|
|
13997
14420
|
return false;
|
|
13998
14421
|
}
|
|
13999
|
-
if (maxDate && date >
|
|
14422
|
+
if (maxDate && compareDays(date, maxDate) > 0) {
|
|
14000
14423
|
return false;
|
|
14001
14424
|
}
|
|
14002
14425
|
if (dateFilter && !dateFilter(date)) {
|
|
@@ -14004,32 +14427,31 @@ class TnMonthViewComponent {
|
|
|
14004
14427
|
}
|
|
14005
14428
|
return true;
|
|
14006
14429
|
}
|
|
14007
|
-
|
|
14008
|
-
|
|
14009
|
-
|
|
14010
|
-
|
|
14011
|
-
|
|
14012
|
-
|
|
14013
|
-
|
|
14430
|
+
/**
|
|
14431
|
+
* Spells out the state a day is in, but only the parts no ARIA attribute already
|
|
14432
|
+
* carries. Selection lives on the button's `aria-pressed` and today on `aria-current`,
|
|
14433
|
+
* so repeating them here had screen readers announce each one twice. Marking and the
|
|
14434
|
+
* range positions have no attribute equivalent, and would otherwise be conveyed by
|
|
14435
|
+
* background colour alone.
|
|
14436
|
+
*/
|
|
14437
|
+
formatAriaLabel(date, isMarked, rangeStart, rangeEnd, inRange) {
|
|
14438
|
+
let label = date.toLocaleDateString(this.resolvedLocale(), {
|
|
14014
14439
|
weekday: 'long',
|
|
14015
14440
|
year: 'numeric',
|
|
14016
14441
|
month: 'long',
|
|
14017
14442
|
day: 'numeric'
|
|
14018
14443
|
});
|
|
14019
|
-
if (
|
|
14020
|
-
label +=
|
|
14021
|
-
}
|
|
14022
|
-
if (isToday) {
|
|
14023
|
-
label += ' (today)';
|
|
14444
|
+
if (isMarked) {
|
|
14445
|
+
label += ` ${this.intl().marked}`;
|
|
14024
14446
|
}
|
|
14025
14447
|
if (rangeStart) {
|
|
14026
|
-
label +=
|
|
14448
|
+
label += ` ${this.intl().rangeStart}`;
|
|
14027
14449
|
}
|
|
14028
14450
|
if (rangeEnd) {
|
|
14029
|
-
label +=
|
|
14451
|
+
label += ` ${this.intl().rangeEnd}`;
|
|
14030
14452
|
}
|
|
14031
14453
|
if (inRange) {
|
|
14032
|
-
label +=
|
|
14454
|
+
label += ` ${this.intl().inRange}`;
|
|
14033
14455
|
}
|
|
14034
14456
|
return label;
|
|
14035
14457
|
}
|
|
@@ -14037,23 +14459,107 @@ class TnMonthViewComponent {
|
|
|
14037
14459
|
if (cell.value === 0) {
|
|
14038
14460
|
return `empty-${index}`;
|
|
14039
14461
|
}
|
|
14040
|
-
return
|
|
14462
|
+
return dateKey(cell.date);
|
|
14041
14463
|
}
|
|
14042
14464
|
trackByRow(index) {
|
|
14043
14465
|
return index;
|
|
14044
14466
|
}
|
|
14045
14467
|
onCellClicked(cell) {
|
|
14046
|
-
if (cell.enabled
|
|
14047
|
-
|
|
14468
|
+
if (!cell.enabled || cell.value === 0) {
|
|
14469
|
+
return;
|
|
14470
|
+
}
|
|
14471
|
+
// The click already moved browser focus here, so the roving tabindex has to come
|
|
14472
|
+
// along or the two diverge: arrowing on from a clicked day would carry on from
|
|
14473
|
+
// wherever the active day happened to be, jumping the focus somewhere unrelated.
|
|
14474
|
+
this.activeDateChange.emit(cell.date);
|
|
14475
|
+
this.selectedChange.emit(cell.date);
|
|
14476
|
+
}
|
|
14477
|
+
/**
|
|
14478
|
+
* Moves the roving tabindex. Selection stays on click/Enter/Space, which the cells
|
|
14479
|
+
* are already buttons for, so this only ever changes which day is active.
|
|
14480
|
+
*
|
|
14481
|
+
* Moves that leave the displayed month emit an `activeDateChange` like any other
|
|
14482
|
+
* navigation; the grid re-renders on the new month with the target day active.
|
|
14483
|
+
*/
|
|
14484
|
+
onKeydown(event) {
|
|
14485
|
+
const from = this.activeCellDate();
|
|
14486
|
+
if (!from) {
|
|
14487
|
+
return;
|
|
14488
|
+
}
|
|
14489
|
+
const move = this.targetForKey(event, from);
|
|
14490
|
+
if (!move) {
|
|
14491
|
+
return;
|
|
14492
|
+
}
|
|
14493
|
+
// Claim the key before the browser scrolls the page with it.
|
|
14494
|
+
event.preventDefault();
|
|
14495
|
+
const landing = this.nearestEnabled(move.date, move.search);
|
|
14496
|
+
if (!landing || isSameDay(landing, from)) {
|
|
14497
|
+
return;
|
|
14048
14498
|
}
|
|
14499
|
+
this.activeDateChange.emit(landing);
|
|
14500
|
+
focusActiveCellAfterRender(this.host, this.injector);
|
|
14501
|
+
}
|
|
14502
|
+
/**
|
|
14503
|
+
* The day a key aims at, and which way to look from there when that day is disabled.
|
|
14504
|
+
*
|
|
14505
|
+
* The search direction belongs to the key, not to where the target happens to fall
|
|
14506
|
+
* relative to the current day. Home and End mean "the ends of *this* month", so they
|
|
14507
|
+
* search inward: looking outward would step straight into the neighbouring month over
|
|
14508
|
+
* a single filtered-out boundary day and page the view, which is not what either key
|
|
14509
|
+
* promises. The arrows and the paging keys keep travelling the way they were already
|
|
14510
|
+
* headed, and crossing into the next month is the point of those.
|
|
14511
|
+
*/
|
|
14512
|
+
targetForKey(event, from) {
|
|
14513
|
+
const year = from.getFullYear();
|
|
14514
|
+
const month = from.getMonth();
|
|
14515
|
+
const day = from.getDate();
|
|
14516
|
+
// Overshooting a month's length is fine — the Date constructor rolls it over, which
|
|
14517
|
+
// is exactly what arrowing off the end of a month should do.
|
|
14518
|
+
switch (event.key) {
|
|
14519
|
+
case 'ArrowLeft': return { date: new Date(year, month, day - 1), search: -1 };
|
|
14520
|
+
case 'ArrowRight': return { date: new Date(year, month, day + 1), search: 1 };
|
|
14521
|
+
case 'ArrowUp': return { date: new Date(year, month, day - 7), search: -1 };
|
|
14522
|
+
case 'ArrowDown': return { date: new Date(year, month, day + 7), search: 1 };
|
|
14523
|
+
case 'Home': return { date: new Date(year, month, 1), search: 1 };
|
|
14524
|
+
case 'End': return { date: new Date(year, month + 1, 0), search: -1 };
|
|
14525
|
+
// Shift pages by a year, matching Material and the wider grid convention.
|
|
14526
|
+
case 'PageUp': return { date: addMonths(from, event.shiftKey ? -12 : -1), search: -1 };
|
|
14527
|
+
case 'PageDown': return { date: addMonths(from, event.shiftKey ? 12 : 1), search: 1 };
|
|
14528
|
+
default: return null;
|
|
14529
|
+
}
|
|
14530
|
+
}
|
|
14531
|
+
/**
|
|
14532
|
+
* Steps past days the caller has disabled, so a gap in `dateFilter` or the far side of
|
|
14533
|
+
* `minDate`/`maxDate` doesn't park focus somewhere unusable.
|
|
14534
|
+
*
|
|
14535
|
+
* Searches the way the key asked for (see `targetForKey`), then doubles back if that
|
|
14536
|
+
* finds nothing. The fallback is for the ends of the allowed range: arrowing right at
|
|
14537
|
+
* `maxDate` finds nothing ahead, doubles back onto the day we started from, and the
|
|
14538
|
+
* caller reads that as "don't move".
|
|
14539
|
+
*/
|
|
14540
|
+
nearestEnabled(target, preferred) {
|
|
14541
|
+
return this.scanForEnabled(target, preferred)
|
|
14542
|
+
?? this.scanForEnabled(target, preferred === 1 ? -1 : 1);
|
|
14543
|
+
}
|
|
14544
|
+
/** Bounded, so an everything-disabled filter stops the move instead of spinning. */
|
|
14545
|
+
scanForEnabled(from, direction) {
|
|
14546
|
+
const maxSteps = 62;
|
|
14547
|
+
const candidate = new Date(from);
|
|
14548
|
+
for (let step = 0; step <= maxSteps; step++) {
|
|
14549
|
+
if (this.isDateEnabled(candidate)) {
|
|
14550
|
+
return new Date(candidate);
|
|
14551
|
+
}
|
|
14552
|
+
candidate.setDate(candidate.getDate() + direction);
|
|
14553
|
+
}
|
|
14554
|
+
return null;
|
|
14049
14555
|
}
|
|
14050
14556
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMonthViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14051
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMonthViewComponent, isStandalone: true, selector: "tn-month-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays; track day) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.
|
|
14557
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMonthViewComponent, isStandalone: true, selector: "tn-month-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, markedDates: { classPropertyName: "markedDates", publicName: "markedDates", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays(); track day.long) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-date]=\"cell.isoDate\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-marked]=\"cell.marked\"\n [class.tn-calendar-body-active]=\"cell.value === activeDay()\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.value === activeDay() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.inSelection\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onCellClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\"\n [class.tn-calendar-body-fill-primary]=\"cell.fill === 'primary'\"\n [class.tn-calendar-body-fill-marked]=\"cell.fill === 'marked'\"\n [class.tn-calendar-body-today-outline]=\"cell.todayOutline\">\n {{ cell.label }}\n </span>\n </button>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{display:block;--calendar-cell-size: 48px;--calendar-header-height: 40px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px;--calendar-indicator-size: var(--calendar-cell-size);--calendar-radius: 4px}.tn-calendar-table{width:100%;min-width:min(100%,7 * var(--calendar-cell-size));table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header th{text-align:center;height:var(--calendar-header-height);padding:8px 0;font-size:var(--calendar-header-font-size);font-weight:500;color:var(--tn-fg2, #666)}.tn-calendar-table-header th:first-child{border-start-start-radius:var(--calendar-radius);border-end-start-radius:var(--calendar-radius)}.tn-calendar-table-header th:last-child{border-start-end-radius:var(--calendar-radius);border-end-end-radius:var(--calendar-radius)}.tn-calendar-table-header-divider{height:1px;border:0}.tn-calendar-body tr{border:0}.tn-calendar-body-cell-container{position:relative;border:0;outline:0;height:var(--calendar-cell-size);width:14.2857142857%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;background:transparent;border:0;outline:0;cursor:pointer;color:var(--tn-fg1, #333)}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-in-range:before,.tn-calendar-body-cell.tn-calendar-body-range-start:before,.tn-calendar-body-cell.tn-calendar-body-range-end:before{content:\"\";position:absolute;top:50%;transform:translateY(-50%);height:var(--calendar-indicator-size);background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-in-range:before{inset-inline:0}.tn-calendar-body-cell.tn-calendar-body-range-start:before{inset-inline-start:50%;inset-inline-end:0}.tn-calendar-body-cell.tn-calendar-body-range-end:before{inset-inline-start:0;inset-inline-end:50%}.tn-calendar-body-cell.tn-calendar-body-range-start.tn-calendar-body-range-end:before{content:none}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-indicator-size);height:var(--calendar-indicator-size);max-width:100%;border-radius:var(--calendar-radius);font-size:var(--calendar-cell-font-size);font-weight:400;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):hover .tn-calendar-body-cell-content:not(.tn-calendar-body-fill-primary){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell-content.tn-calendar-body-fill-marked{background:var(--tn-alt-bg1, #f0f0f0)}.tn-calendar-body-cell-content.tn-calendar-body-fill-primary{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}.tn-calendar-body-cell-content.tn-calendar-body-today-outline{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"] });
|
|
14052
14558
|
}
|
|
14053
14559
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMonthViewComponent, decorators: [{
|
|
14054
14560
|
type: Component,
|
|
14055
|
-
args: [{ selector: 'tn-month-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays; track day) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.
|
|
14056
|
-
}], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }] } });
|
|
14561
|
+
args: [{ selector: 'tn-month-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays(); track day.long) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-date]=\"cell.isoDate\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-marked]=\"cell.marked\"\n [class.tn-calendar-body-active]=\"cell.value === activeDay()\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.value === activeDay() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.inSelection\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onCellClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\"\n [class.tn-calendar-body-fill-primary]=\"cell.fill === 'primary'\"\n [class.tn-calendar-body-fill-marked]=\"cell.fill === 'marked'\"\n [class.tn-calendar-body-today-outline]=\"cell.todayOutline\">\n {{ cell.label }}\n </span>\n </button>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{display:block;--calendar-cell-size: 48px;--calendar-header-height: 40px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px;--calendar-indicator-size: var(--calendar-cell-size);--calendar-radius: 4px}.tn-calendar-table{width:100%;min-width:min(100%,7 * var(--calendar-cell-size));table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header th{text-align:center;height:var(--calendar-header-height);padding:8px 0;font-size:var(--calendar-header-font-size);font-weight:500;color:var(--tn-fg2, #666)}.tn-calendar-table-header th:first-child{border-start-start-radius:var(--calendar-radius);border-end-start-radius:var(--calendar-radius)}.tn-calendar-table-header th:last-child{border-start-end-radius:var(--calendar-radius);border-end-end-radius:var(--calendar-radius)}.tn-calendar-table-header-divider{height:1px;border:0}.tn-calendar-body tr{border:0}.tn-calendar-body-cell-container{position:relative;border:0;outline:0;height:var(--calendar-cell-size);width:14.2857142857%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;background:transparent;border:0;outline:0;cursor:pointer;color:var(--tn-fg1, #333)}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-in-range:before,.tn-calendar-body-cell.tn-calendar-body-range-start:before,.tn-calendar-body-cell.tn-calendar-body-range-end:before{content:\"\";position:absolute;top:50%;transform:translateY(-50%);height:var(--calendar-indicator-size);background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-in-range:before{inset-inline:0}.tn-calendar-body-cell.tn-calendar-body-range-start:before{inset-inline-start:50%;inset-inline-end:0}.tn-calendar-body-cell.tn-calendar-body-range-end:before{inset-inline-start:0;inset-inline-end:50%}.tn-calendar-body-cell.tn-calendar-body-range-start.tn-calendar-body-range-end:before{content:none}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-indicator-size);height:var(--calendar-indicator-size);max-width:100%;border-radius:var(--calendar-radius);font-size:var(--calendar-cell-font-size);font-weight:400;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):hover .tn-calendar-body-cell-content:not(.tn-calendar-body-fill-primary){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell-content.tn-calendar-body-fill-marked{background:var(--tn-alt-bg1, #f0f0f0)}.tn-calendar-body-cell-content.tn-calendar-body-fill-primary{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}.tn-calendar-body-cell-content.tn-calendar-body-today-outline{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"] }]
|
|
14562
|
+
}], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], markedDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "markedDates", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }] } });
|
|
14057
14563
|
|
|
14058
14564
|
class TnMultiYearViewComponent {
|
|
14059
14565
|
activeDate = input(new Date(), ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
|
|
@@ -14061,46 +14567,91 @@ class TnMultiYearViewComponent {
|
|
|
14061
14567
|
minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
|
|
14062
14568
|
maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
|
|
14063
14569
|
dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
|
|
14570
|
+
/**
|
|
14571
|
+
* Locale for the year numerals — Arabic-Indic digits in `ar`, Western in `en`. Falls
|
|
14572
|
+
* back to the app's `LOCALE_ID`; `tn-calendar` binds it from its own `locale` input.
|
|
14573
|
+
*/
|
|
14574
|
+
locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
14064
14575
|
selectedChange = output();
|
|
14065
14576
|
activeDateChange = output();
|
|
14066
|
-
|
|
14067
|
-
|
|
14577
|
+
host = inject(ElementRef);
|
|
14578
|
+
injector = inject(Injector);
|
|
14579
|
+
appLocale = inject(LOCALE_ID);
|
|
14580
|
+
intl = injectTnCalendarIntl();
|
|
14581
|
+
/** Years read out in the locale's own numerals, without a thousands separator. */
|
|
14582
|
+
yearFormat = computed(() => {
|
|
14583
|
+
return new Intl.NumberFormat(this.locale() ?? this.appLocale, { useGrouping: false });
|
|
14584
|
+
}, ...(ngDevMode ? [{ debugName: "yearFormat" }] : []));
|
|
14068
14585
|
yearsPerRow = 4;
|
|
14069
|
-
yearRowCount =
|
|
14070
|
-
|
|
14586
|
+
yearRowCount = YEARS_PER_PAGE / this.yearsPerRow;
|
|
14587
|
+
/**
|
|
14588
|
+
* The page of years on screen. Pages are fixed blocks, so the active year sits
|
|
14589
|
+
* wherever it falls within one rather than in the middle — see `yearPageStart`.
|
|
14590
|
+
*/
|
|
14071
14591
|
yearRange = computed(() => {
|
|
14072
|
-
const
|
|
14073
|
-
|
|
14074
|
-
// Calculate the starting year for a 24-year range
|
|
14075
|
-
// We want the active year to be roughly in the middle
|
|
14076
|
-
const startYear = Math.floor(currentYear / 24) * 24;
|
|
14077
|
-
return { start: startYear, end: startYear + 23 };
|
|
14592
|
+
const start = yearPageStart(this.activeDate().getFullYear());
|
|
14593
|
+
return { start, end: start + YEARS_PER_PAGE - 1 };
|
|
14078
14594
|
}, ...(ngDevMode ? [{ debugName: "yearRange" }] : []));
|
|
14595
|
+
/**
|
|
14596
|
+
* Names the grid after the span of years it shows, so a screen-reader user arrowing
|
|
14597
|
+
* around knows which page they are on rather than just hearing bare years.
|
|
14598
|
+
*/
|
|
14599
|
+
gridLabel = computed(() => {
|
|
14600
|
+
const range = this.yearRange();
|
|
14601
|
+
return this.intl().yearGridLabel(range.start, range.end);
|
|
14602
|
+
}, ...(ngDevMode ? [{ debugName: "gridLabel" }] : []));
|
|
14079
14603
|
yearRows = computed(() => {
|
|
14080
14604
|
const range = this.yearRange();
|
|
14605
|
+
const format = this.yearFormat();
|
|
14606
|
+
const formatYear = (year) => format.format(year);
|
|
14607
|
+
// Read once for the whole grid rather than per cell, matching the month view: every
|
|
14608
|
+
// year is then measured against the same instant, and 24 allocations become one.
|
|
14609
|
+
const thisYear = new Date().getFullYear();
|
|
14081
14610
|
const rows = [];
|
|
14082
14611
|
for (let row = 0; row < this.yearRowCount; row++) {
|
|
14083
14612
|
const yearRow = [];
|
|
14084
14613
|
for (let col = 0; col < this.yearsPerRow; col++) {
|
|
14085
14614
|
const year = range.start + (row * this.yearsPerRow) + col;
|
|
14086
|
-
yearRow.push(this.createYearCell(year));
|
|
14615
|
+
yearRow.push(this.createYearCell(year, formatYear, thisYear));
|
|
14087
14616
|
}
|
|
14088
14617
|
rows.push(yearRow);
|
|
14089
14618
|
}
|
|
14090
14619
|
return rows;
|
|
14091
14620
|
}, ...(ngDevMode ? [{ debugName: "yearRows" }] : []));
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14621
|
+
/**
|
|
14622
|
+
* The year that carries `tabindex="0"` — the grid's single keyboard entry point, per
|
|
14623
|
+
* the roving tabindex pattern. Follows `activeDate`, falling back to the nearest
|
|
14624
|
+
* enabled year in the page so a range whose active year is disabled still has a
|
|
14625
|
+
* tabbable cell. `null` only when every year on the page is disabled.
|
|
14626
|
+
*/
|
|
14627
|
+
activeYear = computed(() => {
|
|
14628
|
+
const range = this.yearRange();
|
|
14629
|
+
// Always inside the page: the range is derived from this same year.
|
|
14630
|
+
const wanted = this.activeDate().getFullYear();
|
|
14631
|
+
if (this.isYearEnabled(wanted)) {
|
|
14632
|
+
return wanted;
|
|
14633
|
+
}
|
|
14634
|
+
// Spiral outwards from the year we wanted, forward first.
|
|
14635
|
+
const span = range.end - range.start;
|
|
14636
|
+
for (let offset = 1; offset <= span; offset++) {
|
|
14637
|
+
if (wanted + offset <= range.end && this.isYearEnabled(wanted + offset)) {
|
|
14638
|
+
return wanted + offset;
|
|
14639
|
+
}
|
|
14640
|
+
if (wanted - offset >= range.start && this.isYearEnabled(wanted - offset)) {
|
|
14641
|
+
return wanted - offset;
|
|
14642
|
+
}
|
|
14643
|
+
}
|
|
14644
|
+
return null;
|
|
14645
|
+
}, ...(ngDevMode ? [{ debugName: "activeYear" }] : []));
|
|
14646
|
+
createYearCell(year, formatYear, thisYear) {
|
|
14095
14647
|
const selectedYear = this.selected()?.getFullYear();
|
|
14096
|
-
const isToday = year ===
|
|
14648
|
+
const isToday = year === thisYear;
|
|
14097
14649
|
const isSelected = year === selectedYear;
|
|
14098
14650
|
const enabled = this.isYearEnabled(year);
|
|
14099
14651
|
return {
|
|
14100
|
-
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
ariaLabel: this.formatYearAriaLabel(year, isSelected, isToday),
|
|
14652
|
+
year,
|
|
14653
|
+
label: formatYear(year),
|
|
14654
|
+
ariaLabel: this.formatYearAriaLabel(formatYear(year), isToday),
|
|
14104
14655
|
enabled,
|
|
14105
14656
|
selected: isSelected,
|
|
14106
14657
|
today: isToday,
|
|
@@ -14116,7 +14667,11 @@ class TnMultiYearViewComponent {
|
|
|
14116
14667
|
if (maxDate && year > maxDate.getFullYear()) {
|
|
14117
14668
|
return false;
|
|
14118
14669
|
}
|
|
14119
|
-
//
|
|
14670
|
+
// Deliberately coarse: a year is a single cell, so it gets a single verdict, and
|
|
14671
|
+
// January 1st stands in for the whole year. A filter that rules out January 1st
|
|
14672
|
+
// while allowing the rest of the year therefore disables the year outright. Material
|
|
14673
|
+
// approximates the same way; testing all 365 days per cell is not worth it, and the
|
|
14674
|
+
// month view applies the filter exactly once the user drills in.
|
|
14120
14675
|
if (dateFilter) {
|
|
14121
14676
|
const testDate = new Date(year, 0, 1);
|
|
14122
14677
|
if (!dateFilter(testDate)) {
|
|
@@ -14125,13 +14680,15 @@ class TnMultiYearViewComponent {
|
|
|
14125
14680
|
}
|
|
14126
14681
|
return true;
|
|
14127
14682
|
}
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
|
|
14683
|
+
/**
|
|
14684
|
+
* Selection is carried by the button's `aria-pressed`, so it is deliberately not
|
|
14685
|
+
* repeated here. "Current year" stays: `aria-current="date"` on a year cell is vague
|
|
14686
|
+
* enough that spelling it out earns its place.
|
|
14687
|
+
*/
|
|
14688
|
+
formatYearAriaLabel(year, isToday) {
|
|
14689
|
+
let label = year;
|
|
14133
14690
|
if (isToday) {
|
|
14134
|
-
label +=
|
|
14691
|
+
label += ` ${this.intl().currentYear}`;
|
|
14135
14692
|
}
|
|
14136
14693
|
return label;
|
|
14137
14694
|
}
|
|
@@ -14143,19 +14700,92 @@ class TnMultiYearViewComponent {
|
|
|
14143
14700
|
}
|
|
14144
14701
|
onYearClicked(cell) {
|
|
14145
14702
|
if (cell.enabled) {
|
|
14146
|
-
//
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
this.selectedChange.emit(newDate);
|
|
14703
|
+
// Keeps the month and day, clamped: moving off 29 February into a non-leap year
|
|
14704
|
+
// would otherwise roll over into March and change the month too.
|
|
14705
|
+
this.selectedChange.emit(withYear(this.activeDate(), cell.year));
|
|
14150
14706
|
}
|
|
14151
14707
|
}
|
|
14708
|
+
/**
|
|
14709
|
+
* Moves the roving tabindex across the year grid. Picking a year stays on
|
|
14710
|
+
* click/Enter/Space, which the cells are already buttons for.
|
|
14711
|
+
*
|
|
14712
|
+
* Moves past either end of the 24-year page emit an `activeDateChange` like the
|
|
14713
|
+
* header's paging buttons do, and the grid re-renders on the neighbouring page.
|
|
14714
|
+
*/
|
|
14715
|
+
onKeydown(event) {
|
|
14716
|
+
const from = this.activeYear();
|
|
14717
|
+
if (from === null) {
|
|
14718
|
+
return;
|
|
14719
|
+
}
|
|
14720
|
+
const move = this.targetForKey(event, from);
|
|
14721
|
+
if (move === null) {
|
|
14722
|
+
return;
|
|
14723
|
+
}
|
|
14724
|
+
// Claim the key before the browser scrolls the page with it.
|
|
14725
|
+
event.preventDefault();
|
|
14726
|
+
const landing = this.nearestEnabledYear(move.year, move.search);
|
|
14727
|
+
if (landing === null || landing === from) {
|
|
14728
|
+
return;
|
|
14729
|
+
}
|
|
14730
|
+
this.activeDateChange.emit(withYear(this.activeDate(), landing));
|
|
14731
|
+
focusActiveCellAfterRender(this.host, this.injector);
|
|
14732
|
+
}
|
|
14733
|
+
/**
|
|
14734
|
+
* The year a key aims at, and which way to look from there when that year is disabled.
|
|
14735
|
+
*
|
|
14736
|
+
* The search direction belongs to the key, not to where the target happens to fall
|
|
14737
|
+
* relative to the current year. Home and End mean "the ends of *this* page", so they
|
|
14738
|
+
* search inward: looking outward would step onto the neighbouring 24-year page over a
|
|
14739
|
+
* single ruled-out boundary year and turn the page, which is not what either key
|
|
14740
|
+
* promises. The arrows and the paging keys keep travelling the way they were already
|
|
14741
|
+
* headed, and moving onto the next page is the point of those.
|
|
14742
|
+
*/
|
|
14743
|
+
targetForKey(event, from) {
|
|
14744
|
+
const range = this.yearRange();
|
|
14745
|
+
const yearsPerPage = range.end - range.start + 1;
|
|
14746
|
+
switch (event.key) {
|
|
14747
|
+
case 'ArrowLeft': return { year: from - 1, search: -1 };
|
|
14748
|
+
case 'ArrowRight': return { year: from + 1, search: 1 };
|
|
14749
|
+
case 'ArrowUp': return { year: from - this.yearsPerRow, search: -1 };
|
|
14750
|
+
case 'ArrowDown': return { year: from + this.yearsPerRow, search: 1 };
|
|
14751
|
+
case 'Home': return { year: range.start, search: 1 };
|
|
14752
|
+
case 'End': return { year: range.end, search: -1 };
|
|
14753
|
+
case 'PageUp': return { year: from - yearsPerPage, search: -1 };
|
|
14754
|
+
case 'PageDown': return { year: from + yearsPerPage, search: 1 };
|
|
14755
|
+
default: return null;
|
|
14756
|
+
}
|
|
14757
|
+
}
|
|
14758
|
+
/**
|
|
14759
|
+
* Steps past years the caller has disabled, so `minDate`/`maxDate` don't park focus
|
|
14760
|
+
* somewhere unusable.
|
|
14761
|
+
*
|
|
14762
|
+
* Searches the way the key asked for (see `targetForKey`), then doubles back if that
|
|
14763
|
+
* finds nothing. The fallback is for the ends of the allowed range: arrowing right at
|
|
14764
|
+
* `maxDate` finds nothing ahead, doubles back onto the year we started from, and the
|
|
14765
|
+
* caller reads that as "don't move".
|
|
14766
|
+
*/
|
|
14767
|
+
nearestEnabledYear(target, preferred) {
|
|
14768
|
+
return this.scanForEnabledYear(target, preferred)
|
|
14769
|
+
?? this.scanForEnabledYear(target, preferred === 1 ? -1 : 1);
|
|
14770
|
+
}
|
|
14771
|
+
/** Bounded, so an everything-disabled range stops the move instead of spinning. */
|
|
14772
|
+
scanForEnabledYear(from, direction) {
|
|
14773
|
+
const maxSteps = 48;
|
|
14774
|
+
for (let step = 0; step <= maxSteps; step++) {
|
|
14775
|
+
const candidate = from + (step * direction);
|
|
14776
|
+
if (this.isYearEnabled(candidate)) {
|
|
14777
|
+
return candidate;
|
|
14778
|
+
}
|
|
14779
|
+
}
|
|
14780
|
+
return null;
|
|
14781
|
+
}
|
|
14152
14782
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMultiYearViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14153
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMultiYearViewComponent, isStandalone: true, selector: "tn-multi-year-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\"
|
|
14783
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMultiYearViewComponent, isStandalone: true, selector: "tn-multi-year-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-year]=\"cell.year\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.year === activeYear()\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.year === activeYear() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onYearClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\">\n {{ cell.label }}\n </span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{--calendar-radius: 4px;--calendar-year-row-height: 48px;--calendar-year-indicator-width: 72px;--calendar-year-indicator-height: 36px}.tn-calendar-table{width:100%;min-width:min(100%,224px);table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header-divider{height:16px;border:none}.tn-calendar-body-cell-container{position:relative;border:none;height:var(--calendar-year-row-height);width:25%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;border:none;background:transparent;color:var(--tn-fg1, #333);cursor:pointer;font-size:14px;font-weight:500}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-year-indicator-width);height:var(--calendar-year-indicator-height);max-width:100%;border-radius:var(--calendar-radius);transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):not(.tn-calendar-body-selected):hover .tn-calendar-body-cell-content{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected) .tn-calendar-body-cell-content{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected .tn-calendar-body-cell-content{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}\n"] });
|
|
14154
14784
|
}
|
|
14155
14785
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMultiYearViewComponent, decorators: [{
|
|
14156
14786
|
type: Component,
|
|
14157
|
-
args: [{ selector: 'tn-multi-year-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\"
|
|
14158
|
-
}], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }] } });
|
|
14787
|
+
args: [{ selector: 'tn-multi-year-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-year]=\"cell.year\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.year === activeYear()\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.year === activeYear() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onYearClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\">\n {{ cell.label }}\n </span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{--calendar-radius: 4px;--calendar-year-row-height: 48px;--calendar-year-indicator-width: 72px;--calendar-year-indicator-height: 36px}.tn-calendar-table{width:100%;min-width:min(100%,224px);table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header-divider{height:16px;border:none}.tn-calendar-body-cell-container{position:relative;border:none;height:var(--calendar-year-row-height);width:25%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;border:none;background:transparent;color:var(--tn-fg1, #333);cursor:pointer;font-size:14px;font-weight:500}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-year-indicator-width);height:var(--calendar-year-indicator-height);max-width:100%;border-radius:var(--calendar-radius);transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):not(.tn-calendar-body-selected):hover .tn-calendar-body-cell-content{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected) .tn-calendar-body-cell-content{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected .tn-calendar-body-cell-content{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}\n"] }]
|
|
14788
|
+
}], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }] } });
|
|
14159
14789
|
|
|
14160
14790
|
class TnCalendarComponent {
|
|
14161
14791
|
startView = input('month', ...(ngDevMode ? [{ debugName: "startView" }] : []));
|
|
@@ -14163,116 +14793,112 @@ class TnCalendarComponent {
|
|
|
14163
14793
|
minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
|
|
14164
14794
|
maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
|
|
14165
14795
|
dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
|
|
14796
|
+
/**
|
|
14797
|
+
* Dates to flag as noteworthy — days a task runs, days with events, and the like.
|
|
14798
|
+
* Order and time-of-day are ignored; only the calendar day is compared. The
|
|
14799
|
+
* calendar owns how a marked day looks, so callers pass dates rather than styles.
|
|
14800
|
+
* Marking is independent of `selected`/`selectedRange`, which stay authoritative
|
|
14801
|
+
* where they overlap.
|
|
14802
|
+
*/
|
|
14803
|
+
markedDates = input(undefined, ...(ngDevMode ? [{ debugName: "markedDates" }] : []));
|
|
14804
|
+
/**
|
|
14805
|
+
* Which month — or which page of years, in the year view — is on screen.
|
|
14806
|
+
*
|
|
14807
|
+
* Optional. Left unbound, the calendar opens on the month the bound value lives in and
|
|
14808
|
+
* drives navigation itself. Bind it, alongside `activeDateChange`, to drive the view
|
|
14809
|
+
* from outside: useful for a calendar that stays mounted while its value jumps to
|
|
14810
|
+
* another month, which the open-on-init behaviour alone won't follow.
|
|
14811
|
+
*
|
|
14812
|
+
* Unlike `selected`, this isn't strictly controlled — the calendar still navigates on
|
|
14813
|
+
* its own when the user pages or arrows around, so binding a constant here won't
|
|
14814
|
+
* freeze the view. Changing what you bind always wins.
|
|
14815
|
+
*/
|
|
14816
|
+
activeDate = input(undefined, ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
|
|
14817
|
+
/**
|
|
14818
|
+
* Locale for dates, month and weekday names, and which day the week starts on.
|
|
14819
|
+
*
|
|
14820
|
+
* Defaults to the app's `LOCALE_ID`, which is where an Angular app already declares
|
|
14821
|
+
* its locale — so a `ng build --localize` app is right without touching this. Bind it
|
|
14822
|
+
* only when one calendar needs a different locale from the rest of the app.
|
|
14823
|
+
*
|
|
14824
|
+
* Nothing here reads the browser's language on its own: doing so would quietly
|
|
14825
|
+
* disagree with `DatePipe` and every other locale-aware part of the app. An app that
|
|
14826
|
+
* wants that behaviour opts into it once, at bootstrap:
|
|
14827
|
+
* `{ provide: LOCALE_ID, useValue: navigator.language }`.
|
|
14828
|
+
*
|
|
14829
|
+
* Wording that isn't a date — "(marked)", the header button labels — comes from
|
|
14830
|
+
* `TN_CALENDAR_INTL` instead.
|
|
14831
|
+
*/
|
|
14832
|
+
locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
14833
|
+
appLocale = inject(LOCALE_ID);
|
|
14834
|
+
host = inject(ElementRef);
|
|
14835
|
+
injector = inject(Injector);
|
|
14836
|
+
/** The locale actually in force: the input when bound, the app's otherwise. */
|
|
14837
|
+
resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
|
|
14166
14838
|
// Range mode inputs
|
|
14167
14839
|
rangeMode = input(false, ...(ngDevMode ? [{ debugName: "rangeMode" }] : []));
|
|
14168
14840
|
selectedRange = input(undefined, ...(ngDevMode ? [{ debugName: "selectedRange" }] : []));
|
|
14169
14841
|
selectedChange = output();
|
|
14842
|
+
/**
|
|
14843
|
+
* The date the calendar is now sitting on — the month it shows, and the day holding
|
|
14844
|
+
* the grid's roving tabindex.
|
|
14845
|
+
*
|
|
14846
|
+
* This is navigation, not selection: it fires on paging, on arrow keys, and on
|
|
14847
|
+
* clicking a day (a click moves the active cell as well as choosing the date, so the
|
|
14848
|
+
* two never drift apart). Treat it as "the view moved", not "the user picked a date" —
|
|
14849
|
+
* `selectedChange` is the one that means a choice was made.
|
|
14850
|
+
*/
|
|
14170
14851
|
activeDateChange = output();
|
|
14171
14852
|
viewChanged = output();
|
|
14172
14853
|
// Range mode outputs
|
|
14173
14854
|
selectedRangeChange = output();
|
|
14174
|
-
|
|
14855
|
+
/**
|
|
14856
|
+
* Which month/year grid is on screen. Navigation state, not selection state.
|
|
14857
|
+
*
|
|
14858
|
+
* Follows the `activeDate` input whenever the caller changes it, but stays writable so
|
|
14859
|
+
* paging and arrow keys keep working with no echo back from the caller.
|
|
14860
|
+
*/
|
|
14861
|
+
currentDate = linkedSignal({ ...(ngDevMode ? { debugName: "currentDate" } : {}), source: () => this.activeDate(),
|
|
14862
|
+
computation: (activeDate, previous) => activeDate ?? previous?.value ?? new Date() });
|
|
14175
14863
|
currentView = signal('month', ...(ngDevMode ? [{ debugName: "currentView" }] : []));
|
|
14176
|
-
// Range selection state - this is the authoritative source for calendar display
|
|
14177
|
-
rangeState = signal({
|
|
14178
|
-
start: null,
|
|
14179
|
-
end: null,
|
|
14180
|
-
selecting: 'start'
|
|
14181
|
-
}, ...(ngDevMode ? [{ debugName: "rangeState" }] : []));
|
|
14182
|
-
// Track if user has interacted with calendar - once true, ignore external selectedRange
|
|
14183
|
-
userHasInteracted = false;
|
|
14184
|
-
constructor() {
|
|
14185
|
-
// Watch for changes to selectedRange input
|
|
14186
|
-
effect(() => {
|
|
14187
|
-
// Track signals to re-run effect when they change
|
|
14188
|
-
this.selectedRange();
|
|
14189
|
-
const rangeMode = this.rangeMode();
|
|
14190
|
-
// Only update range state from external selectedRange if user hasn't interacted yet
|
|
14191
|
-
if (!this.userHasInteracted && rangeMode) {
|
|
14192
|
-
this.initializeRangeState();
|
|
14193
|
-
}
|
|
14194
|
-
});
|
|
14195
|
-
}
|
|
14196
14864
|
ngOnInit() {
|
|
14197
14865
|
this.currentView.set(this.startView());
|
|
14198
|
-
//
|
|
14199
|
-
if (this.
|
|
14200
|
-
|
|
14201
|
-
}
|
|
14202
|
-
else {
|
|
14203
|
-
const selected = this.selected();
|
|
14204
|
-
if (selected) {
|
|
14205
|
-
// For single date mode, navigate to the selected date's month
|
|
14206
|
-
this.currentDate.set(new Date(selected));
|
|
14207
|
-
}
|
|
14866
|
+
// A bound activeDate already says what should be on screen.
|
|
14867
|
+
if (this.activeDate()) {
|
|
14868
|
+
return;
|
|
14208
14869
|
}
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
start: selectedRange.start,
|
|
14216
|
-
end: selectedRange.end,
|
|
14217
|
-
selecting: selectedRange.start && selectedRange.end ? 'start' :
|
|
14218
|
-
selectedRange.start ? 'end' : 'start'
|
|
14219
|
-
});
|
|
14220
|
-
// Navigate to the month of the selected start date, or end date if no start date
|
|
14221
|
-
const dateToShow = selectedRange.start || selectedRange.end;
|
|
14222
|
-
if (dateToShow) {
|
|
14223
|
-
this.currentDate.set(new Date(dateToShow));
|
|
14224
|
-
}
|
|
14225
|
-
}
|
|
14226
|
-
else {
|
|
14227
|
-
// No selected range - initialize empty range state
|
|
14228
|
-
this.rangeState.set({
|
|
14229
|
-
start: null,
|
|
14230
|
-
end: null,
|
|
14231
|
-
selecting: 'start'
|
|
14232
|
-
});
|
|
14233
|
-
}
|
|
14870
|
+
// Otherwise open on the month the caller's value lives in.
|
|
14871
|
+
const initialDate = this.rangeMode()
|
|
14872
|
+
? (this.selectedRange()?.start ?? this.selectedRange()?.end ?? null)
|
|
14873
|
+
: (this.selected() ?? null);
|
|
14874
|
+
if (initialDate) {
|
|
14875
|
+
this.currentDate.set(new Date(initialDate));
|
|
14234
14876
|
}
|
|
14235
14877
|
}
|
|
14236
|
-
onMonthSelected(month) {
|
|
14237
|
-
const newDate = new Date(this.currentDate());
|
|
14238
|
-
newDate.setMonth(month);
|
|
14239
|
-
this.currentDate.set(newDate);
|
|
14240
|
-
this.currentView.set('month');
|
|
14241
|
-
this.viewChanged.emit('month');
|
|
14242
|
-
}
|
|
14243
|
-
onYearSelected(year) {
|
|
14244
|
-
const newDate = new Date(this.currentDate());
|
|
14245
|
-
newDate.setFullYear(year);
|
|
14246
|
-
this.currentDate.set(newDate);
|
|
14247
|
-
this.activeDateChange.emit(newDate);
|
|
14248
|
-
}
|
|
14249
14878
|
onViewChanged(view) {
|
|
14250
14879
|
this.currentView.set(view);
|
|
14251
14880
|
this.viewChanged.emit(view);
|
|
14252
14881
|
}
|
|
14253
14882
|
onPreviousClicked() {
|
|
14254
|
-
|
|
14255
|
-
let newDate;
|
|
14256
|
-
if (this.currentView() === 'month') {
|
|
14257
|
-
newDate = new Date(current.getFullYear(), current.getMonth() - 1, 1);
|
|
14258
|
-
}
|
|
14259
|
-
else {
|
|
14260
|
-
// For year view, navigate by 24-year ranges (like Material)
|
|
14261
|
-
newDate = new Date(current.getFullYear() - 24, current.getMonth(), 1);
|
|
14262
|
-
}
|
|
14263
|
-
this.currentDate.set(newDate);
|
|
14264
|
-
this.activeDateChange.emit(newDate);
|
|
14883
|
+
this.page(-1);
|
|
14265
14884
|
}
|
|
14266
14885
|
onNextClicked() {
|
|
14886
|
+
this.page(1);
|
|
14887
|
+
}
|
|
14888
|
+
/**
|
|
14889
|
+
* Steps the view one page in `direction`: a month in the day view, a page of years in
|
|
14890
|
+
* the year view — the same spans `PageUp`/`PageDown` move, and the same the header
|
|
14891
|
+
* labels.
|
|
14892
|
+
*
|
|
14893
|
+
* Carries the day across, clamped, rather than dropping to the 1st. The keyboard has
|
|
14894
|
+
* always done this, so resetting here left the roving tabindex somewhere different
|
|
14895
|
+
* depending on whether the user clicked the arrow or pressed the key.
|
|
14896
|
+
*/
|
|
14897
|
+
page(direction) {
|
|
14267
14898
|
const current = this.currentDate();
|
|
14268
|
-
|
|
14269
|
-
|
|
14270
|
-
|
|
14271
|
-
}
|
|
14272
|
-
else {
|
|
14273
|
-
// For year view, navigate by 24-year ranges (like Material)
|
|
14274
|
-
newDate = new Date(current.getFullYear() + 24, current.getMonth(), 1);
|
|
14275
|
-
}
|
|
14899
|
+
const newDate = this.currentView() === 'month'
|
|
14900
|
+
? addMonths(current, direction)
|
|
14901
|
+
: withYear(current, current.getFullYear() + (direction * YEARS_PER_PAGE));
|
|
14276
14902
|
this.currentDate.set(newDate);
|
|
14277
14903
|
this.activeDateChange.emit(newDate);
|
|
14278
14904
|
}
|
|
@@ -14284,83 +14910,346 @@ class TnCalendarComponent {
|
|
|
14284
14910
|
this.selectedChange.emit(date);
|
|
14285
14911
|
}
|
|
14286
14912
|
}
|
|
14913
|
+
/**
|
|
14914
|
+
* Works out the next range from the one the caller currently owns and emits it. The
|
|
14915
|
+
* calendar keeps no copy — what it renders next is whatever the caller binds back.
|
|
14916
|
+
*/
|
|
14287
14917
|
handleRangeSelection(date) {
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
const
|
|
14291
|
-
//
|
|
14292
|
-
if (
|
|
14293
|
-
const newRangeState = {
|
|
14294
|
-
start: date,
|
|
14295
|
-
end: null,
|
|
14296
|
-
selecting: 'end'
|
|
14297
|
-
};
|
|
14298
|
-
this.rangeState.set(newRangeState);
|
|
14918
|
+
const current = this.selectedRange();
|
|
14919
|
+
const start = current?.start ?? null;
|
|
14920
|
+
const end = current?.end ?? null;
|
|
14921
|
+
// Nothing pending, or the range is already complete: begin a new one.
|
|
14922
|
+
if (!start || end) {
|
|
14299
14923
|
this.selectedRangeChange.emit({ start: date, end: null });
|
|
14300
14924
|
return;
|
|
14301
14925
|
}
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
end: null,
|
|
14307
|
-
selecting: 'end'
|
|
14308
|
-
};
|
|
14309
|
-
this.rangeState.set(newRangeState);
|
|
14926
|
+
// A second pick before the first restarts the range rather than inverting it.
|
|
14927
|
+
// Compared by day: the clicked date is midnight, but the caller's start may carry a
|
|
14928
|
+
// time of day, which would otherwise make re-picking the same day restart the range.
|
|
14929
|
+
if (compareDays(date, start) < 0) {
|
|
14310
14930
|
this.selectedRangeChange.emit({ start: date, end: null });
|
|
14931
|
+
return;
|
|
14311
14932
|
}
|
|
14312
|
-
|
|
14313
|
-
// Setting end date
|
|
14314
|
-
const start = currentRange.start;
|
|
14315
|
-
// If second date is earlier than first, treat it as new start date
|
|
14316
|
-
if (date < start) {
|
|
14317
|
-
const newRangeState = {
|
|
14318
|
-
start: date,
|
|
14319
|
-
end: null,
|
|
14320
|
-
selecting: 'end'
|
|
14321
|
-
};
|
|
14322
|
-
this.rangeState.set(newRangeState);
|
|
14323
|
-
this.selectedRangeChange.emit({ start: date, end: null });
|
|
14324
|
-
}
|
|
14325
|
-
else {
|
|
14326
|
-
// Valid end date - complete the range
|
|
14327
|
-
const newRangeState = {
|
|
14328
|
-
start: start,
|
|
14329
|
-
end: date,
|
|
14330
|
-
selecting: 'start'
|
|
14331
|
-
};
|
|
14332
|
-
this.rangeState.set(newRangeState);
|
|
14333
|
-
this.selectedRangeChange.emit({ start: start, end: date });
|
|
14334
|
-
}
|
|
14335
|
-
}
|
|
14933
|
+
this.selectedRangeChange.emit({ start, end: date });
|
|
14336
14934
|
}
|
|
14337
14935
|
onActiveDateChange(date) {
|
|
14338
14936
|
this.currentDate.set(date);
|
|
14339
14937
|
this.activeDateChange.emit(date);
|
|
14340
14938
|
}
|
|
14939
|
+
/**
|
|
14940
|
+
* A year picked in the year grid moves the view to that year and returns to the day
|
|
14941
|
+
* grid. It is navigation, not selection — no `selectedChange` — so the user lands on
|
|
14942
|
+
* the month they asked for and picks a day from there.
|
|
14943
|
+
*/
|
|
14341
14944
|
onYearSelectedFromView(date) {
|
|
14342
|
-
// When a year is selected from the multi-year view, update the current date
|
|
14343
|
-
// and switch back to month view
|
|
14344
14945
|
this.currentDate.set(date);
|
|
14345
14946
|
this.currentView.set('month');
|
|
14346
14947
|
this.viewChanged.emit('month');
|
|
14347
14948
|
this.activeDateChange.emit(date);
|
|
14348
|
-
|
|
14349
|
-
|
|
14350
|
-
|
|
14351
|
-
*/
|
|
14352
|
-
resetInteractionState() {
|
|
14353
|
-
this.userHasInteracted = false;
|
|
14354
|
-
// Reinitialize range state from selectedRange if provided
|
|
14355
|
-
this.initializeRangeState();
|
|
14949
|
+
// Switching grids destroys the year cell the user just activated, which drops focus
|
|
14950
|
+
// to `<body>` and restarts the tab order. Carry it over to the day now active.
|
|
14951
|
+
focusActiveCellAfterRender(this.host, this.injector);
|
|
14356
14952
|
}
|
|
14357
14953
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14358
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCalendarComponent, isStandalone: true, selector: "tn-calendar", inputs: { startView: { classPropertyName: "startView", publicName: "startView", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange", viewChanged: "viewChanged", selectedRangeChange: "selectedRangeChange" }, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [
|
|
14954
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCalendarComponent, isStandalone: true, selector: "tn-calendar", inputs: { startView: { classPropertyName: "startView", publicName: "startView", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, markedDates: { classPropertyName: "markedDates", publicName: "markedDates", isSignal: true, isRequired: false, transformFunction: null }, activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange", viewChanged: "viewChanged", selectedRangeChange: "selectedRangeChange" }, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [currentView]=\"currentView()\"\n (viewChanged)=\"onViewChanged($event)\"\n (previousClicked)=\"onPreviousClicked()\"\n (nextClicked)=\"onNextClicked()\" />\n\n<div class=\"tn-calendar-content\">\n @if (currentView() === 'month') {\n <tn-month-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [markedDates]=\"markedDates()\"\n [rangeMode]=\"rangeMode()\"\n [selectedRange]=\"rangeMode() ? selectedRange() : undefined\"\n (selectedChange)=\"onSelectedChange($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n\n <!-- Multi-year view -->\n @if (currentView() === 'year') {\n <tn-multi-year-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n (selectedChange)=\"onYearSelectedFromView($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n</div>\n", styles: [":host{display:block;background:var(--tn-bg2, #f5f5f5);color:var(--tn-fg1, #333);padding:0 8px 8px;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014}.tn-calendar-content{padding:8px;outline:none}.tn-year-view{text-align:center;padding:20px;color:var(--tn-fg2, #666)}\n"], dependencies: [{ kind: "component", type: TnCalendarHeaderComponent, selector: "tn-calendar-header", inputs: ["currentDate", "currentView", "locale"], outputs: ["viewChanged", "previousClicked", "nextClicked"] }, { kind: "component", type: TnMonthViewComponent, selector: "tn-month-view", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter", "markedDates", "rangeMode", "selectedRange", "locale"], outputs: ["selectedChange", "activeDateChange"] }, { kind: "component", type: TnMultiYearViewComponent, selector: "tn-multi-year-view", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter", "locale"], outputs: ["selectedChange", "activeDateChange"] }] });
|
|
14359
14955
|
}
|
|
14360
14956
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarComponent, decorators: [{
|
|
14361
14957
|
type: Component,
|
|
14362
|
-
args: [{ selector: 'tn-calendar', standalone: true, imports: [TnCalendarHeaderComponent, TnMonthViewComponent, TnMultiYearViewComponent], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [
|
|
14363
|
-
}],
|
|
14958
|
+
args: [{ selector: 'tn-calendar', standalone: true, imports: [TnCalendarHeaderComponent, TnMonthViewComponent, TnMultiYearViewComponent], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [currentView]=\"currentView()\"\n (viewChanged)=\"onViewChanged($event)\"\n (previousClicked)=\"onPreviousClicked()\"\n (nextClicked)=\"onNextClicked()\" />\n\n<div class=\"tn-calendar-content\">\n @if (currentView() === 'month') {\n <tn-month-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [markedDates]=\"markedDates()\"\n [rangeMode]=\"rangeMode()\"\n [selectedRange]=\"rangeMode() ? selectedRange() : undefined\"\n (selectedChange)=\"onSelectedChange($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n\n <!-- Multi-year view -->\n @if (currentView() === 'year') {\n <tn-multi-year-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n (selectedChange)=\"onYearSelectedFromView($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n</div>\n", styles: [":host{display:block;background:var(--tn-bg2, #f5f5f5);color:var(--tn-fg1, #333);padding:0 8px 8px;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014}.tn-calendar-content{padding:8px;outline:none}.tn-year-view{text-align:center;padding:20px;color:var(--tn-fg2, #666)}\n"] }]
|
|
14959
|
+
}], propDecorators: { startView: [{ type: i0.Input, args: [{ isSignal: true, alias: "startView", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], markedDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "markedDates", required: false }] }], activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }], viewChanged: [{ type: i0.Output, args: ["viewChanged"] }], selectedRangeChange: [{ type: i0.Output, args: ["selectedRangeChange"] }] } });
|
|
14960
|
+
|
|
14961
|
+
/**
|
|
14962
|
+
* Harness for interacting with a single day (or year) cell inside `tn-calendar`.
|
|
14963
|
+
*
|
|
14964
|
+
* Modelled on Angular Material's `MatCalendarCellHarness` so specs can move across
|
|
14965
|
+
* with the method names intact. Three differences worth knowing:
|
|
14966
|
+
*
|
|
14967
|
+
* - `isMarked()` is new — it reports the `markedDates` state, which Material has no
|
|
14968
|
+
* equivalent for.
|
|
14969
|
+
* - `getDate()` and the `date` filter are new, and are the reliable way to name a cell:
|
|
14970
|
+
* the text is formatted for the app's locale, so `'14'` misses a calendar rendering
|
|
14971
|
+
* `١٤`.
|
|
14972
|
+
* - There are no comparison-range or preview-range methods (`isComparisonRangeStart()`
|
|
14973
|
+
* and friends); `tn-calendar` has no comparison range to report.
|
|
14974
|
+
*
|
|
14975
|
+
* @example
|
|
14976
|
+
* ```typescript
|
|
14977
|
+
* const calendar = await loader.getHarness(TnCalendarHarness);
|
|
14978
|
+
* const [first] = await calendar.getCells({ date: new Date(2031, 4, 14) });
|
|
14979
|
+
* await first.select();
|
|
14980
|
+
* expect(await first.isSelected()).toBe(true);
|
|
14981
|
+
* ```
|
|
14982
|
+
*/
|
|
14983
|
+
class TnCalendarCellHarness extends ComponentHarness {
|
|
14984
|
+
/** The selector for a calendar cell. Padding cells render no button, so only real days match. */
|
|
14985
|
+
static hostSelector = '.tn-calendar-body-cell';
|
|
14986
|
+
_content = this.locatorFor('.tn-calendar-body-cell-content');
|
|
14987
|
+
/**
|
|
14988
|
+
* Gets a `HarnessPredicate` that can be used to search for a cell with specific attributes.
|
|
14989
|
+
*
|
|
14990
|
+
* @param options Options for filtering which cell instances are considered a match.
|
|
14991
|
+
* @returns A `HarnessPredicate` configured with the given options.
|
|
14992
|
+
*
|
|
14993
|
+
* @example
|
|
14994
|
+
* ```typescript
|
|
14995
|
+
* // Every day the schedule runs on
|
|
14996
|
+
* const runDays = await calendar.getCells({ marked: true });
|
|
14997
|
+
*
|
|
14998
|
+
* // The disabled days before minDate
|
|
14999
|
+
* const blocked = await calendar.getCells({ disabled: true });
|
|
15000
|
+
* ```
|
|
15001
|
+
*/
|
|
15002
|
+
static with(options = {}) {
|
|
15003
|
+
return new HarnessPredicate(TnCalendarCellHarness, options)
|
|
15004
|
+
.addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
|
|
15005
|
+
.addOption('date', options.date, async (harness, date) => (await harness.getDateStamp()) === isoDateString(date))
|
|
15006
|
+
.addOption('year', options.year, async (harness, year) => (await harness.getYear()) === year)
|
|
15007
|
+
.addOption('selected', options.selected, async (harness, selected) => (await harness.isSelected()) === selected)
|
|
15008
|
+
.addOption('active', options.active, async (harness, active) => (await harness.isActive()) === active)
|
|
15009
|
+
.addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled)
|
|
15010
|
+
.addOption('today', options.today, async (harness, today) => (await harness.isToday()) === today)
|
|
15011
|
+
.addOption('inRange', options.inRange, async (harness, inRange) => (await harness.isInRange()) === inRange)
|
|
15012
|
+
.addOption('marked', options.marked, async (harness, marked) => (await harness.isMarked()) === marked);
|
|
15013
|
+
}
|
|
15014
|
+
/** Gets the text of the calendar cell. */
|
|
15015
|
+
async getText() {
|
|
15016
|
+
return (await this._content()).text();
|
|
15017
|
+
}
|
|
15018
|
+
/** Gets the aria-label of the calendar cell. */
|
|
15019
|
+
async getAriaLabel() {
|
|
15020
|
+
return (await this.host()).getAttribute('aria-label').then((label) => label ?? '');
|
|
15021
|
+
}
|
|
15022
|
+
/**
|
|
15023
|
+
* Gets the calendar day this cell represents, independent of how it is rendered —
|
|
15024
|
+
* `getText()` is formatted for the locale, so it reads `١٤` in Arabic while this still
|
|
15025
|
+
* answers the 14th. Midnight local time. `null` on a year cell, which has no day.
|
|
15026
|
+
*/
|
|
15027
|
+
async getDate() {
|
|
15028
|
+
const stamp = await this.getDateStamp();
|
|
15029
|
+
if (!stamp) {
|
|
15030
|
+
return null;
|
|
15031
|
+
}
|
|
15032
|
+
const [year, month, day] = stamp.split('-').map(Number);
|
|
15033
|
+
return new Date(year, month - 1, day);
|
|
15034
|
+
}
|
|
15035
|
+
/**
|
|
15036
|
+
* Gets the year this cell represents, independent of how it is rendered. Works on year
|
|
15037
|
+
* cells; `null` on a day cell.
|
|
15038
|
+
*/
|
|
15039
|
+
async getYear() {
|
|
15040
|
+
const year = await (await this.host()).getAttribute('data-tn-year');
|
|
15041
|
+
return year === null ? null : Number(year);
|
|
15042
|
+
}
|
|
15043
|
+
/** The raw `YYYY-MM-DD` stamp, for comparing without rebuilding a `Date`. */
|
|
15044
|
+
async getDateStamp() {
|
|
15045
|
+
return (await this.host()).getAttribute('data-tn-date');
|
|
15046
|
+
}
|
|
15047
|
+
/** Whether the cell is selected. */
|
|
15048
|
+
async isSelected() {
|
|
15049
|
+
return (await this.host()).hasClass('tn-calendar-body-selected');
|
|
15050
|
+
}
|
|
15051
|
+
/** Whether the cell is disabled. */
|
|
15052
|
+
async isDisabled() {
|
|
15053
|
+
return (await this.host()).getProperty('disabled');
|
|
15054
|
+
}
|
|
15055
|
+
/**
|
|
15056
|
+
* Whether the cell holds the grid's roving tabindex — the one cell reachable with Tab,
|
|
15057
|
+
* which the arrow keys move. Independent of selection: arrowing around changes which
|
|
15058
|
+
* cell is active without selecting anything.
|
|
15059
|
+
*/
|
|
15060
|
+
async isActive() {
|
|
15061
|
+
return (await this.host()).hasClass('tn-calendar-body-active');
|
|
15062
|
+
}
|
|
15063
|
+
/** Whether the cell represents today's date. */
|
|
15064
|
+
async isToday() {
|
|
15065
|
+
return (await this.host()).hasClass('tn-calendar-body-today');
|
|
15066
|
+
}
|
|
15067
|
+
/** Whether the cell was flagged through the calendar's `markedDates` input. */
|
|
15068
|
+
async isMarked() {
|
|
15069
|
+
return (await this.host()).hasClass('tn-calendar-body-marked');
|
|
15070
|
+
}
|
|
15071
|
+
/** Whether the cell is the start of the selected range. */
|
|
15072
|
+
async isRangeStart() {
|
|
15073
|
+
return (await this.host()).hasClass('tn-calendar-body-range-start');
|
|
15074
|
+
}
|
|
15075
|
+
/** Whether the cell is the end of the selected range. */
|
|
15076
|
+
async isRangeEnd() {
|
|
15077
|
+
return (await this.host()).hasClass('tn-calendar-body-range-end');
|
|
15078
|
+
}
|
|
15079
|
+
/**
|
|
15080
|
+
* Whether the cell is part of the selected range, ends included — matching Material's
|
|
15081
|
+
* `isInRange()`. Use `isRangeStart()`/`isRangeEnd()` to single out the ends.
|
|
15082
|
+
*/
|
|
15083
|
+
async isInRange() {
|
|
15084
|
+
const host = await this.host();
|
|
15085
|
+
const [between, start, end] = await parallel(() => [
|
|
15086
|
+
host.hasClass('tn-calendar-body-in-range'),
|
|
15087
|
+
host.hasClass('tn-calendar-body-range-start'),
|
|
15088
|
+
host.hasClass('tn-calendar-body-range-end'),
|
|
15089
|
+
]);
|
|
15090
|
+
return between || start || end;
|
|
15091
|
+
}
|
|
15092
|
+
/** Selects the calendar cell. Won't do anything if the cell is disabled. */
|
|
15093
|
+
async select() {
|
|
15094
|
+
return (await this.host()).click();
|
|
15095
|
+
}
|
|
15096
|
+
/**
|
|
15097
|
+
* Presses a key on the cell. The grid handles its keys on the cells themselves, so
|
|
15098
|
+
* this is how the roving tabindex is driven — arrows, Home/End and PageUp/PageDown
|
|
15099
|
+
* move the active cell, while Enter and Space select.
|
|
15100
|
+
*
|
|
15101
|
+
* @param key The key to press, e.g. `TestKey.RIGHT_ARROW`.
|
|
15102
|
+
* @param modifiers Modifier keys held while pressing, e.g. `{ shift: true }`.
|
|
15103
|
+
*
|
|
15104
|
+
* @example
|
|
15105
|
+
* ```typescript
|
|
15106
|
+
* const [cell] = await calendar.getCells({ active: true });
|
|
15107
|
+
* await cell.press(TestKey.PAGE_DOWN, { shift: true }); // a year on
|
|
15108
|
+
* ```
|
|
15109
|
+
*/
|
|
15110
|
+
async press(key, modifiers) {
|
|
15111
|
+
const host = await this.host();
|
|
15112
|
+
return modifiers ? host.sendKeys(modifiers, key) : host.sendKeys(key);
|
|
15113
|
+
}
|
|
15114
|
+
/** Hovers over the calendar cell. */
|
|
15115
|
+
async hover() {
|
|
15116
|
+
return (await this.host()).hover();
|
|
15117
|
+
}
|
|
15118
|
+
/** Moves the mouse away from the calendar cell. */
|
|
15119
|
+
async mouseAway() {
|
|
15120
|
+
return (await this.host()).mouseAway();
|
|
15121
|
+
}
|
|
15122
|
+
/** Focuses the calendar cell. */
|
|
15123
|
+
async focus() {
|
|
15124
|
+
return (await this.host()).focus();
|
|
15125
|
+
}
|
|
15126
|
+
/** Removes focus from the calendar cell. */
|
|
15127
|
+
async blur() {
|
|
15128
|
+
return (await this.host()).blur();
|
|
15129
|
+
}
|
|
15130
|
+
}
|
|
15131
|
+
/**
|
|
15132
|
+
* Harness for interacting with `tn-calendar` in tests.
|
|
15133
|
+
*
|
|
15134
|
+
* The method names mirror Angular Material's `MatCalendarHarness`, so a spec written
|
|
15135
|
+
* against the Material calendar moves over unchanged. Differences to know:
|
|
15136
|
+
*
|
|
15137
|
+
* - `getCurrentView()` resolves to `'month' | 'year'`. `tn-calendar` has no separate
|
|
15138
|
+
* multi-year view — its year view *is* the multi-year grid.
|
|
15139
|
+
* - Cell filters gain `marked` for the `markedDates` state and `date`/`year` for naming
|
|
15140
|
+
* a cell without going through its rendered text, and drop Material's
|
|
15141
|
+
* `inComparisonRange`, which has no equivalent here.
|
|
15142
|
+
*
|
|
15143
|
+
* @example
|
|
15144
|
+
* ```typescript
|
|
15145
|
+
* const calendar = await loader.getHarness(TnCalendarHarness);
|
|
15146
|
+
*
|
|
15147
|
+
* // Which days does the schedule run on this month?
|
|
15148
|
+
* const marked = await calendar.getCells({ marked: true });
|
|
15149
|
+
* expect(await parallel(() => marked.map((cell) => cell.getText()))).toEqual(['24', '25']);
|
|
15150
|
+
*
|
|
15151
|
+
* // Page forward a month
|
|
15152
|
+
* await calendar.next();
|
|
15153
|
+
* expect(await calendar.getCurrentViewLabel()).toBe('MAR 2022');
|
|
15154
|
+
*
|
|
15155
|
+
* // Pick a day
|
|
15156
|
+
* await calendar.selectCell({ date: new Date(2022, 2, 14) });
|
|
15157
|
+
* ```
|
|
15158
|
+
*/
|
|
15159
|
+
class TnCalendarHarness extends ComponentHarness {
|
|
15160
|
+
/** The selector for the host element of a `TnCalendarComponent` instance. */
|
|
15161
|
+
static hostSelector = 'tn-calendar';
|
|
15162
|
+
_periodButton = this.locatorFor('.tn-calendar-period-button');
|
|
15163
|
+
_previousButton = this.locatorFor('.tn-calendar-previous-button');
|
|
15164
|
+
_nextButton = this.locatorFor('.tn-calendar-next-button');
|
|
15165
|
+
_monthView = this.locatorForOptional('tn-month-view');
|
|
15166
|
+
/**
|
|
15167
|
+
* Gets a `HarnessPredicate` that can be used to search for a calendar with specific
|
|
15168
|
+
* attributes.
|
|
15169
|
+
*
|
|
15170
|
+
* @param options Options for filtering which calendar instances are considered a match.
|
|
15171
|
+
* @returns A `HarnessPredicate` configured with the given options.
|
|
15172
|
+
*/
|
|
15173
|
+
static with(options = {}) {
|
|
15174
|
+
return new HarnessPredicate(TnCalendarHarness, options);
|
|
15175
|
+
}
|
|
15176
|
+
/**
|
|
15177
|
+
* Gets a list of cells inside the calendar. Padding cells for days outside the month
|
|
15178
|
+
* are not included.
|
|
15179
|
+
*
|
|
15180
|
+
* @param filter Optionally filters which cells are included.
|
|
15181
|
+
*/
|
|
15182
|
+
async getCells(filter = {}) {
|
|
15183
|
+
return this.locatorForAll(TnCalendarCellHarness.with(filter))();
|
|
15184
|
+
}
|
|
15185
|
+
/** Gets the current view that is being shown inside the calendar. */
|
|
15186
|
+
async getCurrentView() {
|
|
15187
|
+
return (await this._monthView()) ? 'month' : 'year';
|
|
15188
|
+
}
|
|
15189
|
+
/** Gets the label of the current calendar view, e.g. `'MAR 2022'`. */
|
|
15190
|
+
async getCurrentViewLabel() {
|
|
15191
|
+
return (await this._periodButton()).text();
|
|
15192
|
+
}
|
|
15193
|
+
/** Changes the calendar view by clicking on the view toggle button. */
|
|
15194
|
+
async changeView() {
|
|
15195
|
+
return (await this._periodButton()).click();
|
|
15196
|
+
}
|
|
15197
|
+
/** Goes to the next page of the current view (e.g. next month when inside the month view). */
|
|
15198
|
+
async next() {
|
|
15199
|
+
return (await this._nextButton()).click();
|
|
15200
|
+
}
|
|
15201
|
+
/** Goes to the previous page of the current view (e.g. previous month in the month view). */
|
|
15202
|
+
async previous() {
|
|
15203
|
+
return (await this._previousButton()).click();
|
|
15204
|
+
}
|
|
15205
|
+
/**
|
|
15206
|
+
* Selects a cell in the current calendar view.
|
|
15207
|
+
*
|
|
15208
|
+
* @param filter An optional filter to apply to the cells. The first cell matching the
|
|
15209
|
+
* filter will be selected.
|
|
15210
|
+
*/
|
|
15211
|
+
async selectCell(filter = {}) {
|
|
15212
|
+
const cells = await this.getCells(filter);
|
|
15213
|
+
if (!cells.length) {
|
|
15214
|
+
throw Error(`Cannot find calendar cell matching filter ${JSON.stringify(filter)}`);
|
|
15215
|
+
}
|
|
15216
|
+
await cells[0].select();
|
|
15217
|
+
}
|
|
15218
|
+
/**
|
|
15219
|
+
* Gets the cell holding the grid's roving tabindex — the one cell Tab reaches, and the
|
|
15220
|
+
* one the arrow keys move.
|
|
15221
|
+
*
|
|
15222
|
+
* `null` only when every cell in the view is disabled, where there is nothing to focus.
|
|
15223
|
+
*/
|
|
15224
|
+
async getActiveCell() {
|
|
15225
|
+
const [active] = await this.getCells({ active: true });
|
|
15226
|
+
return active ?? null;
|
|
15227
|
+
}
|
|
15228
|
+
/**
|
|
15229
|
+
* Moves the grid's roving tabindex with the keyboard, without going through the DOM.
|
|
15230
|
+
*
|
|
15231
|
+
* Arrows move by a day or a week (a year or a row of years); Home and End reach the
|
|
15232
|
+
* ends of the month or year page; PageUp and PageDown page, shifted by a year. Moving
|
|
15233
|
+
* never changes the selection — use `selectCell()` or `TnCalendarCellHarness.select()`
|
|
15234
|
+
* for that.
|
|
15235
|
+
*
|
|
15236
|
+
* @param key The key to press, e.g. `TestKey.RIGHT_ARROW`.
|
|
15237
|
+
* @param modifiers Modifier keys held while pressing, e.g. `{ shift: true }`.
|
|
15238
|
+
*
|
|
15239
|
+
* @example
|
|
15240
|
+
* ```typescript
|
|
15241
|
+
* await calendar.moveActiveCell(TestKey.DOWN_ARROW);
|
|
15242
|
+
* expect(await (await calendar.getActiveCell())!.getText()).toBe('17');
|
|
15243
|
+
* ```
|
|
15244
|
+
*/
|
|
15245
|
+
async moveActiveCell(key, modifiers) {
|
|
15246
|
+
const active = await this.getActiveCell();
|
|
15247
|
+
if (!active) {
|
|
15248
|
+
throw Error('Cannot move the active cell: the calendar has no cell able to take focus.');
|
|
15249
|
+
}
|
|
15250
|
+
await active.press(key, modifiers);
|
|
15251
|
+
}
|
|
15252
|
+
}
|
|
14364
15253
|
|
|
14365
15254
|
class TnDateInputComponent {
|
|
14366
15255
|
overlay = inject(Overlay);
|
|
@@ -14381,9 +15270,7 @@ class TnDateInputComponent {
|
|
|
14381
15270
|
dayRef = viewChild.required('dayInput');
|
|
14382
15271
|
yearRef = viewChild.required('yearInput');
|
|
14383
15272
|
calendarTemplate = viewChild.required('calendarTemplate');
|
|
14384
|
-
calendar = viewChild.required(TnCalendarComponent);
|
|
14385
15273
|
wrapperEl = viewChild.required('wrapper');
|
|
14386
|
-
destroy$ = new Subject();
|
|
14387
15274
|
overlayRef;
|
|
14388
15275
|
portal;
|
|
14389
15276
|
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
@@ -14399,8 +15286,6 @@ class TnDateInputComponent {
|
|
|
14399
15286
|
this.updateDisplayValues();
|
|
14400
15287
|
}
|
|
14401
15288
|
ngOnDestroy() {
|
|
14402
|
-
this.destroy$.next();
|
|
14403
|
-
this.destroy$.complete();
|
|
14404
15289
|
this.close();
|
|
14405
15290
|
}
|
|
14406
15291
|
// ControlValueAccessor implementation
|
|
@@ -14543,11 +15428,8 @@ class TnDateInputComponent {
|
|
|
14543
15428
|
}
|
|
14544
15429
|
this.createOverlay();
|
|
14545
15430
|
this.isOpen.set(true);
|
|
14546
|
-
//
|
|
14547
|
-
|
|
14548
|
-
if (cal) {
|
|
14549
|
-
setTimeout(() => cal.resetInteractionState(), 0);
|
|
14550
|
-
}
|
|
15431
|
+
// No calendar state to reset — the calendar renders whatever date this component
|
|
15432
|
+
// binds to it, so reopening already shows the current value.
|
|
14551
15433
|
}
|
|
14552
15434
|
close() {
|
|
14553
15435
|
if (this.overlayRef) {
|
|
@@ -14604,6 +15486,8 @@ class TnDateInputComponent {
|
|
|
14604
15486
|
panelClass: 'tn-datepicker-overlay'
|
|
14605
15487
|
});
|
|
14606
15488
|
// Close datepicker when backdrop is clicked
|
|
15489
|
+
// No teardown to track: `dispose()` in close() ends this subscription with the
|
|
15490
|
+
// overlay, and ngOnDestroy closes.
|
|
14607
15491
|
this.overlayRef.backdropClick().subscribe(() => {
|
|
14608
15492
|
this.close();
|
|
14609
15493
|
});
|
|
@@ -14617,7 +15501,7 @@ class TnDateInputComponent {
|
|
|
14617
15501
|
useExisting: forwardRef(() => TnDateInputComponent),
|
|
14618
15502
|
multi: true
|
|
14619
15503
|
}
|
|
14620
|
-
], viewQueries: [{ propertyName: "monthRef", first: true, predicate: ["monthInput"], descendants: true, isSignal: true }, { propertyName: "dayRef", first: true, predicate: ["dayInput"], descendants: true, isSignal: true }, { propertyName: "yearRef", first: true, predicate: ["yearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "
|
|
15504
|
+
], viewQueries: [{ propertyName: "monthRef", first: true, predicate: ["monthInput"], descendants: true, isSignal: true }, { propertyName: "dayRef", first: true, predicate: ["dayInput"], descendants: true, isSignal: true }, { propertyName: "yearRef", first: true, predicate: ["yearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-date-input-container\" tnTestIdType=\"date-input\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-input-wrapper\" style=\"padding-right: 40px;\">\n <!-- Date segments MM/DD/YYYY -->\n <div class=\"tn-date-segment-group\">\n <input\n #monthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('month')\"\n (blur)=\"onSegmentBlur('month')\"\n (keydown)=\"onSegmentKeydown($event, 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #dayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('day')\"\n (blur)=\"onSegmentBlur('day')\"\n (keydown)=\"onSegmentKeydown($event, 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #yearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('year')\"\n (blur)=\"onSegmentBlur('year')\"\n (keydown)=\"onSegmentKeydown($event, 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-input-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"false\"\n [selected]=\"value()\"\n (selectedChange)=\"onDateSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-input-container{position:relative;display:flex;align-items:center}.tn-date-input-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-input-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-input-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-input-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"], dependencies: [{ kind: "component", type: TnCalendarComponent, selector: "tn-calendar", inputs: ["startView", "selected", "minDate", "maxDate", "dateFilter", "markedDates", "activeDate", "locale", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange", "viewChanged", "selectedRangeChange"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
14621
15505
|
}
|
|
14622
15506
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDateInputComponent, decorators: [{
|
|
14623
15507
|
type: Component,
|
|
@@ -14630,7 +15514,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
14630
15514
|
], host: {
|
|
14631
15515
|
'class': 'tn-date-input'
|
|
14632
15516
|
}, template: "<div class=\"tn-date-input-container\" tnTestIdType=\"date-input\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-input-wrapper\" style=\"padding-right: 40px;\">\n <!-- Date segments MM/DD/YYYY -->\n <div class=\"tn-date-segment-group\">\n <input\n #monthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('month')\"\n (blur)=\"onSegmentBlur('month')\"\n (keydown)=\"onSegmentKeydown($event, 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #dayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('day')\"\n (blur)=\"onSegmentBlur('day')\"\n (keydown)=\"onSegmentKeydown($event, 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #yearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('year')\"\n (blur)=\"onSegmentBlur('year')\"\n (keydown)=\"onSegmentKeydown($event, 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-input-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"false\"\n [selected]=\"value()\"\n (selectedChange)=\"onDateSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-input-container{position:relative;display:flex;align-items:center}.tn-date-input-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-input-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-input-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-input-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"] }]
|
|
14633
|
-
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], monthRef: [{ type: i0.ViewChild, args: ['monthInput', { isSignal: true }] }], dayRef: [{ type: i0.ViewChild, args: ['dayInput', { isSignal: true }] }], yearRef: [{ type: i0.ViewChild, args: ['yearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }],
|
|
15517
|
+
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], monthRef: [{ type: i0.ViewChild, args: ['monthInput', { isSignal: true }] }], dayRef: [{ type: i0.ViewChild, args: ['dayInput', { isSignal: true }] }], yearRef: [{ type: i0.ViewChild, args: ['yearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
|
|
14634
15518
|
|
|
14635
15519
|
/**
|
|
14636
15520
|
* Formats a Date into zero-padded month, day, and year strings.
|
|
@@ -14667,12 +15551,8 @@ async function setInputValue(el, value) {
|
|
|
14667
15551
|
}
|
|
14668
15552
|
await el.blur();
|
|
14669
15553
|
}
|
|
14670
|
-
const MONTHS = [
|
|
14671
|
-
'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
|
|
14672
|
-
'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC',
|
|
14673
|
-
];
|
|
14674
15554
|
/**
|
|
14675
|
-
* Navigates the calendar overlay to the target month
|
|
15555
|
+
* Navigates the calendar overlay to the target month, then clicks that day.
|
|
14676
15556
|
* Assumes the calendar popup is already open.
|
|
14677
15557
|
*
|
|
14678
15558
|
* @param rootLocator The document root locator factory (for finding overlay elements).
|
|
@@ -14680,45 +15560,48 @@ const MONTHS = [
|
|
|
14680
15560
|
*/
|
|
14681
15561
|
async function selectCalendarDate(rootLocator, date) {
|
|
14682
15562
|
await navigateCalendarTo(rootLocator, date);
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
throw new Error(`Could not find enabled calendar cell for day ${dayStr}`);
|
|
15563
|
+
// Matched on `data-tn-date` rather than the cell's text: the text is formatted for the
|
|
15564
|
+
// locale, right down to the numerals, so "15" finds nothing in an Arabic calendar.
|
|
15565
|
+
const target = isoDateString(date);
|
|
15566
|
+
const cell = await rootLocator.locatorForOptional(`.tn-calendar-body-cell[data-tn-date="${target}"]:not([disabled])`)();
|
|
15567
|
+
if (!cell) {
|
|
15568
|
+
throw new Error(`Could not find an enabled calendar cell for ${target}`);
|
|
15569
|
+
}
|
|
15570
|
+
await cell.click();
|
|
14692
15571
|
}
|
|
14693
15572
|
/**
|
|
14694
|
-
*
|
|
14695
|
-
*
|
|
15573
|
+
* Pages the calendar to the month the target date falls in.
|
|
15574
|
+
*
|
|
15575
|
+
* Reads where the calendar currently sits from the days it is rendering rather than from
|
|
15576
|
+
* the header, which is written in the app's language and numerals. Every day cell in the
|
|
15577
|
+
* grid belongs to the displayed month, so the first one answers the question.
|
|
14696
15578
|
*/
|
|
14697
15579
|
async function navigateCalendarTo(rootLocator, date) {
|
|
14698
|
-
const
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
15580
|
+
const monthsFrom = (from) => {
|
|
15581
|
+
return (date.getFullYear() * 12 + date.getMonth()) - (from.year * 12 + from.month);
|
|
15582
|
+
};
|
|
15583
|
+
// A generous bound on the paging loop: a decade either way, and far more than the
|
|
15584
|
+
// handful of clicks any real navigation needs.
|
|
15585
|
+
for (let step = 0; step < 240; step++) {
|
|
15586
|
+
const shown = await displayedMonth(rootLocator);
|
|
15587
|
+
const delta = monthsFrom(shown);
|
|
15588
|
+
if (delta === 0) {
|
|
14704
15589
|
return;
|
|
14705
15590
|
}
|
|
14706
|
-
const
|
|
14707
|
-
|
|
14708
|
-
const currentTime = currentDate.year * 12 + currentDate.month;
|
|
14709
|
-
const navSelector = targetTime > currentTime
|
|
14710
|
-
? '.tn-calendar-next-button'
|
|
14711
|
-
: '.tn-calendar-previous-button';
|
|
14712
|
-
const navButton = await rootLocator.locatorFor(navSelector)();
|
|
14713
|
-
await navButton.click();
|
|
15591
|
+
const button = await rootLocator.locatorFor(delta > 0 ? '.tn-calendar-next-button' : '.tn-calendar-previous-button')();
|
|
15592
|
+
await button.click();
|
|
14714
15593
|
}
|
|
14715
|
-
throw new Error(`Could not navigate calendar to ${
|
|
15594
|
+
throw new Error(`Could not navigate the calendar to ${isoDateString(date)}`);
|
|
14716
15595
|
}
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
const
|
|
14720
|
-
const
|
|
14721
|
-
|
|
15596
|
+
/** The month on screen, read off the first day cell the grid rendered. */
|
|
15597
|
+
async function displayedMonth(rootLocator) {
|
|
15598
|
+
const cell = await rootLocator.locatorForOptional('.tn-calendar-body-cell[data-tn-date]')();
|
|
15599
|
+
const stamp = await cell?.getAttribute('data-tn-date');
|
|
15600
|
+
if (!stamp) {
|
|
15601
|
+
throw new Error('Could not read the displayed month: the calendar rendered no day cells.');
|
|
15602
|
+
}
|
|
15603
|
+
const [year, month] = stamp.split('-').map(Number);
|
|
15604
|
+
return { year, month: month - 1 };
|
|
14722
15605
|
}
|
|
14723
15606
|
|
|
14724
15607
|
/**
|
|
@@ -14850,9 +15733,7 @@ class TnDateRangeInputComponent {
|
|
|
14850
15733
|
endDayRef = viewChild.required('endDayInput');
|
|
14851
15734
|
endYearRef = viewChild.required('endYearInput');
|
|
14852
15735
|
calendarTemplate = viewChild.required('calendarTemplate');
|
|
14853
|
-
calendar = viewChild.required(TnCalendarComponent);
|
|
14854
15736
|
wrapperEl = viewChild.required('wrapper');
|
|
14855
|
-
destroy$ = new Subject();
|
|
14856
15737
|
overlayRef;
|
|
14857
15738
|
portal;
|
|
14858
15739
|
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
@@ -14867,19 +15748,14 @@ class TnDateRangeInputComponent {
|
|
|
14867
15748
|
endDay = signal('', ...(ngDevMode ? [{ debugName: "endDay" }] : []));
|
|
14868
15749
|
endYear = signal('', ...(ngDevMode ? [{ debugName: "endYear" }] : []));
|
|
14869
15750
|
currentFocus = 'start';
|
|
14870
|
-
// Always provide current range to calendar for initial display
|
|
14871
|
-
initialRange = computed(() => {
|
|
14872
|
-
return this.value();
|
|
14873
|
-
}, ...(ngDevMode ? [{ debugName: "initialRange" }] : []));
|
|
14874
15751
|
overlay = inject(Overlay);
|
|
14875
15752
|
viewContainerRef = inject(ViewContainerRef);
|
|
15753
|
+
injector = inject(Injector);
|
|
14876
15754
|
ngOnInit() {
|
|
14877
15755
|
// Initialize display values
|
|
14878
15756
|
this.updateDisplayValues();
|
|
14879
15757
|
}
|
|
14880
15758
|
ngOnDestroy() {
|
|
14881
|
-
this.destroy$.next();
|
|
14882
|
-
this.destroy$.complete();
|
|
14883
15759
|
this.close();
|
|
14884
15760
|
}
|
|
14885
15761
|
// ControlValueAccessor implementation
|
|
@@ -14947,8 +15823,9 @@ class TnDateRangeInputComponent {
|
|
|
14947
15823
|
if (this.endYearRef()?.nativeElement) {
|
|
14948
15824
|
this.endYearRef().nativeElement.value = '';
|
|
14949
15825
|
}
|
|
14950
|
-
//
|
|
14951
|
-
|
|
15826
|
+
// Move to the end date once the cleared segments have been painted. Tied to the
|
|
15827
|
+
// render rather than to a zero timeout, as the calendar's own focus handling is.
|
|
15828
|
+
afterNextRender(() => this.endMonthRef()?.nativeElement?.focus(), { injector: this.injector });
|
|
14952
15829
|
}
|
|
14953
15830
|
else if (range.start && range.end) {
|
|
14954
15831
|
// Both dates selected - close calendar
|
|
@@ -15108,11 +15985,8 @@ class TnDateRangeInputComponent {
|
|
|
15108
15985
|
}
|
|
15109
15986
|
this.createOverlay();
|
|
15110
15987
|
this.isOpen.set(true);
|
|
15111
|
-
//
|
|
15112
|
-
|
|
15113
|
-
if (cal) {
|
|
15114
|
-
setTimeout(() => cal.resetInteractionState(), 0);
|
|
15115
|
-
}
|
|
15988
|
+
// No calendar state to reset — the calendar renders whatever range this component
|
|
15989
|
+
// binds to it, so reopening already shows the current value.
|
|
15116
15990
|
}
|
|
15117
15991
|
close() {
|
|
15118
15992
|
if (this.overlayRef) {
|
|
@@ -15169,6 +16043,8 @@ class TnDateRangeInputComponent {
|
|
|
15169
16043
|
panelClass: 'tn-datepicker-overlay'
|
|
15170
16044
|
});
|
|
15171
16045
|
// Close datepicker when backdrop is clicked
|
|
16046
|
+
// No teardown to track: `dispose()` in close() ends this subscription with the
|
|
16047
|
+
// overlay, and ngOnDestroy closes.
|
|
15172
16048
|
this.overlayRef.backdropClick().subscribe(() => {
|
|
15173
16049
|
this.close();
|
|
15174
16050
|
});
|
|
@@ -15182,7 +16058,7 @@ class TnDateRangeInputComponent {
|
|
|
15182
16058
|
useExisting: forwardRef(() => TnDateRangeInputComponent),
|
|
15183
16059
|
multi: true
|
|
15184
16060
|
}
|
|
15185
|
-
], viewQueries: [{ propertyName: "startMonthRef", first: true, predicate: ["startMonthInput"], descendants: true, isSignal: true }, { propertyName: "startDayRef", first: true, predicate: ["startDayInput"], descendants: true, isSignal: true }, { propertyName: "startYearRef", first: true, predicate: ["startYearInput"], descendants: true, isSignal: true }, { propertyName: "endMonthRef", first: true, predicate: ["endMonthInput"], descendants: true, isSignal: true }, { propertyName: "endDayRef", first: true, predicate: ["endDayInput"], descendants: true, isSignal: true }, { propertyName: "endYearRef", first: true, predicate: ["endYearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "
|
|
16061
|
+
], viewQueries: [{ propertyName: "startMonthRef", first: true, predicate: ["startMonthInput"], descendants: true, isSignal: true }, { propertyName: "startDayRef", first: true, predicate: ["startDayInput"], descendants: true, isSignal: true }, { propertyName: "startYearRef", first: true, predicate: ["startYearInput"], descendants: true, isSignal: true }, { propertyName: "endMonthRef", first: true, predicate: ["endMonthInput"], descendants: true, isSignal: true }, { propertyName: "endDayRef", first: true, predicate: ["endDayInput"], descendants: true, isSignal: true }, { propertyName: "endYearRef", first: true, predicate: ["endYearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"value()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"], dependencies: [{ kind: "component", type: TnCalendarComponent, selector: "tn-calendar", inputs: ["startView", "selected", "minDate", "maxDate", "dateFilter", "markedDates", "activeDate", "locale", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange", "viewChanged", "selectedRangeChange"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
15186
16062
|
}
|
|
15187
16063
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDateRangeInputComponent, decorators: [{
|
|
15188
16064
|
type: Component,
|
|
@@ -15194,8 +16070,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
15194
16070
|
}
|
|
15195
16071
|
], host: {
|
|
15196
16072
|
'class': 'tn-date-range-input'
|
|
15197
|
-
}, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"
|
|
15198
|
-
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], startMonthRef: [{ type: i0.ViewChild, args: ['startMonthInput', { isSignal: true }] }], startDayRef: [{ type: i0.ViewChild, args: ['startDayInput', { isSignal: true }] }], startYearRef: [{ type: i0.ViewChild, args: ['startYearInput', { isSignal: true }] }], endMonthRef: [{ type: i0.ViewChild, args: ['endMonthInput', { isSignal: true }] }], endDayRef: [{ type: i0.ViewChild, args: ['endDayInput', { isSignal: true }] }], endYearRef: [{ type: i0.ViewChild, args: ['endYearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }],
|
|
16073
|
+
}, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"value()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"] }]
|
|
16074
|
+
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], startMonthRef: [{ type: i0.ViewChild, args: ['startMonthInput', { isSignal: true }] }], startDayRef: [{ type: i0.ViewChild, args: ['startDayInput', { isSignal: true }] }], startYearRef: [{ type: i0.ViewChild, args: ['startYearInput', { isSignal: true }] }], endMonthRef: [{ type: i0.ViewChild, args: ['endMonthInput', { isSignal: true }] }], endDayRef: [{ type: i0.ViewChild, args: ['endDayInput', { isSignal: true }] }], endYearRef: [{ type: i0.ViewChild, args: ['endYearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
|
|
15199
16075
|
|
|
15200
16076
|
/**
|
|
15201
16077
|
* Harness for interacting with `tn-date-range-input` in tests.
|
|
@@ -16619,7 +17495,7 @@ class TnConfirmDialogComponent {
|
|
|
16619
17495
|
ref = inject((DialogRef));
|
|
16620
17496
|
data = inject(DIALOG_DATA);
|
|
16621
17497
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnConfirmDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
16622
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: TnConfirmDialogComponent, isStandalone: true, selector: "tn-confirm-dialog", host: { properties: { "class.tn-dialog--destructive": "data.destructive" }, classAttribute: "tn-dialog-shell" }, ngImport: i0, template: "<tn-dialog-shell [title]=\"data.title\">\n <p style=\"margin: 0;\">{{ data.message }}</p>\n <div tnDialogAction>\n <tn-button\n type=\"button\"\n variant=\"outline\"\n [label]=\"data.cancelText || 'Cancel'\"\n [testId]=\"data.cancelTestId || 'cancel'\"\n (click)=\"ref.close(false)\" />\n <tn-button\n type=\"button\"\n [color]=\"data.destructive ? 'warn' : 'primary'\"\n [label]=\"data.confirmText || 'OK'\"\n [testId]=\"data.confirmTestId || 'confirm'\"\n (click)=\"ref.close(true)\" />\n </div>\n</tn-dialog-shell>\n", dependencies: [{ kind: "component", type: TnDialogShellComponent, selector: "tn-dialog-shell", inputs: ["title", "showFullscreenButton", "showCloseButton", "hideContent", "hideActions", "testId"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }] });
|
|
17498
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: TnConfirmDialogComponent, isStandalone: true, selector: "tn-confirm-dialog", host: { properties: { "class.tn-dialog--destructive": "data.destructive" }, classAttribute: "tn-dialog-shell" }, ngImport: i0, template: "<tn-dialog-shell [title]=\"data.title\">\n <p style=\"margin: 0;\">{{ data.message }}</p>\n <div tnDialogAction>\n <tn-button\n type=\"button\"\n variant=\"outline\"\n [label]=\"data.cancelText || 'Cancel'\"\n [testId]=\"data.cancelTestId || 'cancel'\"\n (click)=\"ref.close(false)\" />\n <tn-button\n type=\"button\"\n [color]=\"data.destructive ? 'warn' : 'primary'\"\n [label]=\"data.confirmText || 'OK'\"\n [testId]=\"data.confirmTestId || 'confirm'\"\n (click)=\"ref.close(true)\" />\n </div>\n</tn-dialog-shell>\n", dependencies: [{ kind: "component", type: TnDialogShellComponent, selector: "tn-dialog-shell", inputs: ["title", "showFullscreenButton", "showCloseButton", "hideContent", "hideActions", "testId"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }] });
|
|
16623
17499
|
}
|
|
16624
17500
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnConfirmDialogComponent, decorators: [{
|
|
16625
17501
|
type: Component,
|
|
@@ -17937,7 +18813,7 @@ class TnFilePickerPopupComponent {
|
|
|
17937
18813
|
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${timePart}`;
|
|
17938
18814
|
}
|
|
17939
18815
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17940
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFilePickerPopupComponent, isStandalone: true, selector: "tn-file-picker-popup", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, createActions: { classPropertyName: "createActions", publicName: "createActions", isSignal: true, isRequired: false, transformFunction: null }, currentPath: { classPropertyName: "currentPath", publicName: "currentPath", isSignal: true, isRequired: false, transformFunction: null }, rootPath: { classPropertyName: "rootPath", publicName: "rootPath", isSignal: true, isRequired: false, transformFunction: null }, fileItems: { classPropertyName: "fileItems", publicName: "fileItems", isSignal: true, isRequired: false, transformFunction: null }, selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null }, testIdBase: { classPropertyName: "testIdBase", publicName: "testIdBase", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemDoubleClick: "itemDoubleClick", pathNavigate: "pathNavigate", createAction: "createAction", created: "created", clearSelection: "clearSelection", close: "close", submit: "submit", cancel: "cancel" }, host: { classAttribute: "tn-file-picker-popup" }, ngImport: i0, template: "<!-- Header with breadcrumb navigation -->\n<div class=\"tn-file-picker-header\">\n <nav class=\"tn-file-picker-breadcrumb\" aria-label=\"File path\">\n @for (segment of currentPath() | tnTruncatePath : rootPath(); track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n tnTestIdType=\"button\"\n [tnTestId]=\"breadcrumbTestId(segment.name)\"\n [class.current]=\"last\"\n [class.nameless]=\"!segment.name\"\n [disabled]=\"last\"\n [attr.aria-label]=\"segment.name === '\u2026' || !segment.name ? 'Navigate to ' + segment.path : null\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\n</div>\n\n<!-- Loading indicator -->\n@if (loading()) {\n <div class=\"tn-file-picker-loading\">\n <tn-icon name=\"loading\" library=\"mdi\" />\n <span>Loading...</span>\n </div>\n}\n\n<!-- File table -->\n@if (!loading()) {\n <div class=\"tn-file-picker-content\">\n\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\"\n [clickable]=\"true\"\n [activeWhen]=\"isRowSelected\"\n (rowClick)=\"onItemClick($event)\"\n (rowDoubleClick)=\"onItemDoubleClick($event)\">\n\n <!-- Selection column: explicit width so the auto table layout doesn't\n hand the near-empty column a share of the spare width -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\" width=\"20px\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n @if (!item.inlineCreate) {\n <tn-checkbox\n [hideLabel]=\"true\"\n [label]=\"'Select ' + item.name\"\n [testId]=\"itemTestId(item)\"\n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\" />\n }\n </ng-template>\n </ng-container>\n }\n\n <!-- Name column -->\n <ng-container tnColumnDef=\"name\">\n <ng-template tnHeaderCellDef>Name</ng-template>\n <ng-template let-item tnCellDef>\n\n <!-- Inline creation row for create actions with a `create` callback -->\n @if (item.inlineCreate) {\n @if (inlineCreation(); as creation) {\n <div class=\"inline-create-cell\">\n <div class=\"inline-create-row\" [class.has-error]=\"!!creation.error\">\n <tn-icon class=\"file-icon\" [name]=\"creation.action.icon || 'mdi-folder'\" />\n <input\n type=\"text\"\n class=\"inline-create-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n tnTestIdType=\"input\"\n [tnTestId]=\"inlineCreateTestId()\"\n [attr.aria-label]=\"creation.action.label + ' name'\"\n [attr.aria-invalid]=\"creation.error ? true : null\"\n [attr.aria-describedby]=\"creation.error ? inlineErrorId : null\"\n [class.error]=\"!!creation.error\"\n [disabled]=\"creation.loading\"\n [value]=\"creation.name\"\n (input)=\"onInlineCreationInput($event)\"\n (keydown)=\"onInlineCreationKeyDown($event)\"\n (blur)=\"onInlineCreationBlur()\">\n @if (creation.loading) {\n <tn-icon name=\"mdi-loading\" class=\"creation-loading-icon\" aria-hidden=\"true\" />\n }\n </div>\n @if (creation.error) {\n <div class=\"inline-create-error\" role=\"alert\" [id]=\"inlineErrorId\">\n <tn-icon name=\"mdi-alert-circle\" class=\"error-icon\" aria-hidden=\"true\" />\n <span class=\"error-text\">{{ creation.error }}</span>\n </div>\n }\n </div>\n }\n } @else {\n <div\n class=\"file-name-cell\"\n tnTestIdType=\"option\"\n [tnTestId]=\"itemTestId(item)\"\n [class.selected]=\"isSelected(item)\"\n [class.disabled]=\"!!item.dimmed\"\n [class.zfs-object]=\"isZfsObject(item)\">\n <tn-icon\n [name]=\"getItemIcon(item)\"\n [class]=\"'file-icon file-icon-' + item.type\" />\n <span class=\"file-name\">{{ item.name }}</span>\n\n <!-- ZFS badge -->\n @if (isZfsObject(item)) {\n <span\n [class]=\"'zfs-badge zfs-badge-' + item.type\">\n {{ getZfsBadge(item) }}\n </span>\n }\n\n <!-- Permission indicator -->\n @if (item.permissions === 'none') {\n <tn-icon\n name=\"lock\"\n library=\"mdi\"\n class=\"permission-icon\" />\n }\n\n <!-- Navigation affordance: single click opens the directory -->\n @if (isNavigatable(item)) {\n <button\n type=\"button\"\n class=\"navigate-button\"\n tnTestIdType=\"button\"\n [tnTestId]=\"navigateTestId(item)\"\n [attr.aria-label]=\"'Open ' + item.name\"\n [attr.title]=\"'Open ' + item.name\"\n (click)=\"onNavigateClick($event, item)\"\n (dblclick)=\"$event.stopPropagation()\">\n <tn-icon name=\"mdi-chevron-right\" size=\"sm\" />\n </button>\n }\n </div>\n }\n </ng-template>\n </ng-container>\n\n <!-- Size column -->\n <ng-container tnColumnDef=\"size\">\n <ng-template tnHeaderCellDef>Size</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.size !== undefined) {\n <span>{{ item.size | tnFileSize }}</span>\n }\n @if (item.size === undefined && item.type === 'folder') {\n <span class=\"folder-indicator\">--</span>\n }\n </ng-template>\n </ng-container>\n\n <!-- Modified column -->\n <ng-container tnColumnDef=\"modified\">\n <ng-template tnHeaderCellDef>Modified</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.modified) {\n <span>{{ formatDate(item.modified) }}</span>\n }\n </ng-template>\n </ng-container>\n\n\n </tn-table>\n\n <!-- Empty state -->\n @if (filteredFileItems().length === 0) {\n <div class=\"empty-state\">\n <tn-icon name=\"folder-open\" library=\"mdi\" customSize=\"48px\" />\n <p>No items found</p>\n </div>\n }\n </div>\n}\n\n<!-- Footer -->\n@if (!loading()) {\n <div class=\"tn-file-picker-footer\">\n <div class=\"footer-selection\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n <tn-button\n variant=\"outline\"\n label=\"Clear Selection\"\n [testId]=\"clearSelectionTestId()\"\n (onClick)=\"onClearSelection()\" />\n } @else if (allowCurrentDirectorySelection()) {\n <span class=\"selection-count current-directory-selection\">\n Current directory selected\n </span>\n } @else {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n </div>\n <div class=\"footer-actions\">\n @for (action of createActions(); track action.id) {\n <tn-button\n variant=\"outline\"\n [label]=\"action.label\"\n [testId]=\"createActionTestId(action)\"\n [disabled]=\"!!inlineCreation()\"\n (onClick)=\"onCreateAction(action)\" />\n }\n <!-- Disabled while the inline creation row is open: its blur auto-submit\n would race the selection submit -->\n <tn-button\n label=\"Select\"\n [testId]=\"selectTestId()\"\n [disabled]=\"(selectedItems().length === 0 && !allowCurrentDirectorySelection()) || !!inlineCreation()\"\n (onClick)=\"onSubmit()\" />\n </div>\n </div>\n}", styles: [":host{display:block;background:var(--tn-bg1, white);color:var(--tn-fg1, #333);padding:0;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014;border-radius:8px;border:1px solid var(--tn-lines, #e0e0e0);width:520px;max-width:calc(100vw - 32px);min-height:min(500px,100vh - 16px);max-height:min(600px,100vh - 16px);font-family:var(--tn-font-family-body);display:flex;flex-direction:column;overflow:hidden}.tn-file-picker-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tn-content-padding, 24px);padding-bottom:16px;border-bottom:1px solid var(--tn-lines)}.tn-file-picker-breadcrumb{display:flex;align-items:center;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 2px;border-radius:4px;font-size:1rem;white-space:nowrap;transition:background-color .15s ease-in-out}.tn-file-picker-breadcrumb .breadcrumb-segment:hover:not(:disabled){background:var(--tn-bg2)}.tn-file-picker-breadcrumb .breadcrumb-segment:disabled,.tn-file-picker-breadcrumb .breadcrumb-segment.current{color:var(--tn-fg1);cursor:default;font-weight:500}.tn-file-picker-breadcrumb .breadcrumb-segment:not(:last-child):after{content:\"/\";margin-left:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment:first-child:before{content:\"/\";margin-right:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment.nameless:not(:last-child):after{content:none}.tn-file-picker-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px;color:var(--tn-fg2)}.tn-file-picker-loading tn-icon{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tn-file-picker-content{flex:1;min-height:0;overflow-y:auto}.inline-create-cell{display:flex;flex-direction:column;gap:4px;flex:1;min-width:0}.inline-create-row{display:flex;align-items:center;gap:8px}.inline-create-row .inline-create-input{flex:1;padding:4px 8px;border:2px solid var(--tn-primary);border-radius:3px;background:var(--tn-bg1);color:var(--tn-fg1);font-size:inherit;font-family:inherit;outline:none;min-width:0}.inline-create-row .inline-create-input.error{border-color:var(--tn-error, #d32f2f)}.inline-create-row .inline-create-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2)}.inline-create-row .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary);flex-shrink:0}.inline-create-error{display:flex;align-items:center;gap:6px;margin-top:4px;padding:4px 8px;background:color-mix(in srgb,var(--tn-error, #d32f2f) 10%,transparent);border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.inline-create-error .error-icon{flex-shrink:0}.inline-create-error .error-text{flex:1}.file-list-viewport{width:100%;height:100%}.file-list-viewport .cdk-virtual-scroll-content-wrapper{width:100%}tn-table{width:100%}tn-table th,tn-table .tn-table__header-cell{font-weight:600;color:var(--tn-fg1);padding:12px 16px;border-bottom:2px solid var(--tn-lines)}tn-table td,tn-table .tn-table__cell{padding:8px 16px;border-bottom:1px solid var(--tn-lines)}.file-name-cell{display:flex;align-items:center;gap:8px;cursor:pointer}.file-name-cell.disabled{opacity:.5;color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-name{color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-icon{opacity:.6}.file-name-cell.disabled:has(.file-icon-folder),.file-name-cell.disabled:has(.file-icon-dataset),.file-name-cell.disabled:has(.file-icon-mountpoint){cursor:pointer}.file-name-cell.disabled:not(:has(.file-icon-folder)):not(:has(.file-icon-dataset)):not(:has(.file-icon-mountpoint)){cursor:not-allowed}.file-icon{display:flex;align-items:center;justify-content:center;font-size:var(--tn-icon-md, 20px);flex-shrink:0;line-height:1}.file-icon.file-icon-folder{color:var(--tn-primary)}.file-icon.file-icon-dataset{color:var(--tn-blue, #007db3)}.file-icon.file-icon-zvol{color:var(--tn-green, #71BF44)}.file-icon.file-icon-mountpoint{color:var(--tn-orange, #E68D37)}.file-name{flex:1;font-weight:500;line-height:1.4}.zfs-badge{display:inline-flex;align-items:center;background:var(--tn-alt-bg2);color:var(--tn-alt-fg2);font-size:.625rem;font-weight:600;padding:2px 6px;border-radius:12px;text-transform:uppercase;letter-spacing:.5px;line-height:1}.zfs-badge.zfs-badge-dataset{background:var(--tn-blue);color:#fff}.zfs-badge.zfs-badge-zvol{background:var(--tn-green);color:#fff}.zfs-badge.zfs-badge-mountpoint{background:var(--tn-orange);color:#fff}.permission-icon{display:flex;align-items:center;justify-content:center;color:var(--tn-red);font-size:var(--tn-icon-sm, 16px);line-height:1}.navigate-button{display:flex;align-items:center;justify-content:center;margin-left:auto;padding:2px;background:none;border:none;border-radius:4px;color:var(--tn-fg2);cursor:pointer}.navigate-button:hover,.navigate-button:focus-visible{background:var(--tn-bg3);color:var(--tn-primary)}.file-type{font-size:1rem;padding:2px 8px;border-radius:12px}.file-type.type-folder{background:var(--tn-alt-bg1);color:var(--tn-alt-fg2)}.file-type.type-file{background:var(--tn-bg2);color:var(--tn-fg2)}.file-type.type-dataset{background:#007db31a;color:var(--tn-blue)}.file-type.type-zvol{background:#71bf441a;color:var(--tn-green)}.file-type.type-mountpoint{background:#e68d371a;color:var(--tn-orange)}.folder-indicator{color:var(--tn-alt-fg1);font-style:italic}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--tn-alt-fg1);text-align:center}.empty-state tn-icon{margin-bottom:16px;opacity:.5}.empty-state p{margin:0;font-size:1rem}.tn-file-picker-footer{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px var(--tn-content-padding, 24px);border-top:1px solid var(--tn-lines);background:var(--tn-bg2);border-bottom-left-radius:8px;border-bottom-right-radius:8px}.tn-file-picker-footer .footer-selection{display:flex;align-items:center;gap:12px}.selection-count{font-size:1rem;color:var(--tn-fg2);font-weight:500}.footer-actions{display:flex;gap:8px}@media(prefers-reduced-motion:reduce){.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){:host{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnTableComponent, selector: "tn-table", inputs: ["dataSource", "displayedColumns", "trackBy", "emptyMessage", "emptyIcon", "selectable", "expandable", "bordered", "isRowExpandable", "activeRow", "activeWhen", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable"], outputs: ["sortChange", "selectionChange", "rowClick", "rowDoubleClick"] }, { kind: "directive", type: TnTableColumnDirective, selector: "[tnColumnDef]", inputs: ["tnColumnDef", "sortable", "width"], exportAs: ["tnColumnDef"] }, { kind: "directive", type: TnHeaderCellDefDirective, selector: "[tnHeaderCellDef]" }, { kind: "directive", type: TnCellDefDirective, selector: "[tnCellDef]" }, { kind: "ngmodule", type: ScrollingModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
|
|
18816
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFilePickerPopupComponent, isStandalone: true, selector: "tn-file-picker-popup", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, createActions: { classPropertyName: "createActions", publicName: "createActions", isSignal: true, isRequired: false, transformFunction: null }, currentPath: { classPropertyName: "currentPath", publicName: "currentPath", isSignal: true, isRequired: false, transformFunction: null }, rootPath: { classPropertyName: "rootPath", publicName: "rootPath", isSignal: true, isRequired: false, transformFunction: null }, fileItems: { classPropertyName: "fileItems", publicName: "fileItems", isSignal: true, isRequired: false, transformFunction: null }, selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null }, testIdBase: { classPropertyName: "testIdBase", publicName: "testIdBase", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemDoubleClick: "itemDoubleClick", pathNavigate: "pathNavigate", createAction: "createAction", created: "created", clearSelection: "clearSelection", close: "close", submit: "submit", cancel: "cancel" }, host: { classAttribute: "tn-file-picker-popup" }, ngImport: i0, template: "<!-- Header with breadcrumb navigation -->\n<div class=\"tn-file-picker-header\">\n <nav class=\"tn-file-picker-breadcrumb\" aria-label=\"File path\">\n @for (segment of currentPath() | tnTruncatePath : rootPath(); track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n tnTestIdType=\"button\"\n [tnTestId]=\"breadcrumbTestId(segment.name)\"\n [class.current]=\"last\"\n [class.nameless]=\"!segment.name\"\n [disabled]=\"last\"\n [attr.aria-label]=\"segment.name === '\u2026' || !segment.name ? 'Navigate to ' + segment.path : null\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\n</div>\n\n<!-- Loading indicator -->\n@if (loading()) {\n <div class=\"tn-file-picker-loading\">\n <tn-icon name=\"loading\" library=\"mdi\" />\n <span>Loading...</span>\n </div>\n}\n\n<!-- File table -->\n@if (!loading()) {\n <div class=\"tn-file-picker-content\">\n\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\"\n [clickable]=\"true\"\n [activeWhen]=\"isRowSelected\"\n (rowClick)=\"onItemClick($event)\"\n (rowDoubleClick)=\"onItemDoubleClick($event)\">\n\n <!-- Selection column: explicit width so the auto table layout doesn't\n hand the near-empty column a share of the spare width -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\" width=\"20px\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n @if (!item.inlineCreate) {\n <tn-checkbox\n [hideLabel]=\"true\"\n [label]=\"'Select ' + item.name\"\n [testId]=\"itemTestId(item)\"\n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\" />\n }\n </ng-template>\n </ng-container>\n }\n\n <!-- Name column -->\n <ng-container tnColumnDef=\"name\">\n <ng-template tnHeaderCellDef>Name</ng-template>\n <ng-template let-item tnCellDef>\n\n <!-- Inline creation row for create actions with a `create` callback -->\n @if (item.inlineCreate) {\n @if (inlineCreation(); as creation) {\n <div class=\"inline-create-cell\">\n <div class=\"inline-create-row\" [class.has-error]=\"!!creation.error\">\n <tn-icon class=\"file-icon\" [name]=\"creation.action.icon || 'mdi-folder'\" />\n <input\n type=\"text\"\n class=\"inline-create-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n tnTestIdType=\"input\"\n [tnTestId]=\"inlineCreateTestId()\"\n [attr.aria-label]=\"creation.action.label + ' name'\"\n [attr.aria-invalid]=\"creation.error ? true : null\"\n [attr.aria-describedby]=\"creation.error ? inlineErrorId : null\"\n [class.error]=\"!!creation.error\"\n [disabled]=\"creation.loading\"\n [value]=\"creation.name\"\n (input)=\"onInlineCreationInput($event)\"\n (keydown)=\"onInlineCreationKeyDown($event)\"\n (blur)=\"onInlineCreationBlur()\">\n @if (creation.loading) {\n <tn-icon name=\"mdi-loading\" class=\"creation-loading-icon\" aria-hidden=\"true\" />\n }\n </div>\n @if (creation.error) {\n <div class=\"inline-create-error\" role=\"alert\" [id]=\"inlineErrorId\">\n <tn-icon name=\"mdi-alert-circle\" class=\"error-icon\" aria-hidden=\"true\" />\n <span class=\"error-text\">{{ creation.error }}</span>\n </div>\n }\n </div>\n }\n } @else {\n <div\n class=\"file-name-cell\"\n tnTestIdType=\"option\"\n [tnTestId]=\"itemTestId(item)\"\n [class.selected]=\"isSelected(item)\"\n [class.disabled]=\"!!item.dimmed\"\n [class.zfs-object]=\"isZfsObject(item)\">\n <tn-icon\n [name]=\"getItemIcon(item)\"\n [class]=\"'file-icon file-icon-' + item.type\" />\n <span class=\"file-name\">{{ item.name }}</span>\n\n <!-- ZFS badge -->\n @if (isZfsObject(item)) {\n <span\n [class]=\"'zfs-badge zfs-badge-' + item.type\">\n {{ getZfsBadge(item) }}\n </span>\n }\n\n <!-- Permission indicator -->\n @if (item.permissions === 'none') {\n <tn-icon\n name=\"lock\"\n library=\"mdi\"\n class=\"permission-icon\" />\n }\n\n <!-- Navigation affordance: single click opens the directory -->\n @if (isNavigatable(item)) {\n <button\n type=\"button\"\n class=\"navigate-button\"\n tnTestIdType=\"button\"\n [tnTestId]=\"navigateTestId(item)\"\n [attr.aria-label]=\"'Open ' + item.name\"\n [attr.title]=\"'Open ' + item.name\"\n (click)=\"onNavigateClick($event, item)\"\n (dblclick)=\"$event.stopPropagation()\">\n <tn-icon name=\"mdi-chevron-right\" size=\"sm\" />\n </button>\n }\n </div>\n }\n </ng-template>\n </ng-container>\n\n <!-- Size column -->\n <ng-container tnColumnDef=\"size\">\n <ng-template tnHeaderCellDef>Size</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.size !== undefined) {\n <span>{{ item.size | tnFileSize }}</span>\n }\n @if (item.size === undefined && item.type === 'folder') {\n <span class=\"folder-indicator\">--</span>\n }\n </ng-template>\n </ng-container>\n\n <!-- Modified column -->\n <ng-container tnColumnDef=\"modified\">\n <ng-template tnHeaderCellDef>Modified</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.modified) {\n <span>{{ formatDate(item.modified) }}</span>\n }\n </ng-template>\n </ng-container>\n\n\n </tn-table>\n\n <!-- Empty state -->\n @if (filteredFileItems().length === 0) {\n <div class=\"empty-state\">\n <tn-icon name=\"folder-open\" library=\"mdi\" customSize=\"48px\" />\n <p>No items found</p>\n </div>\n }\n </div>\n}\n\n<!-- Footer -->\n@if (!loading()) {\n <div class=\"tn-file-picker-footer\">\n <div class=\"footer-selection\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n <tn-button\n variant=\"outline\"\n label=\"Clear Selection\"\n [testId]=\"clearSelectionTestId()\"\n (onClick)=\"onClearSelection()\" />\n } @else if (allowCurrentDirectorySelection()) {\n <span class=\"selection-count current-directory-selection\">\n Current directory selected\n </span>\n } @else {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n </div>\n <div class=\"footer-actions\">\n @for (action of createActions(); track action.id) {\n <tn-button\n variant=\"outline\"\n [label]=\"action.label\"\n [testId]=\"createActionTestId(action)\"\n [disabled]=\"!!inlineCreation()\"\n (onClick)=\"onCreateAction(action)\" />\n }\n <!-- Disabled while the inline creation row is open: its blur auto-submit\n would race the selection submit -->\n <tn-button\n label=\"Select\"\n [testId]=\"selectTestId()\"\n [disabled]=\"(selectedItems().length === 0 && !allowCurrentDirectorySelection()) || !!inlineCreation()\"\n (onClick)=\"onSubmit()\" />\n </div>\n </div>\n}", styles: [":host{display:block;background:var(--tn-bg1, white);color:var(--tn-fg1, #333);padding:0;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014;border-radius:8px;border:1px solid var(--tn-lines, #e0e0e0);width:520px;max-width:calc(100vw - 32px);min-height:min(500px,100vh - 16px);max-height:min(600px,100vh - 16px);font-family:var(--tn-font-family-body);display:flex;flex-direction:column;overflow:hidden}.tn-file-picker-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tn-content-padding, 24px);padding-bottom:16px;border-bottom:1px solid var(--tn-lines)}.tn-file-picker-breadcrumb{display:flex;align-items:center;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 2px;border-radius:4px;font-size:1rem;white-space:nowrap;transition:background-color .15s ease-in-out}.tn-file-picker-breadcrumb .breadcrumb-segment:hover:not(:disabled){background:var(--tn-bg2)}.tn-file-picker-breadcrumb .breadcrumb-segment:disabled,.tn-file-picker-breadcrumb .breadcrumb-segment.current{color:var(--tn-fg1);cursor:default;font-weight:500}.tn-file-picker-breadcrumb .breadcrumb-segment:not(:last-child):after{content:\"/\";margin-left:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment:first-child:before{content:\"/\";margin-right:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment.nameless:not(:last-child):after{content:none}.tn-file-picker-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px;color:var(--tn-fg2)}.tn-file-picker-loading tn-icon{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tn-file-picker-content{flex:1;min-height:0;overflow-y:auto}.inline-create-cell{display:flex;flex-direction:column;gap:4px;flex:1;min-width:0}.inline-create-row{display:flex;align-items:center;gap:8px}.inline-create-row .inline-create-input{flex:1;padding:4px 8px;border:2px solid var(--tn-primary);border-radius:3px;background:var(--tn-bg1);color:var(--tn-fg1);font-size:inherit;font-family:inherit;outline:none;min-width:0}.inline-create-row .inline-create-input.error{border-color:var(--tn-error, #d32f2f)}.inline-create-row .inline-create-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2)}.inline-create-row .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary);flex-shrink:0}.inline-create-error{display:flex;align-items:center;gap:6px;margin-top:4px;padding:4px 8px;background:color-mix(in srgb,var(--tn-error, #d32f2f) 10%,transparent);border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.inline-create-error .error-icon{flex-shrink:0}.inline-create-error .error-text{flex:1}.file-list-viewport{width:100%;height:100%}.file-list-viewport .cdk-virtual-scroll-content-wrapper{width:100%}tn-table{width:100%}tn-table th,tn-table .tn-table__header-cell{font-weight:600;color:var(--tn-fg1);padding:12px 16px;border-bottom:2px solid var(--tn-lines)}tn-table td,tn-table .tn-table__cell{padding:8px 16px;border-bottom:1px solid var(--tn-lines)}.file-name-cell{display:flex;align-items:center;gap:8px;cursor:pointer}.file-name-cell.disabled{opacity:.5;color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-name{color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-icon{opacity:.6}.file-name-cell.disabled:has(.file-icon-folder),.file-name-cell.disabled:has(.file-icon-dataset),.file-name-cell.disabled:has(.file-icon-mountpoint){cursor:pointer}.file-name-cell.disabled:not(:has(.file-icon-folder)):not(:has(.file-icon-dataset)):not(:has(.file-icon-mountpoint)){cursor:not-allowed}.file-icon{display:flex;align-items:center;justify-content:center;font-size:var(--tn-icon-md, 20px);flex-shrink:0;line-height:1}.file-icon.file-icon-folder{color:var(--tn-primary)}.file-icon.file-icon-dataset{color:var(--tn-blue, #007db3)}.file-icon.file-icon-zvol{color:var(--tn-green, #71BF44)}.file-icon.file-icon-mountpoint{color:var(--tn-orange, #E68D37)}.file-name{flex:1;font-weight:500;line-height:1.4}.zfs-badge{display:inline-flex;align-items:center;background:var(--tn-alt-bg2);color:var(--tn-alt-fg2);font-size:.625rem;font-weight:600;padding:2px 6px;border-radius:12px;text-transform:uppercase;letter-spacing:.5px;line-height:1}.zfs-badge.zfs-badge-dataset{background:var(--tn-blue);color:#fff}.zfs-badge.zfs-badge-zvol{background:var(--tn-green);color:#fff}.zfs-badge.zfs-badge-mountpoint{background:var(--tn-orange);color:#fff}.permission-icon{display:flex;align-items:center;justify-content:center;color:var(--tn-red);font-size:var(--tn-icon-sm, 16px);line-height:1}.navigate-button{display:flex;align-items:center;justify-content:center;margin-left:auto;padding:2px;background:none;border:none;border-radius:4px;color:var(--tn-fg2);cursor:pointer}.navigate-button:hover,.navigate-button:focus-visible{background:var(--tn-bg3);color:var(--tn-primary)}.file-type{font-size:1rem;padding:2px 8px;border-radius:12px}.file-type.type-folder{background:var(--tn-alt-bg1);color:var(--tn-alt-fg2)}.file-type.type-file{background:var(--tn-bg2);color:var(--tn-fg2)}.file-type.type-dataset{background:#007db31a;color:var(--tn-blue)}.file-type.type-zvol{background:#71bf441a;color:var(--tn-green)}.file-type.type-mountpoint{background:#e68d371a;color:var(--tn-orange)}.folder-indicator{color:var(--tn-alt-fg1);font-style:italic}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--tn-alt-fg1);text-align:center}.empty-state tn-icon{margin-bottom:16px;opacity:.5}.empty-state p{margin:0;font-size:1rem}.tn-file-picker-footer{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px var(--tn-content-padding, 24px);border-top:1px solid var(--tn-lines);background:var(--tn-bg2);border-bottom-left-radius:8px;border-bottom-right-radius:8px}.tn-file-picker-footer .footer-selection{display:flex;align-items:center;gap:12px}.selection-count{font-size:1rem;color:var(--tn-fg2);font-weight:500}.footer-actions{display:flex;gap:8px}@media(prefers-reduced-motion:reduce){.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){:host{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnTableComponent, selector: "tn-table", inputs: ["dataSource", "displayedColumns", "trackBy", "emptyMessage", "emptyIcon", "selectable", "expandable", "bordered", "isRowExpandable", "activeRow", "activeWhen", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable"], outputs: ["sortChange", "selectionChange", "rowClick", "rowDoubleClick"] }, { kind: "directive", type: TnTableColumnDirective, selector: "[tnColumnDef]", inputs: ["tnColumnDef", "sortable", "width"], exportAs: ["tnColumnDef"] }, { kind: "directive", type: TnHeaderCellDefDirective, selector: "[tnHeaderCellDef]" }, { kind: "directive", type: TnCellDefDirective, selector: "[tnCellDef]" }, { kind: "ngmodule", type: ScrollingModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
|
|
17941
18817
|
}
|
|
17942
18818
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, decorators: [{
|
|
17943
18819
|
type: Component,
|
|
@@ -18948,14 +19824,15 @@ var TnToastPosition;
|
|
|
18948
19824
|
TnToastPosition["Bottom"] = "bottom";
|
|
18949
19825
|
})(TnToastPosition || (TnToastPosition = {}));
|
|
18950
19826
|
|
|
18951
|
-
//
|
|
18952
|
-
//
|
|
18953
|
-
//
|
|
19827
|
+
// The sprite generator only sees icon names in templates and in `tnIconMarker()` calls.
|
|
19828
|
+
// These live in a map, so without the marker they'd be dropped from the sprite on the
|
|
19829
|
+
// next regeneration — and there is no `material-icons` font in this library to fall
|
|
19830
|
+
// back to, so the toast would render an empty icon.
|
|
18954
19831
|
const TOAST_ICONS = {
|
|
18955
|
-
[TnToastType.Info]: '
|
|
18956
|
-
[TnToastType.Success]: '
|
|
18957
|
-
[TnToastType.Warning]: '
|
|
18958
|
-
[TnToastType.Error]: '
|
|
19832
|
+
[TnToastType.Info]: tnIconMarker('info', 'material'),
|
|
19833
|
+
[TnToastType.Success]: tnIconMarker('check_circle', 'material'),
|
|
19834
|
+
[TnToastType.Warning]: tnIconMarker('warning', 'material'),
|
|
19835
|
+
[TnToastType.Error]: tnIconMarker('error', 'material'),
|
|
18959
19836
|
};
|
|
18960
19837
|
class TnToastComponent {
|
|
18961
19838
|
message = signal('', ...(ngDevMode ? [{ debugName: "message" }] : []));
|
|
@@ -19792,5 +20669,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
19792
20669
|
* Generated bundle index. Do not edit.
|
|
19793
20670
|
*/
|
|
19794
20671
|
|
|
19795
|
-
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_ERRORS, 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, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerHarness, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeDataSource, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, allowsCurrentDirectorySelection, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, getSelectableTypes, injectTnFormFieldAria, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
|
|
20672
|
+
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, THEME_MAP, THEME_STORAGE_KEY, TN_CALENDAR_INTL, TN_CALENDAR_INTL_DEFAULTS, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarCellHarness, TnCalendarComponent, TnCalendarHarness, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerHarness, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeDataSource, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, allowsCurrentDirectorySelection, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, getSelectableTypes, injectTnCalendarIntl, injectTnFormFieldAria, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
|
|
19796
20673
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|