@truenas/ui-components 0.4.4 → 0.4.5
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';
|
|
@@ -13830,43 +13830,251 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
13830
13830
|
}, 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
13831
|
}], 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
13832
|
|
|
13833
|
+
/**
|
|
13834
|
+
* Day-granularity date helpers shared by the calendar views.
|
|
13835
|
+
*
|
|
13836
|
+
* The calendar deals in calendar days, but the dates a caller binds are ordinary
|
|
13837
|
+
* `Date`s and routinely carry a time of day — `new Date()` for a default, or a value
|
|
13838
|
+
* round-tripped through an API. Comparing those against the midnight dates the grid
|
|
13839
|
+
* builds gives answers that are off by up to a day, so every comparison the calendar
|
|
13840
|
+
* makes goes through here.
|
|
13841
|
+
*/
|
|
13842
|
+
/**
|
|
13843
|
+
* Orders two dates by calendar day: negative when `a` falls on an earlier day than `b`,
|
|
13844
|
+
* zero on the same day, positive when later. Time of day is ignored on both sides.
|
|
13845
|
+
*/
|
|
13846
|
+
function compareDays(a, b) {
|
|
13847
|
+
return (a.getFullYear() - b.getFullYear()
|
|
13848
|
+
|| a.getMonth() - b.getMonth()
|
|
13849
|
+
|| a.getDate() - b.getDate());
|
|
13850
|
+
}
|
|
13851
|
+
/** Whether two dates fall on the same calendar day, ignoring time of day. */
|
|
13852
|
+
function isSameDay(a, b) {
|
|
13853
|
+
return compareDays(a, b) === 0;
|
|
13854
|
+
}
|
|
13855
|
+
/** Stable per-day key, for lookups and change tracking. */
|
|
13856
|
+
function dateKey(date) {
|
|
13857
|
+
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
13858
|
+
}
|
|
13859
|
+
/**
|
|
13860
|
+
* `YYYY-MM-DD` for the day this date falls on, built from its local parts.
|
|
13861
|
+
*
|
|
13862
|
+
* Rendered onto each day cell as `data-tn-date`, which gives tests a handle on a day
|
|
13863
|
+
* that survives translation — cell text is formatted for the locale, right down to the
|
|
13864
|
+
* numerals, so matching on it only works in English.
|
|
13865
|
+
*
|
|
13866
|
+
* Deliberately not `toISOString()`: that converts to UTC first, so anywhere east or west
|
|
13867
|
+
* of Greenwich it can name the day before or after the one shown.
|
|
13868
|
+
*/
|
|
13869
|
+
function isoDateString(date) {
|
|
13870
|
+
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
|
13871
|
+
const day = `${date.getDate()}`.padStart(2, '0');
|
|
13872
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
13873
|
+
}
|
|
13874
|
+
/**
|
|
13875
|
+
* Adds whole months, clamping to the last day when the target month is shorter, so
|
|
13876
|
+
* navigating never silently lands in the month after the one asked for. `new Date` rolls
|
|
13877
|
+
* 31 January + 1 month over into early March; this returns 28 (or 29) February.
|
|
13878
|
+
*/
|
|
13879
|
+
function addMonths(date, delta) {
|
|
13880
|
+
return atMonthAndDay(date, date.getFullYear(), date.getMonth() + delta);
|
|
13881
|
+
}
|
|
13882
|
+
/**
|
|
13883
|
+
* The same month and day in another year, clamped the same way — 29 February in a
|
|
13884
|
+
* non-leap year is 28 February, not 1 March. Without the clamp, paging the year grid off
|
|
13885
|
+
* a leap day quietly changes the month as well as the year.
|
|
13886
|
+
*/
|
|
13887
|
+
function withYear(date, year) {
|
|
13888
|
+
return atMonthAndDay(date, year, date.getMonth());
|
|
13889
|
+
}
|
|
13890
|
+
/** Keeps `date`'s day-of-month where the target month is long enough to hold it. */
|
|
13891
|
+
function atMonthAndDay(date, year, month) {
|
|
13892
|
+
const lastDay = new Date(year, month + 1, 0).getDate();
|
|
13893
|
+
return new Date(year, month, Math.min(date.getDate(), lastDay));
|
|
13894
|
+
}
|
|
13895
|
+
/** How many years one page of the year grid shows. */
|
|
13896
|
+
const YEARS_PER_PAGE = 24;
|
|
13897
|
+
/**
|
|
13898
|
+
* The first year of the page containing `year`.
|
|
13899
|
+
*
|
|
13900
|
+
* Pages are fixed blocks counted from year 0, not windows centred on the year asked
|
|
13901
|
+
* about — 2020 sits near the top of 2016–2039 rather than in the middle of 2008–2031.
|
|
13902
|
+
* The year view and the header both need this answer and must agree on it, or the
|
|
13903
|
+
* header labels a span the grid isn't showing.
|
|
13904
|
+
*/
|
|
13905
|
+
function yearPageStart(year) {
|
|
13906
|
+
return Math.floor(year / YEARS_PER_PAGE) * YEARS_PER_PAGE;
|
|
13907
|
+
}
|
|
13908
|
+
/**
|
|
13909
|
+
* The day a week starts on in the given locale, as a `Date.getDay()` index — 0 for
|
|
13910
|
+
* Sunday through 6 for Saturday. Sunday in the US, Monday across most of Europe,
|
|
13911
|
+
* Saturday in much of the Middle East.
|
|
13912
|
+
*
|
|
13913
|
+
* Read from `Intl`, which reports it ISO-style (1 Monday … 7 Sunday). Not every engine
|
|
13914
|
+
* exposes it — Firefox notably lagged — so anything that cannot answer falls back to
|
|
13915
|
+
* Sunday, which is what the grid did for every locale before.
|
|
13916
|
+
*/
|
|
13917
|
+
function firstDayOfWeek(locale) {
|
|
13918
|
+
try {
|
|
13919
|
+
const info = new Intl.Locale(locale);
|
|
13920
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay;
|
|
13921
|
+
// ISO counts Sunday as 7; `Date.getDay()` counts it as 0.
|
|
13922
|
+
return firstDay === undefined ? 0 : firstDay % 7;
|
|
13923
|
+
}
|
|
13924
|
+
catch {
|
|
13925
|
+
// A malformed locale tag shouldn't take the calendar down with it.
|
|
13926
|
+
return 0;
|
|
13927
|
+
}
|
|
13928
|
+
}
|
|
13929
|
+
|
|
13930
|
+
/** The cell holding the grid's roving tabindex — the one cell focus belongs on. */
|
|
13931
|
+
const ACTIVE_CELL = '.tn-calendar-body-cell[tabindex="0"]';
|
|
13932
|
+
/**
|
|
13933
|
+
* Follows the roving tabindex with real focus, once the grid it names has rendered.
|
|
13934
|
+
*
|
|
13935
|
+
* Two situations need it, and both leave focus somewhere wrong until it runs. Arrowing
|
|
13936
|
+
* within a grid re-renders cells that persist as elements, so the browser keeps focus on
|
|
13937
|
+
* the cell just left. Switching between the day and year grids destroys the cell that
|
|
13938
|
+
* was focused outright, which drops focus to `<body>` and restarts the tab order.
|
|
13939
|
+
*
|
|
13940
|
+
* Tied to the render rather than a zero timeout: the target cell doesn't exist until the
|
|
13941
|
+
* new grid is painted.
|
|
13942
|
+
*
|
|
13943
|
+
* Does nothing when no cell can take focus — a month or page with every day disabled has
|
|
13944
|
+
* nothing worth focusing.
|
|
13945
|
+
*/
|
|
13946
|
+
function focusActiveCellAfterRender(host, injector) {
|
|
13947
|
+
afterNextRender(() => {
|
|
13948
|
+
host.nativeElement.querySelector(ACTIVE_CELL)?.focus();
|
|
13949
|
+
}, { injector });
|
|
13950
|
+
}
|
|
13951
|
+
|
|
13952
|
+
/** The wording used when an app provides none of its own. */
|
|
13953
|
+
const TN_CALENDAR_INTL_DEFAULTS = {
|
|
13954
|
+
marked: '(marked)',
|
|
13955
|
+
rangeStart: '(range start)',
|
|
13956
|
+
rangeEnd: '(range end)',
|
|
13957
|
+
inRange: '(in range)',
|
|
13958
|
+
currentYear: '(current year)',
|
|
13959
|
+
monthGridLabel: (period) => period,
|
|
13960
|
+
yearGridLabel: (startYear, endYear) => `Years ${startYear} to ${endYear}`,
|
|
13961
|
+
chooseMonthAndYear: 'Choose month and year',
|
|
13962
|
+
previousMonth: 'Previous month',
|
|
13963
|
+
nextMonth: 'Next month',
|
|
13964
|
+
previousYears: `Previous ${YEARS_PER_PAGE} years`,
|
|
13965
|
+
nextYears: `Next ${YEARS_PER_PAGE} years`,
|
|
13966
|
+
};
|
|
13967
|
+
/**
|
|
13968
|
+
* Injection token for the calendar's wording.
|
|
13969
|
+
*
|
|
13970
|
+
* Because the library ships no localized strings, this is the recommended hook for
|
|
13971
|
+
* wiring a translation service so every `tn-calendar` reads the same way. Anything left
|
|
13972
|
+
* out falls back to {@link TN_CALENDAR_INTL_DEFAULTS}, so an app can translate one
|
|
13973
|
+
* string without restating the rest.
|
|
13974
|
+
*
|
|
13975
|
+
* Dates themselves are not here — they follow `LOCALE_ID`, or the calendar's `locale`
|
|
13976
|
+
* input. See `TnCalendarComponent.locale`.
|
|
13977
|
+
*
|
|
13978
|
+
* @example
|
|
13979
|
+
* ```ts
|
|
13980
|
+
* // Fixed for the lifetime of the app — the language is chosen at build or bootstrap.
|
|
13981
|
+
* providers: [
|
|
13982
|
+
* {
|
|
13983
|
+
* provide: TN_CALENDAR_INTL,
|
|
13984
|
+
* useFactory: (translate: TranslateService): TnCalendarIntlInput => ({
|
|
13985
|
+
* marked: translate.instant('calendar.marked'),
|
|
13986
|
+
* previousMonth: translate.instant('calendar.previousMonth'),
|
|
13987
|
+
* yearGridLabel: (from, to) => translate.instant('calendar.years', { from, to }),
|
|
13988
|
+
* }),
|
|
13989
|
+
* deps: [TranslateService],
|
|
13990
|
+
* },
|
|
13991
|
+
* ];
|
|
13992
|
+
* ```
|
|
13993
|
+
*
|
|
13994
|
+
* @example
|
|
13995
|
+
* ```ts
|
|
13996
|
+
* // Switchable at runtime: a signal, so calendars already on screen follow along.
|
|
13997
|
+
* providers: [
|
|
13998
|
+
* {
|
|
13999
|
+
* provide: TN_CALENDAR_INTL,
|
|
14000
|
+
* useFactory: (): TnCalendarIntlInput => {
|
|
14001
|
+
* const translate = inject(TranslateService);
|
|
14002
|
+
* const lang = toSignal(translate.onLangChange, { initialValue: null });
|
|
14003
|
+
* return computed(() => {
|
|
14004
|
+
* lang(); // Re-read every string when the language changes.
|
|
14005
|
+
* return { marked: translate.instant('calendar.marked') };
|
|
14006
|
+
* });
|
|
14007
|
+
* },
|
|
14008
|
+
* },
|
|
14009
|
+
* ];
|
|
14010
|
+
* ```
|
|
14011
|
+
*/
|
|
14012
|
+
const TN_CALENDAR_INTL = new InjectionToken('TN_CALENDAR_INTL');
|
|
14013
|
+
/**
|
|
14014
|
+
* Resolves the wording for a calendar component: whatever the app provided, laid over
|
|
14015
|
+
* the built-in defaults.
|
|
14016
|
+
*
|
|
14017
|
+
* Returns a signal rather than a plain object so a language switch reaches calendars that
|
|
14018
|
+
* are already on screen. Read it inside a `computed` — as every caller here does — and
|
|
14019
|
+
* the labels recompute on their own.
|
|
14020
|
+
*/
|
|
14021
|
+
function injectTnCalendarIntl() {
|
|
14022
|
+
const provided = inject(TN_CALENDAR_INTL, { optional: true });
|
|
14023
|
+
return computed(() => {
|
|
14024
|
+
// Signals are functions; a plain object of wording is not.
|
|
14025
|
+
const wording = typeof provided === 'function' ? provided() : provided;
|
|
14026
|
+
return { ...TN_CALENDAR_INTL_DEFAULTS, ...(wording ?? {}) };
|
|
14027
|
+
});
|
|
14028
|
+
}
|
|
14029
|
+
|
|
13833
14030
|
class TnCalendarHeaderComponent {
|
|
13834
14031
|
currentDate = input(new Date(), ...(ngDevMode ? [{ debugName: "currentDate" }] : []));
|
|
13835
14032
|
currentView = input('month', ...(ngDevMode ? [{ debugName: "currentView" }] : []));
|
|
13836
|
-
|
|
13837
|
-
|
|
14033
|
+
/**
|
|
14034
|
+
* Locale for the month abbreviation. Falls back to the app's `LOCALE_ID`;
|
|
14035
|
+
* `tn-calendar` binds it from its own `locale` input.
|
|
14036
|
+
*/
|
|
14037
|
+
locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
13838
14038
|
viewChanged = output();
|
|
13839
14039
|
previousClicked = output();
|
|
13840
14040
|
nextClicked = output();
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
14041
|
+
appLocale = inject(LOCALE_ID);
|
|
14042
|
+
intl = injectTnCalendarIntl();
|
|
14043
|
+
resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
|
|
14044
|
+
/**
|
|
14045
|
+
* Ties the period button to its live region through `aria-describedby`. Counted rather
|
|
14046
|
+
* than drawn at random: two calendars on one page — a start and an end picker, say —
|
|
14047
|
+
* had a real chance of minting the same id out of only 10,000, and a duplicate id
|
|
14048
|
+
* silently unhooks the association with nothing to show for it.
|
|
14049
|
+
*/
|
|
14050
|
+
periodLabelId = inject(_IdGenerator).getId('tn-calendar-period-label-');
|
|
13846
14051
|
periodLabel = computed(() => {
|
|
13847
14052
|
const date = this.currentDate();
|
|
13848
14053
|
if (!date) {
|
|
13849
14054
|
return '';
|
|
13850
14055
|
}
|
|
14056
|
+
const locale = this.resolvedLocale();
|
|
13851
14057
|
if (this.currentView() === 'month') {
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
14058
|
+
// Formatted as one unit so the locale decides the order and the numerals, not a
|
|
14059
|
+
// template string. Upper-cased after the fact: no locale offers a short month name
|
|
14060
|
+
// already capitalised the way this header wants it.
|
|
14061
|
+
return date.toLocaleDateString(locale, { month: 'short', year: 'numeric' }).toUpperCase();
|
|
13855
14062
|
}
|
|
13856
14063
|
else {
|
|
13857
|
-
//
|
|
13858
|
-
|
|
13859
|
-
const startYear =
|
|
13860
|
-
const
|
|
13861
|
-
return `${startYear} – ${
|
|
14064
|
+
// The span the year grid is showing. Shared with the view so the two cannot
|
|
14065
|
+
// disagree about which page is on screen.
|
|
14066
|
+
const startYear = yearPageStart(date.getFullYear());
|
|
14067
|
+
const yearFormat = new Intl.NumberFormat(locale, { useGrouping: false });
|
|
14068
|
+
return `${yearFormat.format(startYear)} – ${yearFormat.format(startYear + YEARS_PER_PAGE - 1)}`;
|
|
13862
14069
|
}
|
|
13863
14070
|
}, ...(ngDevMode ? [{ debugName: "periodLabel" }] : []));
|
|
13864
14071
|
previousLabel = computed(() => {
|
|
13865
|
-
return this.currentView() === 'month' ?
|
|
14072
|
+
return this.currentView() === 'month' ? this.intl().previousMonth : this.intl().previousYears;
|
|
13866
14073
|
}, ...(ngDevMode ? [{ debugName: "previousLabel" }] : []));
|
|
13867
14074
|
nextLabel = computed(() => {
|
|
13868
|
-
return this.currentView() === 'month' ?
|
|
14075
|
+
return this.currentView() === 'month' ? this.intl().nextMonth : this.intl().nextYears;
|
|
13869
14076
|
}, ...(ngDevMode ? [{ debugName: "nextLabel" }] : []));
|
|
14077
|
+
periodButtonLabel = computed(() => this.intl().chooseMonthAndYear, ...(ngDevMode ? [{ debugName: "periodButtonLabel" }] : []));
|
|
13870
14078
|
toggleView() {
|
|
13871
14079
|
const newView = this.currentView() === 'month' ? 'year' : 'month';
|
|
13872
14080
|
this.viewChanged.emit(newView);
|
|
@@ -13878,12 +14086,12 @@ class TnCalendarHeaderComponent {
|
|
|
13878
14086
|
this.nextClicked.emit();
|
|
13879
14087
|
}
|
|
13880
14088
|
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 }
|
|
14089
|
+
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
14090
|
}
|
|
13883
14091
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarHeaderComponent, decorators: [{
|
|
13884
14092
|
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 }] }],
|
|
14093
|
+
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"] }]
|
|
14094
|
+
}], 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
14095
|
|
|
13888
14096
|
class TnMonthViewComponent {
|
|
13889
14097
|
activeDate = input(new Date(), ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
|
|
@@ -13891,25 +14099,114 @@ class TnMonthViewComponent {
|
|
|
13891
14099
|
minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
|
|
13892
14100
|
maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
|
|
13893
14101
|
dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
|
|
14102
|
+
/**
|
|
14103
|
+
* Dates to flag as noteworthy — days a task runs, days with events, and the like.
|
|
14104
|
+
* Order and time-of-day are ignored; only the calendar day is compared. The
|
|
14105
|
+
* calendar owns how a marked day looks, so callers pass dates rather than styles.
|
|
14106
|
+
*/
|
|
14107
|
+
markedDates = input(undefined, ...(ngDevMode ? [{ debugName: "markedDates" }] : []));
|
|
13894
14108
|
// Range mode inputs
|
|
13895
14109
|
rangeMode = input(false, ...(ngDevMode ? [{ debugName: "rangeMode" }] : []));
|
|
13896
14110
|
selectedRange = input(undefined, ...(ngDevMode ? [{ debugName: "selectedRange" }] : []));
|
|
13897
14111
|
selectedChange = output();
|
|
13898
14112
|
activeDateChange = output();
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
13906
|
-
|
|
13907
|
-
|
|
14113
|
+
/**
|
|
14114
|
+
* Locale for dates, weekday names and the first day of the week. Falls back to the
|
|
14115
|
+
* app's `LOCALE_ID`; `tn-calendar` binds it from its own `locale` input.
|
|
14116
|
+
*/
|
|
14117
|
+
locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
14118
|
+
host = inject(ElementRef);
|
|
14119
|
+
injector = inject(Injector);
|
|
14120
|
+
appLocale = inject(LOCALE_ID);
|
|
14121
|
+
intl = injectTnCalendarIntl();
|
|
14122
|
+
resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
|
|
14123
|
+
/**
|
|
14124
|
+
* Which day the week starts on in this locale, as a `Date.getDay()` index. Everything
|
|
14125
|
+
* about the grid's shape hangs off it: the order of the headings and how many blank
|
|
14126
|
+
* cells precede the 1st.
|
|
14127
|
+
*/
|
|
14128
|
+
weekStart = computed(() => firstDayOfWeek(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "weekStart" }] : []));
|
|
14129
|
+
/** Day numbers in the locale's own numerals, without a thousands separator. */
|
|
14130
|
+
dayFormat = computed(() => {
|
|
14131
|
+
return new Intl.NumberFormat(this.resolvedLocale(), { useGrouping: false });
|
|
14132
|
+
}, ...(ngDevMode ? [{ debugName: "dayFormat" }] : []));
|
|
14133
|
+
/**
|
|
14134
|
+
* Column headings, named and ordered by the locale — Sunday first in the US, Monday
|
|
14135
|
+
* across most of Europe. The dates are an arbitrary week, used only to name the days.
|
|
14136
|
+
*/
|
|
14137
|
+
weekdays = computed(() => {
|
|
14138
|
+
const locale = this.resolvedLocale();
|
|
14139
|
+
const long = new Intl.DateTimeFormat(locale, { weekday: 'long' });
|
|
14140
|
+
const narrow = new Intl.DateTimeFormat(locale, { weekday: 'narrow' });
|
|
14141
|
+
const weekStart = this.weekStart();
|
|
14142
|
+
return Array.from({ length: 7 }, (_, column) => {
|
|
14143
|
+
// 7 Jan 2024 was a Sunday, so adding a getDay() index lands on that weekday.
|
|
14144
|
+
const date = new Date(2024, 0, 7 + ((weekStart + column) % 7));
|
|
14145
|
+
return { long: long.format(date), short: narrow.format(date) };
|
|
14146
|
+
});
|
|
14147
|
+
}, ...(ngDevMode ? [{ debugName: "weekdays" }] : []));
|
|
13908
14148
|
// Cell sizing now controlled via CSS custom properties in the SCSS file
|
|
14149
|
+
markedDateKeys = computed(() => {
|
|
14150
|
+
return new Set((this.markedDates() ?? []).map((date) => dateKey(date)));
|
|
14151
|
+
}, ...(ngDevMode ? [{ debugName: "markedDateKeys" }] : []));
|
|
14152
|
+
/**
|
|
14153
|
+
* Names the grid after the month it shows. Without it a screen-reader user arrowing
|
|
14154
|
+
* around hears each day but never which month they are in — the period only appears in
|
|
14155
|
+
* the header, outside the grid.
|
|
14156
|
+
*/
|
|
14157
|
+
gridLabel = computed(() => {
|
|
14158
|
+
const period = this.activeDate()
|
|
14159
|
+
.toLocaleDateString(this.resolvedLocale(), { month: 'long', year: 'numeric' });
|
|
14160
|
+
return this.intl().monthGridLabel(period);
|
|
14161
|
+
}, ...(ngDevMode ? [{ debugName: "gridLabel" }] : []));
|
|
14162
|
+
/**
|
|
14163
|
+
* The day of the month that carries `tabindex="0"` — the grid's single keyboard entry
|
|
14164
|
+
* point, per the roving tabindex pattern.
|
|
14165
|
+
*
|
|
14166
|
+
* It follows `activeDate`, but falls back to the nearest enabled day when that day
|
|
14167
|
+
* can't take focus. Without the fallback a month whose active day is disabled (say,
|
|
14168
|
+
* anything before `minDate`) would have no tabbable cell at all, leaving the grid
|
|
14169
|
+
* unreachable by keyboard. `null` only when the whole month is disabled, where there
|
|
14170
|
+
* is nothing worth focusing anyway.
|
|
14171
|
+
*
|
|
14172
|
+
* Derived from the month rather than from `calendarRows()`, which would be circular.
|
|
14173
|
+
*/
|
|
14174
|
+
activeDay = computed(() => {
|
|
14175
|
+
const activeDate = this.activeDate();
|
|
14176
|
+
if (!activeDate) {
|
|
14177
|
+
return null;
|
|
14178
|
+
}
|
|
14179
|
+
const year = activeDate.getFullYear();
|
|
14180
|
+
const month = activeDate.getMonth();
|
|
14181
|
+
const lastDay = new Date(year, month + 1, 0).getDate();
|
|
14182
|
+
const isEnabled = (day) => this.isDateEnabled(new Date(year, month, day));
|
|
14183
|
+
// Always within the rendered month: both come off the same Date, and `Date` has
|
|
14184
|
+
// already rolled over any overflow the caller handed us.
|
|
14185
|
+
const wanted = activeDate.getDate();
|
|
14186
|
+
if (isEnabled(wanted)) {
|
|
14187
|
+
return wanted;
|
|
14188
|
+
}
|
|
14189
|
+
// Spiral outwards from the day we wanted, forward first.
|
|
14190
|
+
for (let offset = 1; offset < lastDay; offset++) {
|
|
14191
|
+
if (wanted + offset <= lastDay && isEnabled(wanted + offset)) {
|
|
14192
|
+
return wanted + offset;
|
|
14193
|
+
}
|
|
14194
|
+
if (wanted - offset >= 1 && isEnabled(wanted - offset)) {
|
|
14195
|
+
return wanted - offset;
|
|
14196
|
+
}
|
|
14197
|
+
}
|
|
14198
|
+
return null;
|
|
14199
|
+
}, ...(ngDevMode ? [{ debugName: "activeDay" }] : []));
|
|
14200
|
+
activeCellDate = computed(() => {
|
|
14201
|
+
const day = this.activeDay();
|
|
14202
|
+
if (day === null) {
|
|
14203
|
+
return null;
|
|
14204
|
+
}
|
|
14205
|
+
const activeDate = this.activeDate();
|
|
14206
|
+
return new Date(activeDate.getFullYear(), activeDate.getMonth(), day);
|
|
14207
|
+
}, ...(ngDevMode ? [{ debugName: "activeCellDate" }] : []));
|
|
13909
14208
|
calendarRows = computed(() => {
|
|
13910
14209
|
const activeDate = this.activeDate();
|
|
13911
|
-
// Track selectedRange signal so computed recalculates when range changes
|
|
13912
|
-
this.selectedRange();
|
|
13913
14210
|
if (!activeDate) {
|
|
13914
14211
|
return [];
|
|
13915
14212
|
}
|
|
@@ -13917,7 +14214,13 @@ class TnMonthViewComponent {
|
|
|
13917
14214
|
const month = activeDate.getMonth();
|
|
13918
14215
|
const firstDate = new Date(year, month, 1);
|
|
13919
14216
|
const lastDate = new Date(year, month + 1, 0);
|
|
13920
|
-
|
|
14217
|
+
// How far into the week the 1st falls, counted from wherever this locale's week
|
|
14218
|
+
// begins — that many blanks lead the grid.
|
|
14219
|
+
const startDayOfWeek = (firstDate.getDay() - this.weekStart() + 7) % 7;
|
|
14220
|
+
// Read once for the whole grid rather than per cell, so every day is measured
|
|
14221
|
+
// against the same instant as well as allocating a fortieth as much.
|
|
14222
|
+
const today = new Date();
|
|
14223
|
+
const dayFormat = this.dayFormat();
|
|
13921
14224
|
const rows = [];
|
|
13922
14225
|
let currentRow = [];
|
|
13923
14226
|
// Add empty cells for days before month starts
|
|
@@ -13927,7 +14230,7 @@ class TnMonthViewComponent {
|
|
|
13927
14230
|
// Add all days of the month
|
|
13928
14231
|
for (let day = 1; day <= lastDate.getDate(); day++) {
|
|
13929
14232
|
const date = new Date(year, month, day);
|
|
13930
|
-
currentRow.push(this.createCell(date, day));
|
|
14233
|
+
currentRow.push(this.createCell(date, day, today, dayFormat));
|
|
13931
14234
|
// If we have 7 cells, complete the row
|
|
13932
14235
|
if (currentRow.length === 7) {
|
|
13933
14236
|
rows.push(currentRow);
|
|
@@ -13943,10 +14246,10 @@ class TnMonthViewComponent {
|
|
|
13943
14246
|
}
|
|
13944
14247
|
return rows;
|
|
13945
14248
|
}, ...(ngDevMode ? [{ debugName: "calendarRows" }] : []));
|
|
13946
|
-
createCell(date, value) {
|
|
13947
|
-
const
|
|
13948
|
-
const
|
|
13949
|
-
const
|
|
14249
|
+
createCell(date, value, today, dayFormat) {
|
|
14250
|
+
const isToday = isSameDay(date, today);
|
|
14251
|
+
const isSelected = this.selected() ? isSameDay(date, this.selected()) : false;
|
|
14252
|
+
const isMarked = this.markedDateKeys().has(dateKey(date));
|
|
13950
14253
|
const enabled = this.isDateEnabled(date);
|
|
13951
14254
|
// Range mode calculations
|
|
13952
14255
|
let rangeStart = false;
|
|
@@ -13955,24 +14258,36 @@ class TnMonthViewComponent {
|
|
|
13955
14258
|
const currentRange = this.selectedRange();
|
|
13956
14259
|
if (this.rangeMode() && currentRange) {
|
|
13957
14260
|
const { start, end } = currentRange;
|
|
13958
|
-
if (start &&
|
|
14261
|
+
if (start && isSameDay(date, start)) {
|
|
13959
14262
|
rangeStart = true;
|
|
13960
14263
|
}
|
|
13961
|
-
if (end &&
|
|
14264
|
+
if (end && isSameDay(date, end)) {
|
|
13962
14265
|
rangeEnd = true;
|
|
13963
14266
|
}
|
|
13964
|
-
|
|
14267
|
+
// Strictly between the ends. Compared by day so a start or end carrying a time of
|
|
14268
|
+
// day can't come out as both a cap and an in-between day.
|
|
14269
|
+
if (start && end && compareDays(date, start) > 0 && compareDays(date, end) < 0) {
|
|
13965
14270
|
inRange = true;
|
|
13966
14271
|
}
|
|
13967
14272
|
}
|
|
14273
|
+
// Selection and the range caps share one solid fill; marking sits below them and
|
|
14274
|
+
// shows only where neither applies. A cap is a selection too, so `today` must not
|
|
14275
|
+
// paint its outline over it.
|
|
14276
|
+
const isPrimary = isSelected || rangeStart || rangeEnd;
|
|
14277
|
+
const fill = isPrimary ? 'primary' : isMarked ? 'marked' : 'none';
|
|
13968
14278
|
return {
|
|
13969
14279
|
value,
|
|
13970
14280
|
date: new Date(date),
|
|
13971
|
-
|
|
13972
|
-
|
|
14281
|
+
isoDate: isoDateString(date),
|
|
14282
|
+
label: dayFormat.format(value),
|
|
14283
|
+
ariaLabel: this.formatAriaLabel(date, isMarked, rangeStart, rangeEnd, inRange),
|
|
13973
14284
|
enabled,
|
|
13974
14285
|
selected: isSelected,
|
|
13975
14286
|
today: isToday,
|
|
14287
|
+
marked: isMarked,
|
|
14288
|
+
inSelection: isSelected || rangeStart || rangeEnd || inRange,
|
|
14289
|
+
fill,
|
|
14290
|
+
todayOutline: isToday && !isPrimary,
|
|
13976
14291
|
rangeStart,
|
|
13977
14292
|
rangeEnd,
|
|
13978
14293
|
inRange,
|
|
@@ -13982,21 +14297,28 @@ class TnMonthViewComponent {
|
|
|
13982
14297
|
return {
|
|
13983
14298
|
value: 0,
|
|
13984
14299
|
date: new Date(),
|
|
14300
|
+
isoDate: '',
|
|
13985
14301
|
label: '',
|
|
13986
14302
|
ariaLabel: '',
|
|
13987
14303
|
enabled: false,
|
|
13988
14304
|
selected: false,
|
|
13989
14305
|
today: false,
|
|
14306
|
+
marked: false,
|
|
14307
|
+
inSelection: false,
|
|
14308
|
+
fill: 'none',
|
|
14309
|
+
todayOutline: false,
|
|
13990
14310
|
};
|
|
13991
14311
|
}
|
|
13992
14312
|
isDateEnabled(date) {
|
|
13993
14313
|
const minDate = this.minDate();
|
|
13994
14314
|
const maxDate = this.maxDate();
|
|
13995
14315
|
const dateFilter = this.dateFilter();
|
|
13996
|
-
|
|
14316
|
+
// Bounds are inclusive of their own day: a `minDate` of "today, 14:30" means today
|
|
14317
|
+
// is selectable, not that today is half past the deadline.
|
|
14318
|
+
if (minDate && compareDays(date, minDate) < 0) {
|
|
13997
14319
|
return false;
|
|
13998
14320
|
}
|
|
13999
|
-
if (maxDate && date >
|
|
14321
|
+
if (maxDate && compareDays(date, maxDate) > 0) {
|
|
14000
14322
|
return false;
|
|
14001
14323
|
}
|
|
14002
14324
|
if (dateFilter && !dateFilter(date)) {
|
|
@@ -14004,32 +14326,31 @@ class TnMonthViewComponent {
|
|
|
14004
14326
|
}
|
|
14005
14327
|
return true;
|
|
14006
14328
|
}
|
|
14007
|
-
|
|
14008
|
-
|
|
14009
|
-
|
|
14010
|
-
|
|
14011
|
-
|
|
14012
|
-
|
|
14013
|
-
|
|
14329
|
+
/**
|
|
14330
|
+
* Spells out the state a day is in, but only the parts no ARIA attribute already
|
|
14331
|
+
* carries. Selection lives on the button's `aria-pressed` and today on `aria-current`,
|
|
14332
|
+
* so repeating them here had screen readers announce each one twice. Marking and the
|
|
14333
|
+
* range positions have no attribute equivalent, and would otherwise be conveyed by
|
|
14334
|
+
* background colour alone.
|
|
14335
|
+
*/
|
|
14336
|
+
formatAriaLabel(date, isMarked, rangeStart, rangeEnd, inRange) {
|
|
14337
|
+
let label = date.toLocaleDateString(this.resolvedLocale(), {
|
|
14014
14338
|
weekday: 'long',
|
|
14015
14339
|
year: 'numeric',
|
|
14016
14340
|
month: 'long',
|
|
14017
14341
|
day: 'numeric'
|
|
14018
14342
|
});
|
|
14019
|
-
if (
|
|
14020
|
-
label +=
|
|
14021
|
-
}
|
|
14022
|
-
if (isToday) {
|
|
14023
|
-
label += ' (today)';
|
|
14343
|
+
if (isMarked) {
|
|
14344
|
+
label += ` ${this.intl().marked}`;
|
|
14024
14345
|
}
|
|
14025
14346
|
if (rangeStart) {
|
|
14026
|
-
label +=
|
|
14347
|
+
label += ` ${this.intl().rangeStart}`;
|
|
14027
14348
|
}
|
|
14028
14349
|
if (rangeEnd) {
|
|
14029
|
-
label +=
|
|
14350
|
+
label += ` ${this.intl().rangeEnd}`;
|
|
14030
14351
|
}
|
|
14031
14352
|
if (inRange) {
|
|
14032
|
-
label +=
|
|
14353
|
+
label += ` ${this.intl().inRange}`;
|
|
14033
14354
|
}
|
|
14034
14355
|
return label;
|
|
14035
14356
|
}
|
|
@@ -14037,23 +14358,107 @@ class TnMonthViewComponent {
|
|
|
14037
14358
|
if (cell.value === 0) {
|
|
14038
14359
|
return `empty-${index}`;
|
|
14039
14360
|
}
|
|
14040
|
-
return
|
|
14361
|
+
return dateKey(cell.date);
|
|
14041
14362
|
}
|
|
14042
14363
|
trackByRow(index) {
|
|
14043
14364
|
return index;
|
|
14044
14365
|
}
|
|
14045
14366
|
onCellClicked(cell) {
|
|
14046
|
-
if (cell.enabled
|
|
14047
|
-
|
|
14367
|
+
if (!cell.enabled || cell.value === 0) {
|
|
14368
|
+
return;
|
|
14369
|
+
}
|
|
14370
|
+
// The click already moved browser focus here, so the roving tabindex has to come
|
|
14371
|
+
// along or the two diverge: arrowing on from a clicked day would carry on from
|
|
14372
|
+
// wherever the active day happened to be, jumping the focus somewhere unrelated.
|
|
14373
|
+
this.activeDateChange.emit(cell.date);
|
|
14374
|
+
this.selectedChange.emit(cell.date);
|
|
14375
|
+
}
|
|
14376
|
+
/**
|
|
14377
|
+
* Moves the roving tabindex. Selection stays on click/Enter/Space, which the cells
|
|
14378
|
+
* are already buttons for, so this only ever changes which day is active.
|
|
14379
|
+
*
|
|
14380
|
+
* Moves that leave the displayed month emit an `activeDateChange` like any other
|
|
14381
|
+
* navigation; the grid re-renders on the new month with the target day active.
|
|
14382
|
+
*/
|
|
14383
|
+
onKeydown(event) {
|
|
14384
|
+
const from = this.activeCellDate();
|
|
14385
|
+
if (!from) {
|
|
14386
|
+
return;
|
|
14387
|
+
}
|
|
14388
|
+
const move = this.targetForKey(event, from);
|
|
14389
|
+
if (!move) {
|
|
14390
|
+
return;
|
|
14391
|
+
}
|
|
14392
|
+
// Claim the key before the browser scrolls the page with it.
|
|
14393
|
+
event.preventDefault();
|
|
14394
|
+
const landing = this.nearestEnabled(move.date, move.search);
|
|
14395
|
+
if (!landing || isSameDay(landing, from)) {
|
|
14396
|
+
return;
|
|
14397
|
+
}
|
|
14398
|
+
this.activeDateChange.emit(landing);
|
|
14399
|
+
focusActiveCellAfterRender(this.host, this.injector);
|
|
14400
|
+
}
|
|
14401
|
+
/**
|
|
14402
|
+
* The day a key aims at, and which way to look from there when that day is disabled.
|
|
14403
|
+
*
|
|
14404
|
+
* The search direction belongs to the key, not to where the target happens to fall
|
|
14405
|
+
* relative to the current day. Home and End mean "the ends of *this* month", so they
|
|
14406
|
+
* search inward: looking outward would step straight into the neighbouring month over
|
|
14407
|
+
* a single filtered-out boundary day and page the view, which is not what either key
|
|
14408
|
+
* promises. The arrows and the paging keys keep travelling the way they were already
|
|
14409
|
+
* headed, and crossing into the next month is the point of those.
|
|
14410
|
+
*/
|
|
14411
|
+
targetForKey(event, from) {
|
|
14412
|
+
const year = from.getFullYear();
|
|
14413
|
+
const month = from.getMonth();
|
|
14414
|
+
const day = from.getDate();
|
|
14415
|
+
// Overshooting a month's length is fine — the Date constructor rolls it over, which
|
|
14416
|
+
// is exactly what arrowing off the end of a month should do.
|
|
14417
|
+
switch (event.key) {
|
|
14418
|
+
case 'ArrowLeft': return { date: new Date(year, month, day - 1), search: -1 };
|
|
14419
|
+
case 'ArrowRight': return { date: new Date(year, month, day + 1), search: 1 };
|
|
14420
|
+
case 'ArrowUp': return { date: new Date(year, month, day - 7), search: -1 };
|
|
14421
|
+
case 'ArrowDown': return { date: new Date(year, month, day + 7), search: 1 };
|
|
14422
|
+
case 'Home': return { date: new Date(year, month, 1), search: 1 };
|
|
14423
|
+
case 'End': return { date: new Date(year, month + 1, 0), search: -1 };
|
|
14424
|
+
// Shift pages by a year, matching Material and the wider grid convention.
|
|
14425
|
+
case 'PageUp': return { date: addMonths(from, event.shiftKey ? -12 : -1), search: -1 };
|
|
14426
|
+
case 'PageDown': return { date: addMonths(from, event.shiftKey ? 12 : 1), search: 1 };
|
|
14427
|
+
default: return null;
|
|
14428
|
+
}
|
|
14429
|
+
}
|
|
14430
|
+
/**
|
|
14431
|
+
* Steps past days the caller has disabled, so a gap in `dateFilter` or the far side of
|
|
14432
|
+
* `minDate`/`maxDate` doesn't park focus somewhere unusable.
|
|
14433
|
+
*
|
|
14434
|
+
* Searches the way the key asked for (see `targetForKey`), then doubles back if that
|
|
14435
|
+
* finds nothing. The fallback is for the ends of the allowed range: arrowing right at
|
|
14436
|
+
* `maxDate` finds nothing ahead, doubles back onto the day we started from, and the
|
|
14437
|
+
* caller reads that as "don't move".
|
|
14438
|
+
*/
|
|
14439
|
+
nearestEnabled(target, preferred) {
|
|
14440
|
+
return this.scanForEnabled(target, preferred)
|
|
14441
|
+
?? this.scanForEnabled(target, preferred === 1 ? -1 : 1);
|
|
14442
|
+
}
|
|
14443
|
+
/** Bounded, so an everything-disabled filter stops the move instead of spinning. */
|
|
14444
|
+
scanForEnabled(from, direction) {
|
|
14445
|
+
const maxSteps = 62;
|
|
14446
|
+
const candidate = new Date(from);
|
|
14447
|
+
for (let step = 0; step <= maxSteps; step++) {
|
|
14448
|
+
if (this.isDateEnabled(candidate)) {
|
|
14449
|
+
return new Date(candidate);
|
|
14450
|
+
}
|
|
14451
|
+
candidate.setDate(candidate.getDate() + direction);
|
|
14048
14452
|
}
|
|
14453
|
+
return null;
|
|
14049
14454
|
}
|
|
14050
14455
|
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.
|
|
14456
|
+
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
14457
|
}
|
|
14053
14458
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMonthViewComponent, decorators: [{
|
|
14054
14459
|
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"] }] } });
|
|
14460
|
+
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"] }]
|
|
14461
|
+
}], 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
14462
|
|
|
14058
14463
|
class TnMultiYearViewComponent {
|
|
14059
14464
|
activeDate = input(new Date(), ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
|
|
@@ -14061,46 +14466,91 @@ class TnMultiYearViewComponent {
|
|
|
14061
14466
|
minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
|
|
14062
14467
|
maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
|
|
14063
14468
|
dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
|
|
14469
|
+
/**
|
|
14470
|
+
* Locale for the year numerals — Arabic-Indic digits in `ar`, Western in `en`. Falls
|
|
14471
|
+
* back to the app's `LOCALE_ID`; `tn-calendar` binds it from its own `locale` input.
|
|
14472
|
+
*/
|
|
14473
|
+
locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
14064
14474
|
selectedChange = output();
|
|
14065
14475
|
activeDateChange = output();
|
|
14066
|
-
|
|
14067
|
-
|
|
14476
|
+
host = inject(ElementRef);
|
|
14477
|
+
injector = inject(Injector);
|
|
14478
|
+
appLocale = inject(LOCALE_ID);
|
|
14479
|
+
intl = injectTnCalendarIntl();
|
|
14480
|
+
/** Years read out in the locale's own numerals, without a thousands separator. */
|
|
14481
|
+
yearFormat = computed(() => {
|
|
14482
|
+
return new Intl.NumberFormat(this.locale() ?? this.appLocale, { useGrouping: false });
|
|
14483
|
+
}, ...(ngDevMode ? [{ debugName: "yearFormat" }] : []));
|
|
14068
14484
|
yearsPerRow = 4;
|
|
14069
|
-
yearRowCount =
|
|
14070
|
-
|
|
14485
|
+
yearRowCount = YEARS_PER_PAGE / this.yearsPerRow;
|
|
14486
|
+
/**
|
|
14487
|
+
* The page of years on screen. Pages are fixed blocks, so the active year sits
|
|
14488
|
+
* wherever it falls within one rather than in the middle — see `yearPageStart`.
|
|
14489
|
+
*/
|
|
14071
14490
|
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 };
|
|
14491
|
+
const start = yearPageStart(this.activeDate().getFullYear());
|
|
14492
|
+
return { start, end: start + YEARS_PER_PAGE - 1 };
|
|
14078
14493
|
}, ...(ngDevMode ? [{ debugName: "yearRange" }] : []));
|
|
14494
|
+
/**
|
|
14495
|
+
* Names the grid after the span of years it shows, so a screen-reader user arrowing
|
|
14496
|
+
* around knows which page they are on rather than just hearing bare years.
|
|
14497
|
+
*/
|
|
14498
|
+
gridLabel = computed(() => {
|
|
14499
|
+
const range = this.yearRange();
|
|
14500
|
+
return this.intl().yearGridLabel(range.start, range.end);
|
|
14501
|
+
}, ...(ngDevMode ? [{ debugName: "gridLabel" }] : []));
|
|
14079
14502
|
yearRows = computed(() => {
|
|
14080
14503
|
const range = this.yearRange();
|
|
14504
|
+
const format = this.yearFormat();
|
|
14505
|
+
const formatYear = (year) => format.format(year);
|
|
14506
|
+
// Read once for the whole grid rather than per cell, matching the month view: every
|
|
14507
|
+
// year is then measured against the same instant, and 24 allocations become one.
|
|
14508
|
+
const thisYear = new Date().getFullYear();
|
|
14081
14509
|
const rows = [];
|
|
14082
14510
|
for (let row = 0; row < this.yearRowCount; row++) {
|
|
14083
14511
|
const yearRow = [];
|
|
14084
14512
|
for (let col = 0; col < this.yearsPerRow; col++) {
|
|
14085
14513
|
const year = range.start + (row * this.yearsPerRow) + col;
|
|
14086
|
-
yearRow.push(this.createYearCell(year));
|
|
14514
|
+
yearRow.push(this.createYearCell(year, formatYear, thisYear));
|
|
14087
14515
|
}
|
|
14088
14516
|
rows.push(yearRow);
|
|
14089
14517
|
}
|
|
14090
14518
|
return rows;
|
|
14091
14519
|
}, ...(ngDevMode ? [{ debugName: "yearRows" }] : []));
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14520
|
+
/**
|
|
14521
|
+
* The year that carries `tabindex="0"` — the grid's single keyboard entry point, per
|
|
14522
|
+
* the roving tabindex pattern. Follows `activeDate`, falling back to the nearest
|
|
14523
|
+
* enabled year in the page so a range whose active year is disabled still has a
|
|
14524
|
+
* tabbable cell. `null` only when every year on the page is disabled.
|
|
14525
|
+
*/
|
|
14526
|
+
activeYear = computed(() => {
|
|
14527
|
+
const range = this.yearRange();
|
|
14528
|
+
// Always inside the page: the range is derived from this same year.
|
|
14529
|
+
const wanted = this.activeDate().getFullYear();
|
|
14530
|
+
if (this.isYearEnabled(wanted)) {
|
|
14531
|
+
return wanted;
|
|
14532
|
+
}
|
|
14533
|
+
// Spiral outwards from the year we wanted, forward first.
|
|
14534
|
+
const span = range.end - range.start;
|
|
14535
|
+
for (let offset = 1; offset <= span; offset++) {
|
|
14536
|
+
if (wanted + offset <= range.end && this.isYearEnabled(wanted + offset)) {
|
|
14537
|
+
return wanted + offset;
|
|
14538
|
+
}
|
|
14539
|
+
if (wanted - offset >= range.start && this.isYearEnabled(wanted - offset)) {
|
|
14540
|
+
return wanted - offset;
|
|
14541
|
+
}
|
|
14542
|
+
}
|
|
14543
|
+
return null;
|
|
14544
|
+
}, ...(ngDevMode ? [{ debugName: "activeYear" }] : []));
|
|
14545
|
+
createYearCell(year, formatYear, thisYear) {
|
|
14095
14546
|
const selectedYear = this.selected()?.getFullYear();
|
|
14096
|
-
const isToday = year ===
|
|
14547
|
+
const isToday = year === thisYear;
|
|
14097
14548
|
const isSelected = year === selectedYear;
|
|
14098
14549
|
const enabled = this.isYearEnabled(year);
|
|
14099
14550
|
return {
|
|
14100
|
-
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
ariaLabel: this.formatYearAriaLabel(year, isSelected, isToday),
|
|
14551
|
+
year,
|
|
14552
|
+
label: formatYear(year),
|
|
14553
|
+
ariaLabel: this.formatYearAriaLabel(formatYear(year), isToday),
|
|
14104
14554
|
enabled,
|
|
14105
14555
|
selected: isSelected,
|
|
14106
14556
|
today: isToday,
|
|
@@ -14116,7 +14566,11 @@ class TnMultiYearViewComponent {
|
|
|
14116
14566
|
if (maxDate && year > maxDate.getFullYear()) {
|
|
14117
14567
|
return false;
|
|
14118
14568
|
}
|
|
14119
|
-
//
|
|
14569
|
+
// Deliberately coarse: a year is a single cell, so it gets a single verdict, and
|
|
14570
|
+
// January 1st stands in for the whole year. A filter that rules out January 1st
|
|
14571
|
+
// while allowing the rest of the year therefore disables the year outright. Material
|
|
14572
|
+
// approximates the same way; testing all 365 days per cell is not worth it, and the
|
|
14573
|
+
// month view applies the filter exactly once the user drills in.
|
|
14120
14574
|
if (dateFilter) {
|
|
14121
14575
|
const testDate = new Date(year, 0, 1);
|
|
14122
14576
|
if (!dateFilter(testDate)) {
|
|
@@ -14125,13 +14579,15 @@ class TnMultiYearViewComponent {
|
|
|
14125
14579
|
}
|
|
14126
14580
|
return true;
|
|
14127
14581
|
}
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
|
|
14582
|
+
/**
|
|
14583
|
+
* Selection is carried by the button's `aria-pressed`, so it is deliberately not
|
|
14584
|
+
* repeated here. "Current year" stays: `aria-current="date"` on a year cell is vague
|
|
14585
|
+
* enough that spelling it out earns its place.
|
|
14586
|
+
*/
|
|
14587
|
+
formatYearAriaLabel(year, isToday) {
|
|
14588
|
+
let label = year;
|
|
14133
14589
|
if (isToday) {
|
|
14134
|
-
label +=
|
|
14590
|
+
label += ` ${this.intl().currentYear}`;
|
|
14135
14591
|
}
|
|
14136
14592
|
return label;
|
|
14137
14593
|
}
|
|
@@ -14143,19 +14599,92 @@ class TnMultiYearViewComponent {
|
|
|
14143
14599
|
}
|
|
14144
14600
|
onYearClicked(cell) {
|
|
14145
14601
|
if (cell.enabled) {
|
|
14146
|
-
//
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14602
|
+
// Keeps the month and day, clamped: moving off 29 February into a non-leap year
|
|
14603
|
+
// would otherwise roll over into March and change the month too.
|
|
14604
|
+
this.selectedChange.emit(withYear(this.activeDate(), cell.year));
|
|
14605
|
+
}
|
|
14606
|
+
}
|
|
14607
|
+
/**
|
|
14608
|
+
* Moves the roving tabindex across the year grid. Picking a year stays on
|
|
14609
|
+
* click/Enter/Space, which the cells are already buttons for.
|
|
14610
|
+
*
|
|
14611
|
+
* Moves past either end of the 24-year page emit an `activeDateChange` like the
|
|
14612
|
+
* header's paging buttons do, and the grid re-renders on the neighbouring page.
|
|
14613
|
+
*/
|
|
14614
|
+
onKeydown(event) {
|
|
14615
|
+
const from = this.activeYear();
|
|
14616
|
+
if (from === null) {
|
|
14617
|
+
return;
|
|
14618
|
+
}
|
|
14619
|
+
const move = this.targetForKey(event, from);
|
|
14620
|
+
if (move === null) {
|
|
14621
|
+
return;
|
|
14622
|
+
}
|
|
14623
|
+
// Claim the key before the browser scrolls the page with it.
|
|
14624
|
+
event.preventDefault();
|
|
14625
|
+
const landing = this.nearestEnabledYear(move.year, move.search);
|
|
14626
|
+
if (landing === null || landing === from) {
|
|
14627
|
+
return;
|
|
14628
|
+
}
|
|
14629
|
+
this.activeDateChange.emit(withYear(this.activeDate(), landing));
|
|
14630
|
+
focusActiveCellAfterRender(this.host, this.injector);
|
|
14631
|
+
}
|
|
14632
|
+
/**
|
|
14633
|
+
* The year a key aims at, and which way to look from there when that year is disabled.
|
|
14634
|
+
*
|
|
14635
|
+
* The search direction belongs to the key, not to where the target happens to fall
|
|
14636
|
+
* relative to the current year. Home and End mean "the ends of *this* page", so they
|
|
14637
|
+
* search inward: looking outward would step onto the neighbouring 24-year page over a
|
|
14638
|
+
* single ruled-out boundary year and turn the page, which is not what either key
|
|
14639
|
+
* promises. The arrows and the paging keys keep travelling the way they were already
|
|
14640
|
+
* headed, and moving onto the next page is the point of those.
|
|
14641
|
+
*/
|
|
14642
|
+
targetForKey(event, from) {
|
|
14643
|
+
const range = this.yearRange();
|
|
14644
|
+
const yearsPerPage = range.end - range.start + 1;
|
|
14645
|
+
switch (event.key) {
|
|
14646
|
+
case 'ArrowLeft': return { year: from - 1, search: -1 };
|
|
14647
|
+
case 'ArrowRight': return { year: from + 1, search: 1 };
|
|
14648
|
+
case 'ArrowUp': return { year: from - this.yearsPerRow, search: -1 };
|
|
14649
|
+
case 'ArrowDown': return { year: from + this.yearsPerRow, search: 1 };
|
|
14650
|
+
case 'Home': return { year: range.start, search: 1 };
|
|
14651
|
+
case 'End': return { year: range.end, search: -1 };
|
|
14652
|
+
case 'PageUp': return { year: from - yearsPerPage, search: -1 };
|
|
14653
|
+
case 'PageDown': return { year: from + yearsPerPage, search: 1 };
|
|
14654
|
+
default: return null;
|
|
14655
|
+
}
|
|
14656
|
+
}
|
|
14657
|
+
/**
|
|
14658
|
+
* Steps past years the caller has disabled, so `minDate`/`maxDate` don't park focus
|
|
14659
|
+
* somewhere unusable.
|
|
14660
|
+
*
|
|
14661
|
+
* Searches the way the key asked for (see `targetForKey`), then doubles back if that
|
|
14662
|
+
* finds nothing. The fallback is for the ends of the allowed range: arrowing right at
|
|
14663
|
+
* `maxDate` finds nothing ahead, doubles back onto the year we started from, and the
|
|
14664
|
+
* caller reads that as "don't move".
|
|
14665
|
+
*/
|
|
14666
|
+
nearestEnabledYear(target, preferred) {
|
|
14667
|
+
return this.scanForEnabledYear(target, preferred)
|
|
14668
|
+
?? this.scanForEnabledYear(target, preferred === 1 ? -1 : 1);
|
|
14669
|
+
}
|
|
14670
|
+
/** Bounded, so an everything-disabled range stops the move instead of spinning. */
|
|
14671
|
+
scanForEnabledYear(from, direction) {
|
|
14672
|
+
const maxSteps = 48;
|
|
14673
|
+
for (let step = 0; step <= maxSteps; step++) {
|
|
14674
|
+
const candidate = from + (step * direction);
|
|
14675
|
+
if (this.isYearEnabled(candidate)) {
|
|
14676
|
+
return candidate;
|
|
14677
|
+
}
|
|
14150
14678
|
}
|
|
14679
|
+
return null;
|
|
14151
14680
|
}
|
|
14152
14681
|
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\"
|
|
14682
|
+
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
14683
|
}
|
|
14155
14684
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMultiYearViewComponent, decorators: [{
|
|
14156
14685
|
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"] }] } });
|
|
14686
|
+
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"] }]
|
|
14687
|
+
}], 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
14688
|
|
|
14160
14689
|
class TnCalendarComponent {
|
|
14161
14690
|
startView = input('month', ...(ngDevMode ? [{ debugName: "startView" }] : []));
|
|
@@ -14163,116 +14692,112 @@ class TnCalendarComponent {
|
|
|
14163
14692
|
minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
|
|
14164
14693
|
maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
|
|
14165
14694
|
dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
|
|
14695
|
+
/**
|
|
14696
|
+
* Dates to flag as noteworthy — days a task runs, days with events, and the like.
|
|
14697
|
+
* Order and time-of-day are ignored; only the calendar day is compared. The
|
|
14698
|
+
* calendar owns how a marked day looks, so callers pass dates rather than styles.
|
|
14699
|
+
* Marking is independent of `selected`/`selectedRange`, which stay authoritative
|
|
14700
|
+
* where they overlap.
|
|
14701
|
+
*/
|
|
14702
|
+
markedDates = input(undefined, ...(ngDevMode ? [{ debugName: "markedDates" }] : []));
|
|
14703
|
+
/**
|
|
14704
|
+
* Which month — or which page of years, in the year view — is on screen.
|
|
14705
|
+
*
|
|
14706
|
+
* Optional. Left unbound, the calendar opens on the month the bound value lives in and
|
|
14707
|
+
* drives navigation itself. Bind it, alongside `activeDateChange`, to drive the view
|
|
14708
|
+
* from outside: useful for a calendar that stays mounted while its value jumps to
|
|
14709
|
+
* another month, which the open-on-init behaviour alone won't follow.
|
|
14710
|
+
*
|
|
14711
|
+
* Unlike `selected`, this isn't strictly controlled — the calendar still navigates on
|
|
14712
|
+
* its own when the user pages or arrows around, so binding a constant here won't
|
|
14713
|
+
* freeze the view. Changing what you bind always wins.
|
|
14714
|
+
*/
|
|
14715
|
+
activeDate = input(undefined, ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
|
|
14716
|
+
/**
|
|
14717
|
+
* Locale for dates, month and weekday names, and which day the week starts on.
|
|
14718
|
+
*
|
|
14719
|
+
* Defaults to the app's `LOCALE_ID`, which is where an Angular app already declares
|
|
14720
|
+
* its locale — so a `ng build --localize` app is right without touching this. Bind it
|
|
14721
|
+
* only when one calendar needs a different locale from the rest of the app.
|
|
14722
|
+
*
|
|
14723
|
+
* Nothing here reads the browser's language on its own: doing so would quietly
|
|
14724
|
+
* disagree with `DatePipe` and every other locale-aware part of the app. An app that
|
|
14725
|
+
* wants that behaviour opts into it once, at bootstrap:
|
|
14726
|
+
* `{ provide: LOCALE_ID, useValue: navigator.language }`.
|
|
14727
|
+
*
|
|
14728
|
+
* Wording that isn't a date — "(marked)", the header button labels — comes from
|
|
14729
|
+
* `TN_CALENDAR_INTL` instead.
|
|
14730
|
+
*/
|
|
14731
|
+
locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
14732
|
+
appLocale = inject(LOCALE_ID);
|
|
14733
|
+
host = inject(ElementRef);
|
|
14734
|
+
injector = inject(Injector);
|
|
14735
|
+
/** The locale actually in force: the input when bound, the app's otherwise. */
|
|
14736
|
+
resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
|
|
14166
14737
|
// Range mode inputs
|
|
14167
14738
|
rangeMode = input(false, ...(ngDevMode ? [{ debugName: "rangeMode" }] : []));
|
|
14168
14739
|
selectedRange = input(undefined, ...(ngDevMode ? [{ debugName: "selectedRange" }] : []));
|
|
14169
14740
|
selectedChange = output();
|
|
14741
|
+
/**
|
|
14742
|
+
* The date the calendar is now sitting on — the month it shows, and the day holding
|
|
14743
|
+
* the grid's roving tabindex.
|
|
14744
|
+
*
|
|
14745
|
+
* This is navigation, not selection: it fires on paging, on arrow keys, and on
|
|
14746
|
+
* clicking a day (a click moves the active cell as well as choosing the date, so the
|
|
14747
|
+
* two never drift apart). Treat it as "the view moved", not "the user picked a date" —
|
|
14748
|
+
* `selectedChange` is the one that means a choice was made.
|
|
14749
|
+
*/
|
|
14170
14750
|
activeDateChange = output();
|
|
14171
14751
|
viewChanged = output();
|
|
14172
14752
|
// Range mode outputs
|
|
14173
14753
|
selectedRangeChange = output();
|
|
14174
|
-
|
|
14754
|
+
/**
|
|
14755
|
+
* Which month/year grid is on screen. Navigation state, not selection state.
|
|
14756
|
+
*
|
|
14757
|
+
* Follows the `activeDate` input whenever the caller changes it, but stays writable so
|
|
14758
|
+
* paging and arrow keys keep working with no echo back from the caller.
|
|
14759
|
+
*/
|
|
14760
|
+
currentDate = linkedSignal({ ...(ngDevMode ? { debugName: "currentDate" } : {}), source: () => this.activeDate(),
|
|
14761
|
+
computation: (activeDate, previous) => activeDate ?? previous?.value ?? new Date() });
|
|
14175
14762
|
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
14763
|
ngOnInit() {
|
|
14197
14764
|
this.currentView.set(this.startView());
|
|
14198
|
-
//
|
|
14199
|
-
if (this.
|
|
14200
|
-
|
|
14765
|
+
// A bound activeDate already says what should be on screen.
|
|
14766
|
+
if (this.activeDate()) {
|
|
14767
|
+
return;
|
|
14201
14768
|
}
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
14769
|
+
// Otherwise open on the month the caller's value lives in.
|
|
14770
|
+
const initialDate = this.rangeMode()
|
|
14771
|
+
? (this.selectedRange()?.start ?? this.selectedRange()?.end ?? null)
|
|
14772
|
+
: (this.selected() ?? null);
|
|
14773
|
+
if (initialDate) {
|
|
14774
|
+
this.currentDate.set(new Date(initialDate));
|
|
14208
14775
|
}
|
|
14209
14776
|
}
|
|
14210
|
-
initializeRangeState() {
|
|
14211
|
-
if (this.rangeMode()) {
|
|
14212
|
-
const selectedRange = this.selectedRange();
|
|
14213
|
-
if (selectedRange) {
|
|
14214
|
-
this.rangeState.set({
|
|
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
|
-
}
|
|
14234
|
-
}
|
|
14235
|
-
}
|
|
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
14777
|
onViewChanged(view) {
|
|
14250
14778
|
this.currentView.set(view);
|
|
14251
14779
|
this.viewChanged.emit(view);
|
|
14252
14780
|
}
|
|
14253
14781
|
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);
|
|
14782
|
+
this.page(-1);
|
|
14265
14783
|
}
|
|
14266
14784
|
onNextClicked() {
|
|
14785
|
+
this.page(1);
|
|
14786
|
+
}
|
|
14787
|
+
/**
|
|
14788
|
+
* Steps the view one page in `direction`: a month in the day view, a page of years in
|
|
14789
|
+
* the year view — the same spans `PageUp`/`PageDown` move, and the same the header
|
|
14790
|
+
* labels.
|
|
14791
|
+
*
|
|
14792
|
+
* Carries the day across, clamped, rather than dropping to the 1st. The keyboard has
|
|
14793
|
+
* always done this, so resetting here left the roving tabindex somewhere different
|
|
14794
|
+
* depending on whether the user clicked the arrow or pressed the key.
|
|
14795
|
+
*/
|
|
14796
|
+
page(direction) {
|
|
14267
14797
|
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
|
-
}
|
|
14798
|
+
const newDate = this.currentView() === 'month'
|
|
14799
|
+
? addMonths(current, direction)
|
|
14800
|
+
: withYear(current, current.getFullYear() + (direction * YEARS_PER_PAGE));
|
|
14276
14801
|
this.currentDate.set(newDate);
|
|
14277
14802
|
this.activeDateChange.emit(newDate);
|
|
14278
14803
|
}
|
|
@@ -14284,83 +14809,346 @@ class TnCalendarComponent {
|
|
|
14284
14809
|
this.selectedChange.emit(date);
|
|
14285
14810
|
}
|
|
14286
14811
|
}
|
|
14812
|
+
/**
|
|
14813
|
+
* Works out the next range from the one the caller currently owns and emits it. The
|
|
14814
|
+
* calendar keeps no copy — what it renders next is whatever the caller binds back.
|
|
14815
|
+
*/
|
|
14287
14816
|
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);
|
|
14817
|
+
const current = this.selectedRange();
|
|
14818
|
+
const start = current?.start ?? null;
|
|
14819
|
+
const end = current?.end ?? null;
|
|
14820
|
+
// Nothing pending, or the range is already complete: begin a new one.
|
|
14821
|
+
if (!start || end) {
|
|
14299
14822
|
this.selectedRangeChange.emit({ start: date, end: null });
|
|
14300
14823
|
return;
|
|
14301
14824
|
}
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
end: null,
|
|
14307
|
-
selecting: 'end'
|
|
14308
|
-
};
|
|
14309
|
-
this.rangeState.set(newRangeState);
|
|
14825
|
+
// A second pick before the first restarts the range rather than inverting it.
|
|
14826
|
+
// Compared by day: the clicked date is midnight, but the caller's start may carry a
|
|
14827
|
+
// time of day, which would otherwise make re-picking the same day restart the range.
|
|
14828
|
+
if (compareDays(date, start) < 0) {
|
|
14310
14829
|
this.selectedRangeChange.emit({ start: date, end: null });
|
|
14830
|
+
return;
|
|
14311
14831
|
}
|
|
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
|
-
}
|
|
14832
|
+
this.selectedRangeChange.emit({ start, end: date });
|
|
14336
14833
|
}
|
|
14337
14834
|
onActiveDateChange(date) {
|
|
14338
14835
|
this.currentDate.set(date);
|
|
14339
14836
|
this.activeDateChange.emit(date);
|
|
14340
14837
|
}
|
|
14838
|
+
/**
|
|
14839
|
+
* A year picked in the year grid moves the view to that year and returns to the day
|
|
14840
|
+
* grid. It is navigation, not selection — no `selectedChange` — so the user lands on
|
|
14841
|
+
* the month they asked for and picks a day from there.
|
|
14842
|
+
*/
|
|
14341
14843
|
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
14844
|
this.currentDate.set(date);
|
|
14345
14845
|
this.currentView.set('month');
|
|
14346
14846
|
this.viewChanged.emit('month');
|
|
14347
14847
|
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();
|
|
14848
|
+
// Switching grids destroys the year cell the user just activated, which drops focus
|
|
14849
|
+
// to `<body>` and restarts the tab order. Carry it over to the day now active.
|
|
14850
|
+
focusActiveCellAfterRender(this.host, this.injector);
|
|
14356
14851
|
}
|
|
14357
14852
|
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 [
|
|
14853
|
+
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
14854
|
}
|
|
14360
14855
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarComponent, decorators: [{
|
|
14361
14856
|
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
|
-
}],
|
|
14857
|
+
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"] }]
|
|
14858
|
+
}], 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"] }] } });
|
|
14859
|
+
|
|
14860
|
+
/**
|
|
14861
|
+
* Harness for interacting with a single day (or year) cell inside `tn-calendar`.
|
|
14862
|
+
*
|
|
14863
|
+
* Modelled on Angular Material's `MatCalendarCellHarness` so specs can move across
|
|
14864
|
+
* with the method names intact. Three differences worth knowing:
|
|
14865
|
+
*
|
|
14866
|
+
* - `isMarked()` is new — it reports the `markedDates` state, which Material has no
|
|
14867
|
+
* equivalent for.
|
|
14868
|
+
* - `getDate()` and the `date` filter are new, and are the reliable way to name a cell:
|
|
14869
|
+
* the text is formatted for the app's locale, so `'14'` misses a calendar rendering
|
|
14870
|
+
* `١٤`.
|
|
14871
|
+
* - There are no comparison-range or preview-range methods (`isComparisonRangeStart()`
|
|
14872
|
+
* and friends); `tn-calendar` has no comparison range to report.
|
|
14873
|
+
*
|
|
14874
|
+
* @example
|
|
14875
|
+
* ```typescript
|
|
14876
|
+
* const calendar = await loader.getHarness(TnCalendarHarness);
|
|
14877
|
+
* const [first] = await calendar.getCells({ date: new Date(2031, 4, 14) });
|
|
14878
|
+
* await first.select();
|
|
14879
|
+
* expect(await first.isSelected()).toBe(true);
|
|
14880
|
+
* ```
|
|
14881
|
+
*/
|
|
14882
|
+
class TnCalendarCellHarness extends ComponentHarness {
|
|
14883
|
+
/** The selector for a calendar cell. Padding cells render no button, so only real days match. */
|
|
14884
|
+
static hostSelector = '.tn-calendar-body-cell';
|
|
14885
|
+
_content = this.locatorFor('.tn-calendar-body-cell-content');
|
|
14886
|
+
/**
|
|
14887
|
+
* Gets a `HarnessPredicate` that can be used to search for a cell with specific attributes.
|
|
14888
|
+
*
|
|
14889
|
+
* @param options Options for filtering which cell instances are considered a match.
|
|
14890
|
+
* @returns A `HarnessPredicate` configured with the given options.
|
|
14891
|
+
*
|
|
14892
|
+
* @example
|
|
14893
|
+
* ```typescript
|
|
14894
|
+
* // Every day the schedule runs on
|
|
14895
|
+
* const runDays = await calendar.getCells({ marked: true });
|
|
14896
|
+
*
|
|
14897
|
+
* // The disabled days before minDate
|
|
14898
|
+
* const blocked = await calendar.getCells({ disabled: true });
|
|
14899
|
+
* ```
|
|
14900
|
+
*/
|
|
14901
|
+
static with(options = {}) {
|
|
14902
|
+
return new HarnessPredicate(TnCalendarCellHarness, options)
|
|
14903
|
+
.addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
|
|
14904
|
+
.addOption('date', options.date, async (harness, date) => (await harness.getDateStamp()) === isoDateString(date))
|
|
14905
|
+
.addOption('year', options.year, async (harness, year) => (await harness.getYear()) === year)
|
|
14906
|
+
.addOption('selected', options.selected, async (harness, selected) => (await harness.isSelected()) === selected)
|
|
14907
|
+
.addOption('active', options.active, async (harness, active) => (await harness.isActive()) === active)
|
|
14908
|
+
.addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled)
|
|
14909
|
+
.addOption('today', options.today, async (harness, today) => (await harness.isToday()) === today)
|
|
14910
|
+
.addOption('inRange', options.inRange, async (harness, inRange) => (await harness.isInRange()) === inRange)
|
|
14911
|
+
.addOption('marked', options.marked, async (harness, marked) => (await harness.isMarked()) === marked);
|
|
14912
|
+
}
|
|
14913
|
+
/** Gets the text of the calendar cell. */
|
|
14914
|
+
async getText() {
|
|
14915
|
+
return (await this._content()).text();
|
|
14916
|
+
}
|
|
14917
|
+
/** Gets the aria-label of the calendar cell. */
|
|
14918
|
+
async getAriaLabel() {
|
|
14919
|
+
return (await this.host()).getAttribute('aria-label').then((label) => label ?? '');
|
|
14920
|
+
}
|
|
14921
|
+
/**
|
|
14922
|
+
* Gets the calendar day this cell represents, independent of how it is rendered —
|
|
14923
|
+
* `getText()` is formatted for the locale, so it reads `١٤` in Arabic while this still
|
|
14924
|
+
* answers the 14th. Midnight local time. `null` on a year cell, which has no day.
|
|
14925
|
+
*/
|
|
14926
|
+
async getDate() {
|
|
14927
|
+
const stamp = await this.getDateStamp();
|
|
14928
|
+
if (!stamp) {
|
|
14929
|
+
return null;
|
|
14930
|
+
}
|
|
14931
|
+
const [year, month, day] = stamp.split('-').map(Number);
|
|
14932
|
+
return new Date(year, month - 1, day);
|
|
14933
|
+
}
|
|
14934
|
+
/**
|
|
14935
|
+
* Gets the year this cell represents, independent of how it is rendered. Works on year
|
|
14936
|
+
* cells; `null` on a day cell.
|
|
14937
|
+
*/
|
|
14938
|
+
async getYear() {
|
|
14939
|
+
const year = await (await this.host()).getAttribute('data-tn-year');
|
|
14940
|
+
return year === null ? null : Number(year);
|
|
14941
|
+
}
|
|
14942
|
+
/** The raw `YYYY-MM-DD` stamp, for comparing without rebuilding a `Date`. */
|
|
14943
|
+
async getDateStamp() {
|
|
14944
|
+
return (await this.host()).getAttribute('data-tn-date');
|
|
14945
|
+
}
|
|
14946
|
+
/** Whether the cell is selected. */
|
|
14947
|
+
async isSelected() {
|
|
14948
|
+
return (await this.host()).hasClass('tn-calendar-body-selected');
|
|
14949
|
+
}
|
|
14950
|
+
/** Whether the cell is disabled. */
|
|
14951
|
+
async isDisabled() {
|
|
14952
|
+
return (await this.host()).getProperty('disabled');
|
|
14953
|
+
}
|
|
14954
|
+
/**
|
|
14955
|
+
* Whether the cell holds the grid's roving tabindex — the one cell reachable with Tab,
|
|
14956
|
+
* which the arrow keys move. Independent of selection: arrowing around changes which
|
|
14957
|
+
* cell is active without selecting anything.
|
|
14958
|
+
*/
|
|
14959
|
+
async isActive() {
|
|
14960
|
+
return (await this.host()).hasClass('tn-calendar-body-active');
|
|
14961
|
+
}
|
|
14962
|
+
/** Whether the cell represents today's date. */
|
|
14963
|
+
async isToday() {
|
|
14964
|
+
return (await this.host()).hasClass('tn-calendar-body-today');
|
|
14965
|
+
}
|
|
14966
|
+
/** Whether the cell was flagged through the calendar's `markedDates` input. */
|
|
14967
|
+
async isMarked() {
|
|
14968
|
+
return (await this.host()).hasClass('tn-calendar-body-marked');
|
|
14969
|
+
}
|
|
14970
|
+
/** Whether the cell is the start of the selected range. */
|
|
14971
|
+
async isRangeStart() {
|
|
14972
|
+
return (await this.host()).hasClass('tn-calendar-body-range-start');
|
|
14973
|
+
}
|
|
14974
|
+
/** Whether the cell is the end of the selected range. */
|
|
14975
|
+
async isRangeEnd() {
|
|
14976
|
+
return (await this.host()).hasClass('tn-calendar-body-range-end');
|
|
14977
|
+
}
|
|
14978
|
+
/**
|
|
14979
|
+
* Whether the cell is part of the selected range, ends included — matching Material's
|
|
14980
|
+
* `isInRange()`. Use `isRangeStart()`/`isRangeEnd()` to single out the ends.
|
|
14981
|
+
*/
|
|
14982
|
+
async isInRange() {
|
|
14983
|
+
const host = await this.host();
|
|
14984
|
+
const [between, start, end] = await parallel(() => [
|
|
14985
|
+
host.hasClass('tn-calendar-body-in-range'),
|
|
14986
|
+
host.hasClass('tn-calendar-body-range-start'),
|
|
14987
|
+
host.hasClass('tn-calendar-body-range-end'),
|
|
14988
|
+
]);
|
|
14989
|
+
return between || start || end;
|
|
14990
|
+
}
|
|
14991
|
+
/** Selects the calendar cell. Won't do anything if the cell is disabled. */
|
|
14992
|
+
async select() {
|
|
14993
|
+
return (await this.host()).click();
|
|
14994
|
+
}
|
|
14995
|
+
/**
|
|
14996
|
+
* Presses a key on the cell. The grid handles its keys on the cells themselves, so
|
|
14997
|
+
* this is how the roving tabindex is driven — arrows, Home/End and PageUp/PageDown
|
|
14998
|
+
* move the active cell, while Enter and Space select.
|
|
14999
|
+
*
|
|
15000
|
+
* @param key The key to press, e.g. `TestKey.RIGHT_ARROW`.
|
|
15001
|
+
* @param modifiers Modifier keys held while pressing, e.g. `{ shift: true }`.
|
|
15002
|
+
*
|
|
15003
|
+
* @example
|
|
15004
|
+
* ```typescript
|
|
15005
|
+
* const [cell] = await calendar.getCells({ active: true });
|
|
15006
|
+
* await cell.press(TestKey.PAGE_DOWN, { shift: true }); // a year on
|
|
15007
|
+
* ```
|
|
15008
|
+
*/
|
|
15009
|
+
async press(key, modifiers) {
|
|
15010
|
+
const host = await this.host();
|
|
15011
|
+
return modifiers ? host.sendKeys(modifiers, key) : host.sendKeys(key);
|
|
15012
|
+
}
|
|
15013
|
+
/** Hovers over the calendar cell. */
|
|
15014
|
+
async hover() {
|
|
15015
|
+
return (await this.host()).hover();
|
|
15016
|
+
}
|
|
15017
|
+
/** Moves the mouse away from the calendar cell. */
|
|
15018
|
+
async mouseAway() {
|
|
15019
|
+
return (await this.host()).mouseAway();
|
|
15020
|
+
}
|
|
15021
|
+
/** Focuses the calendar cell. */
|
|
15022
|
+
async focus() {
|
|
15023
|
+
return (await this.host()).focus();
|
|
15024
|
+
}
|
|
15025
|
+
/** Removes focus from the calendar cell. */
|
|
15026
|
+
async blur() {
|
|
15027
|
+
return (await this.host()).blur();
|
|
15028
|
+
}
|
|
15029
|
+
}
|
|
15030
|
+
/**
|
|
15031
|
+
* Harness for interacting with `tn-calendar` in tests.
|
|
15032
|
+
*
|
|
15033
|
+
* The method names mirror Angular Material's `MatCalendarHarness`, so a spec written
|
|
15034
|
+
* against the Material calendar moves over unchanged. Differences to know:
|
|
15035
|
+
*
|
|
15036
|
+
* - `getCurrentView()` resolves to `'month' | 'year'`. `tn-calendar` has no separate
|
|
15037
|
+
* multi-year view — its year view *is* the multi-year grid.
|
|
15038
|
+
* - Cell filters gain `marked` for the `markedDates` state and `date`/`year` for naming
|
|
15039
|
+
* a cell without going through its rendered text, and drop Material's
|
|
15040
|
+
* `inComparisonRange`, which has no equivalent here.
|
|
15041
|
+
*
|
|
15042
|
+
* @example
|
|
15043
|
+
* ```typescript
|
|
15044
|
+
* const calendar = await loader.getHarness(TnCalendarHarness);
|
|
15045
|
+
*
|
|
15046
|
+
* // Which days does the schedule run on this month?
|
|
15047
|
+
* const marked = await calendar.getCells({ marked: true });
|
|
15048
|
+
* expect(await parallel(() => marked.map((cell) => cell.getText()))).toEqual(['24', '25']);
|
|
15049
|
+
*
|
|
15050
|
+
* // Page forward a month
|
|
15051
|
+
* await calendar.next();
|
|
15052
|
+
* expect(await calendar.getCurrentViewLabel()).toBe('MAR 2022');
|
|
15053
|
+
*
|
|
15054
|
+
* // Pick a day
|
|
15055
|
+
* await calendar.selectCell({ date: new Date(2022, 2, 14) });
|
|
15056
|
+
* ```
|
|
15057
|
+
*/
|
|
15058
|
+
class TnCalendarHarness extends ComponentHarness {
|
|
15059
|
+
/** The selector for the host element of a `TnCalendarComponent` instance. */
|
|
15060
|
+
static hostSelector = 'tn-calendar';
|
|
15061
|
+
_periodButton = this.locatorFor('.tn-calendar-period-button');
|
|
15062
|
+
_previousButton = this.locatorFor('.tn-calendar-previous-button');
|
|
15063
|
+
_nextButton = this.locatorFor('.tn-calendar-next-button');
|
|
15064
|
+
_monthView = this.locatorForOptional('tn-month-view');
|
|
15065
|
+
/**
|
|
15066
|
+
* Gets a `HarnessPredicate` that can be used to search for a calendar with specific
|
|
15067
|
+
* attributes.
|
|
15068
|
+
*
|
|
15069
|
+
* @param options Options for filtering which calendar instances are considered a match.
|
|
15070
|
+
* @returns A `HarnessPredicate` configured with the given options.
|
|
15071
|
+
*/
|
|
15072
|
+
static with(options = {}) {
|
|
15073
|
+
return new HarnessPredicate(TnCalendarHarness, options);
|
|
15074
|
+
}
|
|
15075
|
+
/**
|
|
15076
|
+
* Gets a list of cells inside the calendar. Padding cells for days outside the month
|
|
15077
|
+
* are not included.
|
|
15078
|
+
*
|
|
15079
|
+
* @param filter Optionally filters which cells are included.
|
|
15080
|
+
*/
|
|
15081
|
+
async getCells(filter = {}) {
|
|
15082
|
+
return this.locatorForAll(TnCalendarCellHarness.with(filter))();
|
|
15083
|
+
}
|
|
15084
|
+
/** Gets the current view that is being shown inside the calendar. */
|
|
15085
|
+
async getCurrentView() {
|
|
15086
|
+
return (await this._monthView()) ? 'month' : 'year';
|
|
15087
|
+
}
|
|
15088
|
+
/** Gets the label of the current calendar view, e.g. `'MAR 2022'`. */
|
|
15089
|
+
async getCurrentViewLabel() {
|
|
15090
|
+
return (await this._periodButton()).text();
|
|
15091
|
+
}
|
|
15092
|
+
/** Changes the calendar view by clicking on the view toggle button. */
|
|
15093
|
+
async changeView() {
|
|
15094
|
+
return (await this._periodButton()).click();
|
|
15095
|
+
}
|
|
15096
|
+
/** Goes to the next page of the current view (e.g. next month when inside the month view). */
|
|
15097
|
+
async next() {
|
|
15098
|
+
return (await this._nextButton()).click();
|
|
15099
|
+
}
|
|
15100
|
+
/** Goes to the previous page of the current view (e.g. previous month in the month view). */
|
|
15101
|
+
async previous() {
|
|
15102
|
+
return (await this._previousButton()).click();
|
|
15103
|
+
}
|
|
15104
|
+
/**
|
|
15105
|
+
* Selects a cell in the current calendar view.
|
|
15106
|
+
*
|
|
15107
|
+
* @param filter An optional filter to apply to the cells. The first cell matching the
|
|
15108
|
+
* filter will be selected.
|
|
15109
|
+
*/
|
|
15110
|
+
async selectCell(filter = {}) {
|
|
15111
|
+
const cells = await this.getCells(filter);
|
|
15112
|
+
if (!cells.length) {
|
|
15113
|
+
throw Error(`Cannot find calendar cell matching filter ${JSON.stringify(filter)}`);
|
|
15114
|
+
}
|
|
15115
|
+
await cells[0].select();
|
|
15116
|
+
}
|
|
15117
|
+
/**
|
|
15118
|
+
* Gets the cell holding the grid's roving tabindex — the one cell Tab reaches, and the
|
|
15119
|
+
* one the arrow keys move.
|
|
15120
|
+
*
|
|
15121
|
+
* `null` only when every cell in the view is disabled, where there is nothing to focus.
|
|
15122
|
+
*/
|
|
15123
|
+
async getActiveCell() {
|
|
15124
|
+
const [active] = await this.getCells({ active: true });
|
|
15125
|
+
return active ?? null;
|
|
15126
|
+
}
|
|
15127
|
+
/**
|
|
15128
|
+
* Moves the grid's roving tabindex with the keyboard, without going through the DOM.
|
|
15129
|
+
*
|
|
15130
|
+
* Arrows move by a day or a week (a year or a row of years); Home and End reach the
|
|
15131
|
+
* ends of the month or year page; PageUp and PageDown page, shifted by a year. Moving
|
|
15132
|
+
* never changes the selection — use `selectCell()` or `TnCalendarCellHarness.select()`
|
|
15133
|
+
* for that.
|
|
15134
|
+
*
|
|
15135
|
+
* @param key The key to press, e.g. `TestKey.RIGHT_ARROW`.
|
|
15136
|
+
* @param modifiers Modifier keys held while pressing, e.g. `{ shift: true }`.
|
|
15137
|
+
*
|
|
15138
|
+
* @example
|
|
15139
|
+
* ```typescript
|
|
15140
|
+
* await calendar.moveActiveCell(TestKey.DOWN_ARROW);
|
|
15141
|
+
* expect(await (await calendar.getActiveCell())!.getText()).toBe('17');
|
|
15142
|
+
* ```
|
|
15143
|
+
*/
|
|
15144
|
+
async moveActiveCell(key, modifiers) {
|
|
15145
|
+
const active = await this.getActiveCell();
|
|
15146
|
+
if (!active) {
|
|
15147
|
+
throw Error('Cannot move the active cell: the calendar has no cell able to take focus.');
|
|
15148
|
+
}
|
|
15149
|
+
await active.press(key, modifiers);
|
|
15150
|
+
}
|
|
15151
|
+
}
|
|
14364
15152
|
|
|
14365
15153
|
class TnDateInputComponent {
|
|
14366
15154
|
overlay = inject(Overlay);
|
|
@@ -14381,9 +15169,7 @@ class TnDateInputComponent {
|
|
|
14381
15169
|
dayRef = viewChild.required('dayInput');
|
|
14382
15170
|
yearRef = viewChild.required('yearInput');
|
|
14383
15171
|
calendarTemplate = viewChild.required('calendarTemplate');
|
|
14384
|
-
calendar = viewChild.required(TnCalendarComponent);
|
|
14385
15172
|
wrapperEl = viewChild.required('wrapper');
|
|
14386
|
-
destroy$ = new Subject();
|
|
14387
15173
|
overlayRef;
|
|
14388
15174
|
portal;
|
|
14389
15175
|
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
@@ -14399,8 +15185,6 @@ class TnDateInputComponent {
|
|
|
14399
15185
|
this.updateDisplayValues();
|
|
14400
15186
|
}
|
|
14401
15187
|
ngOnDestroy() {
|
|
14402
|
-
this.destroy$.next();
|
|
14403
|
-
this.destroy$.complete();
|
|
14404
15188
|
this.close();
|
|
14405
15189
|
}
|
|
14406
15190
|
// ControlValueAccessor implementation
|
|
@@ -14543,11 +15327,8 @@ class TnDateInputComponent {
|
|
|
14543
15327
|
}
|
|
14544
15328
|
this.createOverlay();
|
|
14545
15329
|
this.isOpen.set(true);
|
|
14546
|
-
//
|
|
14547
|
-
|
|
14548
|
-
if (cal) {
|
|
14549
|
-
setTimeout(() => cal.resetInteractionState(), 0);
|
|
14550
|
-
}
|
|
15330
|
+
// No calendar state to reset — the calendar renders whatever date this component
|
|
15331
|
+
// binds to it, so reopening already shows the current value.
|
|
14551
15332
|
}
|
|
14552
15333
|
close() {
|
|
14553
15334
|
if (this.overlayRef) {
|
|
@@ -14604,6 +15385,8 @@ class TnDateInputComponent {
|
|
|
14604
15385
|
panelClass: 'tn-datepicker-overlay'
|
|
14605
15386
|
});
|
|
14606
15387
|
// Close datepicker when backdrop is clicked
|
|
15388
|
+
// No teardown to track: `dispose()` in close() ends this subscription with the
|
|
15389
|
+
// overlay, and ngOnDestroy closes.
|
|
14607
15390
|
this.overlayRef.backdropClick().subscribe(() => {
|
|
14608
15391
|
this.close();
|
|
14609
15392
|
});
|
|
@@ -14617,7 +15400,7 @@ class TnDateInputComponent {
|
|
|
14617
15400
|
useExisting: forwardRef(() => TnDateInputComponent),
|
|
14618
15401
|
multi: true
|
|
14619
15402
|
}
|
|
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: "
|
|
15403
|
+
], 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
15404
|
}
|
|
14622
15405
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDateInputComponent, decorators: [{
|
|
14623
15406
|
type: Component,
|
|
@@ -14630,7 +15413,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
14630
15413
|
], host: {
|
|
14631
15414
|
'class': 'tn-date-input'
|
|
14632
15415
|
}, 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 }] }],
|
|
15416
|
+
}], 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
15417
|
|
|
14635
15418
|
/**
|
|
14636
15419
|
* Formats a Date into zero-padded month, day, and year strings.
|
|
@@ -14667,12 +15450,8 @@ async function setInputValue(el, value) {
|
|
|
14667
15450
|
}
|
|
14668
15451
|
await el.blur();
|
|
14669
15452
|
}
|
|
14670
|
-
const MONTHS = [
|
|
14671
|
-
'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
|
|
14672
|
-
'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC',
|
|
14673
|
-
];
|
|
14674
15453
|
/**
|
|
14675
|
-
* Navigates the calendar overlay to the target month
|
|
15454
|
+
* Navigates the calendar overlay to the target month, then clicks that day.
|
|
14676
15455
|
* Assumes the calendar popup is already open.
|
|
14677
15456
|
*
|
|
14678
15457
|
* @param rootLocator The document root locator factory (for finding overlay elements).
|
|
@@ -14680,45 +15459,48 @@ const MONTHS = [
|
|
|
14680
15459
|
*/
|
|
14681
15460
|
async function selectCalendarDate(rootLocator, date) {
|
|
14682
15461
|
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}`);
|
|
15462
|
+
// Matched on `data-tn-date` rather than the cell's text: the text is formatted for the
|
|
15463
|
+
// locale, right down to the numerals, so "15" finds nothing in an Arabic calendar.
|
|
15464
|
+
const target = isoDateString(date);
|
|
15465
|
+
const cell = await rootLocator.locatorForOptional(`.tn-calendar-body-cell[data-tn-date="${target}"]:not([disabled])`)();
|
|
15466
|
+
if (!cell) {
|
|
15467
|
+
throw new Error(`Could not find an enabled calendar cell for ${target}`);
|
|
15468
|
+
}
|
|
15469
|
+
await cell.click();
|
|
14692
15470
|
}
|
|
14693
15471
|
/**
|
|
14694
|
-
*
|
|
14695
|
-
*
|
|
15472
|
+
* Pages the calendar to the month the target date falls in.
|
|
15473
|
+
*
|
|
15474
|
+
* Reads where the calendar currently sits from the days it is rendering rather than from
|
|
15475
|
+
* the header, which is written in the app's language and numerals. Every day cell in the
|
|
15476
|
+
* grid belongs to the displayed month, so the first one answers the question.
|
|
14696
15477
|
*/
|
|
14697
15478
|
async function navigateCalendarTo(rootLocator, date) {
|
|
14698
|
-
const
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
15479
|
+
const monthsFrom = (from) => {
|
|
15480
|
+
return (date.getFullYear() * 12 + date.getMonth()) - (from.year * 12 + from.month);
|
|
15481
|
+
};
|
|
15482
|
+
// A generous bound on the paging loop: a decade either way, and far more than the
|
|
15483
|
+
// handful of clicks any real navigation needs.
|
|
15484
|
+
for (let step = 0; step < 240; step++) {
|
|
15485
|
+
const shown = await displayedMonth(rootLocator);
|
|
15486
|
+
const delta = monthsFrom(shown);
|
|
15487
|
+
if (delta === 0) {
|
|
14704
15488
|
return;
|
|
14705
15489
|
}
|
|
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();
|
|
15490
|
+
const button = await rootLocator.locatorFor(delta > 0 ? '.tn-calendar-next-button' : '.tn-calendar-previous-button')();
|
|
15491
|
+
await button.click();
|
|
14714
15492
|
}
|
|
14715
|
-
throw new Error(`Could not navigate calendar to ${
|
|
15493
|
+
throw new Error(`Could not navigate the calendar to ${isoDateString(date)}`);
|
|
14716
15494
|
}
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
const
|
|
14720
|
-
const
|
|
14721
|
-
|
|
15495
|
+
/** The month on screen, read off the first day cell the grid rendered. */
|
|
15496
|
+
async function displayedMonth(rootLocator) {
|
|
15497
|
+
const cell = await rootLocator.locatorForOptional('.tn-calendar-body-cell[data-tn-date]')();
|
|
15498
|
+
const stamp = await cell?.getAttribute('data-tn-date');
|
|
15499
|
+
if (!stamp) {
|
|
15500
|
+
throw new Error('Could not read the displayed month: the calendar rendered no day cells.');
|
|
15501
|
+
}
|
|
15502
|
+
const [year, month] = stamp.split('-').map(Number);
|
|
15503
|
+
return { year, month: month - 1 };
|
|
14722
15504
|
}
|
|
14723
15505
|
|
|
14724
15506
|
/**
|
|
@@ -14850,9 +15632,7 @@ class TnDateRangeInputComponent {
|
|
|
14850
15632
|
endDayRef = viewChild.required('endDayInput');
|
|
14851
15633
|
endYearRef = viewChild.required('endYearInput');
|
|
14852
15634
|
calendarTemplate = viewChild.required('calendarTemplate');
|
|
14853
|
-
calendar = viewChild.required(TnCalendarComponent);
|
|
14854
15635
|
wrapperEl = viewChild.required('wrapper');
|
|
14855
|
-
destroy$ = new Subject();
|
|
14856
15636
|
overlayRef;
|
|
14857
15637
|
portal;
|
|
14858
15638
|
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
@@ -14867,19 +15647,14 @@ class TnDateRangeInputComponent {
|
|
|
14867
15647
|
endDay = signal('', ...(ngDevMode ? [{ debugName: "endDay" }] : []));
|
|
14868
15648
|
endYear = signal('', ...(ngDevMode ? [{ debugName: "endYear" }] : []));
|
|
14869
15649
|
currentFocus = 'start';
|
|
14870
|
-
// Always provide current range to calendar for initial display
|
|
14871
|
-
initialRange = computed(() => {
|
|
14872
|
-
return this.value();
|
|
14873
|
-
}, ...(ngDevMode ? [{ debugName: "initialRange" }] : []));
|
|
14874
15650
|
overlay = inject(Overlay);
|
|
14875
15651
|
viewContainerRef = inject(ViewContainerRef);
|
|
15652
|
+
injector = inject(Injector);
|
|
14876
15653
|
ngOnInit() {
|
|
14877
15654
|
// Initialize display values
|
|
14878
15655
|
this.updateDisplayValues();
|
|
14879
15656
|
}
|
|
14880
15657
|
ngOnDestroy() {
|
|
14881
|
-
this.destroy$.next();
|
|
14882
|
-
this.destroy$.complete();
|
|
14883
15658
|
this.close();
|
|
14884
15659
|
}
|
|
14885
15660
|
// ControlValueAccessor implementation
|
|
@@ -14947,8 +15722,9 @@ class TnDateRangeInputComponent {
|
|
|
14947
15722
|
if (this.endYearRef()?.nativeElement) {
|
|
14948
15723
|
this.endYearRef().nativeElement.value = '';
|
|
14949
15724
|
}
|
|
14950
|
-
//
|
|
14951
|
-
|
|
15725
|
+
// Move to the end date once the cleared segments have been painted. Tied to the
|
|
15726
|
+
// render rather than to a zero timeout, as the calendar's own focus handling is.
|
|
15727
|
+
afterNextRender(() => this.endMonthRef()?.nativeElement?.focus(), { injector: this.injector });
|
|
14952
15728
|
}
|
|
14953
15729
|
else if (range.start && range.end) {
|
|
14954
15730
|
// Both dates selected - close calendar
|
|
@@ -15108,11 +15884,8 @@ class TnDateRangeInputComponent {
|
|
|
15108
15884
|
}
|
|
15109
15885
|
this.createOverlay();
|
|
15110
15886
|
this.isOpen.set(true);
|
|
15111
|
-
//
|
|
15112
|
-
|
|
15113
|
-
if (cal) {
|
|
15114
|
-
setTimeout(() => cal.resetInteractionState(), 0);
|
|
15115
|
-
}
|
|
15887
|
+
// No calendar state to reset — the calendar renders whatever range this component
|
|
15888
|
+
// binds to it, so reopening already shows the current value.
|
|
15116
15889
|
}
|
|
15117
15890
|
close() {
|
|
15118
15891
|
if (this.overlayRef) {
|
|
@@ -15169,6 +15942,8 @@ class TnDateRangeInputComponent {
|
|
|
15169
15942
|
panelClass: 'tn-datepicker-overlay'
|
|
15170
15943
|
});
|
|
15171
15944
|
// Close datepicker when backdrop is clicked
|
|
15945
|
+
// No teardown to track: `dispose()` in close() ends this subscription with the
|
|
15946
|
+
// overlay, and ngOnDestroy closes.
|
|
15172
15947
|
this.overlayRef.backdropClick().subscribe(() => {
|
|
15173
15948
|
this.close();
|
|
15174
15949
|
});
|
|
@@ -15182,7 +15957,7 @@ class TnDateRangeInputComponent {
|
|
|
15182
15957
|
useExisting: forwardRef(() => TnDateRangeInputComponent),
|
|
15183
15958
|
multi: true
|
|
15184
15959
|
}
|
|
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: "
|
|
15960
|
+
], 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
15961
|
}
|
|
15187
15962
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDateRangeInputComponent, decorators: [{
|
|
15188
15963
|
type: Component,
|
|
@@ -15194,8 +15969,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
15194
15969
|
}
|
|
15195
15970
|
], host: {
|
|
15196
15971
|
'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 }] }],
|
|
15972
|
+
}, 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"] }]
|
|
15973
|
+
}], 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
15974
|
|
|
15200
15975
|
/**
|
|
15201
15976
|
* Harness for interacting with `tn-date-range-input` in tests.
|
|
@@ -18948,14 +19723,15 @@ var TnToastPosition;
|
|
|
18948
19723
|
TnToastPosition["Bottom"] = "bottom";
|
|
18949
19724
|
})(TnToastPosition || (TnToastPosition = {}));
|
|
18950
19725
|
|
|
18951
|
-
//
|
|
18952
|
-
//
|
|
18953
|
-
//
|
|
19726
|
+
// The sprite generator only sees icon names in templates and in `tnIconMarker()` calls.
|
|
19727
|
+
// These live in a map, so without the marker they'd be dropped from the sprite on the
|
|
19728
|
+
// next regeneration — and there is no `material-icons` font in this library to fall
|
|
19729
|
+
// back to, so the toast would render an empty icon.
|
|
18954
19730
|
const TOAST_ICONS = {
|
|
18955
|
-
[TnToastType.Info]: '
|
|
18956
|
-
[TnToastType.Success]: '
|
|
18957
|
-
[TnToastType.Warning]: '
|
|
18958
|
-
[TnToastType.Error]: '
|
|
19731
|
+
[TnToastType.Info]: tnIconMarker('info', 'material'),
|
|
19732
|
+
[TnToastType.Success]: tnIconMarker('check_circle', 'material'),
|
|
19733
|
+
[TnToastType.Warning]: tnIconMarker('warning', 'material'),
|
|
19734
|
+
[TnToastType.Error]: tnIconMarker('error', 'material'),
|
|
18959
19735
|
};
|
|
18960
19736
|
class TnToastComponent {
|
|
18961
19737
|
message = signal('', ...(ngDevMode ? [{ debugName: "message" }] : []));
|
|
@@ -19792,5 +20568,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
19792
20568
|
* Generated bundle index. Do not edit.
|
|
19793
20569
|
*/
|
|
19794
20570
|
|
|
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 };
|
|
20571
|
+
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
20572
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|