@neuravision/ng-construct 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, output, signal, computed, ChangeDetectionStrategy, Component,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { InjectionToken, inject, input, output, signal, computed, ChangeDetectionStrategy, Component, Injectable, forwardRef, model, booleanAttribute, viewChild, contentChildren, effect, DOCUMENT as DOCUMENT$1, isDevMode, contentChild, ElementRef, TemplateRef, Directive, viewChildren, Renderer2, numberAttribute, Pipe } from '@angular/core';
|
|
3
|
+
import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
|
|
4
|
+
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
|
|
5
5
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Injection token to provide custom i18n strings for {@link AfAlertComponent}.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* providers: [{ provide: AF_ALERT_I18N, useValue: { dismiss: 'Schliessen', dismissed: 'Warnung geschlossen' } }]
|
|
12
|
+
*/
|
|
13
|
+
const AF_ALERT_I18N = new InjectionToken('AF_ALERT_I18N', {
|
|
14
|
+
factory: () => ({ dismiss: 'Dismiss alert', dismissed: 'Alert dismissed' }),
|
|
15
|
+
});
|
|
7
16
|
/**
|
|
8
17
|
* Alert component for displaying contextual feedback messages.
|
|
9
18
|
*
|
|
@@ -11,6 +20,14 @@ import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
|
11
20
|
* - `danger` / `warning` → `role="alert"` (assertive announcement)
|
|
12
21
|
* - `info` / `success` → `role="status"` (polite announcement)
|
|
13
22
|
*
|
|
23
|
+
* ### Accessibility
|
|
24
|
+
* - The dismiss button is keyboard-accessible via Tab, activated by Enter/Space (native `<button>`)
|
|
25
|
+
* - When dismissed, a screen-reader announcement is made via an `aria-live="polite"` region
|
|
26
|
+
* - The icon slot is marked `aria-hidden="true"` to prevent redundant announcements
|
|
27
|
+
* - The `aria-label` on the dismiss button is configurable via {@link AF_ALERT_I18N} for i18n
|
|
28
|
+
* - Supports `forced-colors` (Windows High Contrast) mode
|
|
29
|
+
* - Uses CSS logical properties for RTL layout support
|
|
30
|
+
*
|
|
14
31
|
* @example
|
|
15
32
|
* <af-alert variant="warning" [dismissible]="true" (dismissed)="onDismiss()">
|
|
16
33
|
* <span icon>⚠</span>
|
|
@@ -22,14 +39,17 @@ import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
|
22
39
|
* </af-alert>
|
|
23
40
|
*/
|
|
24
41
|
class AfAlertComponent {
|
|
25
|
-
|
|
42
|
+
i18n = inject(AF_ALERT_I18N);
|
|
43
|
+
/** Color variant determining the alert's visual style and ARIA role. */
|
|
26
44
|
variant = input('info', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
27
|
-
/** Whether the alert can be dismissed by the user */
|
|
45
|
+
/** Whether the alert can be dismissed by the user. */
|
|
28
46
|
dismissible = input(false, ...(ngDevMode ? [{ debugName: "dismissible" }] : []));
|
|
29
|
-
/** Emits when the user dismisses the alert */
|
|
47
|
+
/** Emits when the user dismisses the alert. */
|
|
30
48
|
dismissed = output();
|
|
31
|
-
/** Controls alert visibility */
|
|
49
|
+
/** Controls alert visibility. */
|
|
32
50
|
visible = signal(true, ...(ngDevMode ? [{ debugName: "visible" }] : []));
|
|
51
|
+
/** @internal Screen-reader announcement text. */
|
|
52
|
+
liveAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "liveAnnouncement" }] : []));
|
|
33
53
|
alertClasses = computed(() => {
|
|
34
54
|
const classes = ['ct-alert'];
|
|
35
55
|
if (this.dismissible()) {
|
|
@@ -46,10 +66,12 @@ class AfAlertComponent {
|
|
|
46
66
|
const v = this.variant();
|
|
47
67
|
return v === 'danger' || v === 'warning' ? 'alert' : 'status';
|
|
48
68
|
}, ...(ngDevMode ? [{ debugName: "alertRole" }] : []));
|
|
49
|
-
/** Hides the alert and emits the dismissed event */
|
|
69
|
+
/** Hides the alert and emits the dismissed event. */
|
|
50
70
|
dismiss() {
|
|
51
71
|
this.visible.set(false);
|
|
52
72
|
this.dismissed.emit();
|
|
73
|
+
this.liveAnnouncement.set('');
|
|
74
|
+
setTimeout(() => this.liveAnnouncement.set(this.i18n.dismissed));
|
|
53
75
|
}
|
|
54
76
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfAlertComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
55
77
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfAlertComponent, isStandalone: true, selector: "af-alert", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismissed: "dismissed" }, ngImport: i0, template: `
|
|
@@ -76,14 +98,17 @@ class AfAlertComponent {
|
|
|
76
98
|
<button
|
|
77
99
|
type="button"
|
|
78
100
|
class="af-alert__dismiss"
|
|
79
|
-
aria-label="
|
|
101
|
+
[attr.aria-label]="i18n.dismiss"
|
|
80
102
|
(click)="dismiss()">
|
|
81
103
|
<span class="ct-icon ct-icon--sm" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></span>
|
|
82
104
|
</button>
|
|
83
105
|
}
|
|
84
106
|
</div>
|
|
85
107
|
}
|
|
86
|
-
|
|
108
|
+
<span class="af-alert__sr-only" aria-live="polite" aria-atomic="true">
|
|
109
|
+
{{ liveAnnouncement() }}
|
|
110
|
+
</span>
|
|
111
|
+
`, isInline: true, styles: [":host{display:contents}.ct-alert--dismissible{position:relative;padding-inline-end:2.5rem}.af-alert__dismiss{position:absolute;inset-block-start:.625rem;inset-inline-end:.625rem;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;min-width:24px;min-height:24px;padding:0;border:none;background:none;border-radius:var(--radius-sm, .25rem);color:var(--color-text-secondary);font-size:1.25rem;line-height:1;cursor:pointer}.af-alert__dismiss:hover{color:var(--color-text-primary)}.af-alert__dismiss:focus-visible{outline:2px solid var(--ct-alert-accent, var(--color-brand-primary));outline-offset:2px}@media(forced-colors:active){.af-alert__dismiss{border:1px solid ButtonText}.af-alert__dismiss:focus-visible{outline-color:Highlight}}.af-alert__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
87
112
|
}
|
|
88
113
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfAlertComponent, decorators: [{
|
|
89
114
|
type: Component,
|
|
@@ -111,16 +136,183 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
111
136
|
<button
|
|
112
137
|
type="button"
|
|
113
138
|
class="af-alert__dismiss"
|
|
114
|
-
aria-label="
|
|
139
|
+
[attr.aria-label]="i18n.dismiss"
|
|
115
140
|
(click)="dismiss()">
|
|
116
141
|
<span class="ct-icon ct-icon--sm" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></span>
|
|
117
142
|
</button>
|
|
118
143
|
}
|
|
119
144
|
</div>
|
|
120
145
|
}
|
|
121
|
-
|
|
146
|
+
<span class="af-alert__sr-only" aria-live="polite" aria-atomic="true">
|
|
147
|
+
{{ liveAnnouncement() }}
|
|
148
|
+
</span>
|
|
149
|
+
`, styles: [":host{display:contents}.ct-alert--dismissible{position:relative;padding-inline-end:2.5rem}.af-alert__dismiss{position:absolute;inset-block-start:.625rem;inset-inline-end:.625rem;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;min-width:24px;min-height:24px;padding:0;border:none;background:none;border-radius:var(--radius-sm, .25rem);color:var(--color-text-secondary);font-size:1.25rem;line-height:1;cursor:pointer}.af-alert__dismiss:hover{color:var(--color-text-primary)}.af-alert__dismiss:focus-visible{outline:2px solid var(--ct-alert-accent, var(--color-brand-primary));outline-offset:2px}@media(forced-colors:active){.af-alert__dismiss{border:1px solid ButtonText}.af-alert__dismiss:focus-visible{outline-color:Highlight}}.af-alert__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
122
150
|
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
|
|
123
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Test harness for AfAlertComponent.
|
|
154
|
+
*
|
|
155
|
+
* Provides a semantic API for interacting with the alert in tests,
|
|
156
|
+
* abstracting DOM details behind readable method names.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* const harness = new AfAlertHarness(fixture.nativeElement);
|
|
160
|
+
* expect(harness.getVariant()).toBe('info');
|
|
161
|
+
* expect(harness.getRole()).toBe('status');
|
|
162
|
+
* harness.dismiss();
|
|
163
|
+
*/
|
|
164
|
+
class AfAlertHarness {
|
|
165
|
+
hostEl;
|
|
166
|
+
constructor(container) {
|
|
167
|
+
const el = container.querySelector('af-alert');
|
|
168
|
+
if (!el) {
|
|
169
|
+
throw new Error('AfAlertHarness: af-alert element not found in container.');
|
|
170
|
+
}
|
|
171
|
+
this.hostEl = el;
|
|
172
|
+
}
|
|
173
|
+
/** Returns the inner `.ct-alert` wrapper element, or `null` if the alert is not visible. */
|
|
174
|
+
getAlertElement() {
|
|
175
|
+
return this.hostEl.querySelector('.ct-alert');
|
|
176
|
+
}
|
|
177
|
+
/** Returns the `data-variant` attribute value. Throws if the alert is not visible. */
|
|
178
|
+
getVariant() {
|
|
179
|
+
const el = this.requireVisible();
|
|
180
|
+
return el.getAttribute('data-variant') ?? '';
|
|
181
|
+
}
|
|
182
|
+
/** Returns the ARIA `role` attribute value. Throws if the alert is not visible. */
|
|
183
|
+
getRole() {
|
|
184
|
+
const el = this.requireVisible();
|
|
185
|
+
return el.getAttribute('role') ?? '';
|
|
186
|
+
}
|
|
187
|
+
/** Returns the full trimmed text content of the alert. Throws if not visible. */
|
|
188
|
+
getText() {
|
|
189
|
+
const el = this.requireVisible();
|
|
190
|
+
return el.textContent?.trim() ?? '';
|
|
191
|
+
}
|
|
192
|
+
/** Returns the trimmed text content of the title slot. Throws if not visible. */
|
|
193
|
+
getTitle() {
|
|
194
|
+
const el = this.requireVisible();
|
|
195
|
+
const title = el.querySelector('.ct-alert__title');
|
|
196
|
+
return title?.textContent?.trim() ?? '';
|
|
197
|
+
}
|
|
198
|
+
/** Returns whether the alert is currently visible in the DOM. */
|
|
199
|
+
isVisible() {
|
|
200
|
+
return this.getAlertElement() !== null;
|
|
201
|
+
}
|
|
202
|
+
/** Returns whether the alert has a dismiss button. Throws if not visible. */
|
|
203
|
+
isDismissible() {
|
|
204
|
+
const el = this.requireVisible();
|
|
205
|
+
return el.querySelector('.af-alert__dismiss') !== null;
|
|
206
|
+
}
|
|
207
|
+
/** Clicks the dismiss button. Throws if the alert is not visible or not dismissible. */
|
|
208
|
+
dismiss() {
|
|
209
|
+
const btn = this.getDismissButton();
|
|
210
|
+
if (!btn) {
|
|
211
|
+
throw new Error('AfAlertHarness: dismiss button not found. Is the alert dismissible?');
|
|
212
|
+
}
|
|
213
|
+
btn.click();
|
|
214
|
+
}
|
|
215
|
+
/** Returns the dismiss button element, or `null` if not present. */
|
|
216
|
+
getDismissButton() {
|
|
217
|
+
return (this.getAlertElement()?.querySelector('.af-alert__dismiss') ?? null);
|
|
218
|
+
}
|
|
219
|
+
/** Returns the `aria-live` region element for screen-reader announcements. */
|
|
220
|
+
getLiveRegion() {
|
|
221
|
+
return this.hostEl.querySelector('[aria-live="polite"]');
|
|
222
|
+
}
|
|
223
|
+
requireVisible() {
|
|
224
|
+
const el = this.getAlertElement();
|
|
225
|
+
if (!el) {
|
|
226
|
+
throw new Error('AfAlertHarness: alert is not visible.');
|
|
227
|
+
}
|
|
228
|
+
return el;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Lightweight service that announces messages to screen readers
|
|
234
|
+
* via an `aria-live` region. No `@angular/cdk` dependency required.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* private announcer = inject(AriaLiveAnnouncer);
|
|
238
|
+
* this.announcer.announce('Section expanded');
|
|
239
|
+
*/
|
|
240
|
+
class AriaLiveAnnouncer {
|
|
241
|
+
document = inject(DOCUMENT);
|
|
242
|
+
liveEl = null;
|
|
243
|
+
clearTimer = null;
|
|
244
|
+
/** Announce a message to screen readers. */
|
|
245
|
+
announce(message, politeness = 'polite') {
|
|
246
|
+
const el = this.getOrCreateLiveElement();
|
|
247
|
+
el.setAttribute('aria-live', politeness);
|
|
248
|
+
// Clear then re-set so assistive tech registers the change.
|
|
249
|
+
el.textContent = '';
|
|
250
|
+
if (this.clearTimer) {
|
|
251
|
+
clearTimeout(this.clearTimer);
|
|
252
|
+
}
|
|
253
|
+
// Small delay so the DOM mutation is picked up as a new announcement.
|
|
254
|
+
setTimeout(() => {
|
|
255
|
+
el.textContent = message;
|
|
256
|
+
}, 50);
|
|
257
|
+
// Auto-clear after 3 seconds to avoid stale text.
|
|
258
|
+
this.clearTimer = setTimeout(() => {
|
|
259
|
+
el.textContent = '';
|
|
260
|
+
}, 3000);
|
|
261
|
+
}
|
|
262
|
+
ngOnDestroy() {
|
|
263
|
+
if (this.clearTimer) {
|
|
264
|
+
clearTimeout(this.clearTimer);
|
|
265
|
+
}
|
|
266
|
+
this.liveEl?.remove();
|
|
267
|
+
this.liveEl = null;
|
|
268
|
+
}
|
|
269
|
+
getOrCreateLiveElement() {
|
|
270
|
+
if (this.liveEl)
|
|
271
|
+
return this.liveEl;
|
|
272
|
+
const el = this.document.createElement('div');
|
|
273
|
+
el.setAttribute('aria-live', 'polite');
|
|
274
|
+
el.setAttribute('aria-atomic', 'true');
|
|
275
|
+
el.classList.add('cdk-visually-hidden');
|
|
276
|
+
// Visually hidden but accessible to screen readers.
|
|
277
|
+
Object.assign(el.style, {
|
|
278
|
+
position: 'absolute',
|
|
279
|
+
width: '1px',
|
|
280
|
+
height: '1px',
|
|
281
|
+
padding: '0',
|
|
282
|
+
margin: '-1px',
|
|
283
|
+
overflow: 'hidden',
|
|
284
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
285
|
+
whiteSpace: 'nowrap',
|
|
286
|
+
border: '0',
|
|
287
|
+
});
|
|
288
|
+
this.document.body.appendChild(el);
|
|
289
|
+
this.liveEl = el;
|
|
290
|
+
return el;
|
|
291
|
+
}
|
|
292
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AriaLiveAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
293
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AriaLiveAnnouncer, providedIn: 'root' });
|
|
294
|
+
}
|
|
295
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AriaLiveAnnouncer, decorators: [{
|
|
296
|
+
type: Injectable,
|
|
297
|
+
args: [{ providedIn: 'root' }]
|
|
298
|
+
}] });
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Injection token to override accordion screen-reader announcements.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* providers: [{
|
|
305
|
+
* provide: AF_ACCORDION_I18N,
|
|
306
|
+
* useValue: { expanded: '{heading} ausgeklappt', collapsed: '{heading} eingeklappt' },
|
|
307
|
+
* }]
|
|
308
|
+
*/
|
|
309
|
+
const AF_ACCORDION_I18N = new InjectionToken('AfAccordionI18n', {
|
|
310
|
+
factory: () => ({
|
|
311
|
+
expanded: '{heading} expanded',
|
|
312
|
+
collapsed: '{heading} collapsed',
|
|
313
|
+
}),
|
|
314
|
+
});
|
|
315
|
+
|
|
124
316
|
let nextId$1 = 0;
|
|
125
317
|
/**
|
|
126
318
|
* Individual accordion item used within af-accordion.
|
|
@@ -133,6 +325,8 @@ let nextId$1 = 0;
|
|
|
133
325
|
class AfAccordionItemComponent {
|
|
134
326
|
itemId = nextId$1++;
|
|
135
327
|
accordion = inject(forwardRef(() => AfAccordionComponent), { optional: true });
|
|
328
|
+
announcer = inject(AriaLiveAnnouncer);
|
|
329
|
+
i18n = inject(AF_ACCORDION_I18N);
|
|
136
330
|
/** Heading text displayed in the accordion trigger. */
|
|
137
331
|
heading = input.required(...(ngDevMode ? [{ debugName: "heading" }] : []));
|
|
138
332
|
/** Whether this item is expanded (supports two-way binding). */
|
|
@@ -165,6 +359,8 @@ class AfAccordionItemComponent {
|
|
|
165
359
|
if (willExpand) {
|
|
166
360
|
this.accordion?.onItemExpanded(this);
|
|
167
361
|
}
|
|
362
|
+
const template = willExpand ? this.i18n.expanded : this.i18n.collapsed;
|
|
363
|
+
this.announcer.announce(template.replace('{heading}', this.heading()));
|
|
168
364
|
}
|
|
169
365
|
/** Programmatically collapse this item. */
|
|
170
366
|
collapse() {
|
|
@@ -354,6 +550,96 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
354
550
|
`, styles: [":host{display:block}\n"] }]
|
|
355
551
|
}], propDecorators: { multi: [{ type: i0.Input, args: [{ isSignal: true, alias: "multi", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => AfAccordionItemComponent), { isSignal: true }] }] } });
|
|
356
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Test harness for AfAccordionItemComponent.
|
|
555
|
+
*
|
|
556
|
+
* Provides a semantic API for interacting with a single accordion item,
|
|
557
|
+
* abstracting DOM details behind readable method names.
|
|
558
|
+
*/
|
|
559
|
+
class AfAccordionItemHarness {
|
|
560
|
+
hostEl;
|
|
561
|
+
constructor(hostEl) {
|
|
562
|
+
this.hostEl = hostEl;
|
|
563
|
+
}
|
|
564
|
+
/** Returns the heading text. */
|
|
565
|
+
getHeading() {
|
|
566
|
+
const heading = this.hostEl.querySelector('.ct-accordion__heading');
|
|
567
|
+
return heading?.textContent?.trim() ?? '';
|
|
568
|
+
}
|
|
569
|
+
/** Returns whether the item is expanded. */
|
|
570
|
+
isExpanded() {
|
|
571
|
+
return this.getTriggerElement().getAttribute('aria-expanded') === 'true';
|
|
572
|
+
}
|
|
573
|
+
/** Returns whether the item is disabled. */
|
|
574
|
+
isDisabled() {
|
|
575
|
+
return this.getTriggerElement().getAttribute('aria-disabled') === 'true';
|
|
576
|
+
}
|
|
577
|
+
/** Clicks the trigger to toggle the item. */
|
|
578
|
+
toggle() {
|
|
579
|
+
this.getTriggerElement().click();
|
|
580
|
+
}
|
|
581
|
+
/** Returns the trigger (summary) element. */
|
|
582
|
+
getTriggerElement() {
|
|
583
|
+
const summary = this.hostEl.querySelector('summary');
|
|
584
|
+
if (!summary) {
|
|
585
|
+
throw new Error('AfAccordionItemHarness: <summary> element not found.');
|
|
586
|
+
}
|
|
587
|
+
return summary;
|
|
588
|
+
}
|
|
589
|
+
/** Returns the content panel element. */
|
|
590
|
+
getContentElement() {
|
|
591
|
+
const panel = this.hostEl.querySelector('[role="region"]');
|
|
592
|
+
if (!panel) {
|
|
593
|
+
throw new Error('AfAccordionItemHarness: content region not found.');
|
|
594
|
+
}
|
|
595
|
+
return panel;
|
|
596
|
+
}
|
|
597
|
+
/** Returns the `aria-controls` value of the trigger. */
|
|
598
|
+
getAriaControls() {
|
|
599
|
+
return this.getTriggerElement().getAttribute('aria-controls');
|
|
600
|
+
}
|
|
601
|
+
/** Returns the `aria-expanded` value of the trigger. */
|
|
602
|
+
getAriaExpanded() {
|
|
603
|
+
return this.getTriggerElement().getAttribute('aria-expanded');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Test harness for AfAccordionComponent.
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* const harness = new AfAccordionHarness(fixture.nativeElement);
|
|
611
|
+
* expect(harness.getItems()).toHaveLength(3);
|
|
612
|
+
* harness.getItem(0).toggle();
|
|
613
|
+
*/
|
|
614
|
+
class AfAccordionHarness {
|
|
615
|
+
hostEl;
|
|
616
|
+
constructor(container) {
|
|
617
|
+
const el = container.querySelector('af-accordion');
|
|
618
|
+
if (!el) {
|
|
619
|
+
throw new Error('AfAccordionHarness: af-accordion element not found in container.');
|
|
620
|
+
}
|
|
621
|
+
this.hostEl = el;
|
|
622
|
+
}
|
|
623
|
+
/** Returns harnesses for all accordion items. */
|
|
624
|
+
getItems() {
|
|
625
|
+
const items = Array.from(this.hostEl.querySelectorAll('af-accordion-item'));
|
|
626
|
+
return items.map((el) => new AfAccordionItemHarness(el));
|
|
627
|
+
}
|
|
628
|
+
/** Returns the harness for the accordion item at the given index. */
|
|
629
|
+
getItem(index) {
|
|
630
|
+
const items = this.getItems();
|
|
631
|
+
if (index < 0 || index >= items.length) {
|
|
632
|
+
throw new Error(`AfAccordionHarness: index ${index} out of range (${items.length} items).`);
|
|
633
|
+
}
|
|
634
|
+
return items[index];
|
|
635
|
+
}
|
|
636
|
+
/** Returns whether the bordered variant is applied. */
|
|
637
|
+
isBordered() {
|
|
638
|
+
const wrapper = this.hostEl.querySelector('.ct-accordion');
|
|
639
|
+
return wrapper?.classList.contains('ct-accordion--bordered') ?? false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
357
643
|
/**
|
|
358
644
|
* Skip-link component for keyboard-only navigation bypass.
|
|
359
645
|
*
|
|
@@ -373,7 +659,7 @@ class AfSkipLinkComponent {
|
|
|
373
659
|
target = input.required(...(ngDevMode ? [{ debugName: "target" }] : []));
|
|
374
660
|
/** Visible label text shown when the link receives focus. */
|
|
375
661
|
label = input('Skip to main content', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
376
|
-
document = inject(DOCUMENT);
|
|
662
|
+
document = inject(DOCUMENT$1);
|
|
377
663
|
/**
|
|
378
664
|
* Moves focus to the target element so keyboard navigation
|
|
379
665
|
* continues from there instead of the top of the page.
|
|
@@ -909,15 +1195,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
909
1195
|
}], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], alt: [{ type: i0.Input, args: [{ isSignal: true, alias: "alt", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }] } });
|
|
910
1196
|
|
|
911
1197
|
/**
|
|
912
|
-
* Button component from the Construct Design System
|
|
1198
|
+
* Button component from the Construct Design System.
|
|
913
1199
|
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
1200
|
+
* Wraps a native `<button>` element with design system tokens and
|
|
1201
|
+
* variant/size modifiers.
|
|
1202
|
+
*
|
|
1203
|
+
* @example Basic usage
|
|
1204
|
+
* <af-button variant="primary" (clicked)="save()">Save</af-button>
|
|
916
1205
|
*
|
|
917
|
-
* @example
|
|
1206
|
+
* @example Variants
|
|
1207
|
+
* <af-button variant="secondary">Secondary</af-button>
|
|
1208
|
+
* <af-button variant="ghost">Ghost</af-button>
|
|
1209
|
+
* <af-button variant="outline">Outline</af-button>
|
|
1210
|
+
* <af-button variant="danger">Danger</af-button>
|
|
1211
|
+
* <af-button variant="accent">Accent</af-button>
|
|
1212
|
+
* <af-button variant="link">Link</af-button>
|
|
1213
|
+
*
|
|
1214
|
+
* @example Icon-only button (ariaLabel required)
|
|
918
1215
|
* <af-button variant="ghost" size="sm" iconOnly ariaLabel="Delete item">
|
|
919
1216
|
* <af-icon name="delete" />
|
|
920
1217
|
* </af-button>
|
|
1218
|
+
*
|
|
1219
|
+
* @example Disabled button
|
|
1220
|
+
* <af-button [disabled]="true">Cannot click</af-button>
|
|
1221
|
+
*
|
|
1222
|
+
* @accessibility
|
|
1223
|
+
* - Uses native `<button>` element — keyboard (Enter/Space) and screen reader support built-in.
|
|
1224
|
+
* - Disabled state uses the native `disabled` attribute.
|
|
1225
|
+
* - Icon-only buttons must provide `ariaLabel` for screen reader users.
|
|
1226
|
+
* A dev-mode warning is emitted if `ariaLabel` is missing on an icon-only button.
|
|
1227
|
+
* - Focus indicator: 2px outline via `:focus-visible` (design system CSS).
|
|
1228
|
+
* - Reduced motion: `transform` animation disabled via `prefers-reduced-motion`.
|
|
921
1229
|
*/
|
|
922
1230
|
class AfButtonComponent {
|
|
923
1231
|
/** Button variant/style */
|
|
@@ -949,6 +1257,11 @@ class AfButtonComponent {
|
|
|
949
1257
|
}
|
|
950
1258
|
return classes.join(' ');
|
|
951
1259
|
}, ...(ngDevMode ? [{ debugName: "buttonClasses" }] : []));
|
|
1260
|
+
iconOnlyWarning = effect(() => {
|
|
1261
|
+
if (isDevMode() && this.iconOnly() && !this.ariaLabel()) {
|
|
1262
|
+
console.warn('af-button: iconOnly buttons require an ariaLabel for screen readers.');
|
|
1263
|
+
}
|
|
1264
|
+
}, ...(ngDevMode ? [{ debugName: "iconOnlyWarning" }] : []));
|
|
952
1265
|
handleClick(event) {
|
|
953
1266
|
if (!this.disabled()) {
|
|
954
1267
|
this.clicked.emit(event);
|
|
@@ -983,7 +1296,88 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
983
1296
|
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], clicked: [{ type: i0.Output, args: ["clicked"] }] } });
|
|
984
1297
|
|
|
985
1298
|
/**
|
|
986
|
-
*
|
|
1299
|
+
* Test harness for AfButtonComponent.
|
|
1300
|
+
*
|
|
1301
|
+
* Provides a semantic API for interacting with the button in tests,
|
|
1302
|
+
* abstracting DOM details behind readable method names.
|
|
1303
|
+
*
|
|
1304
|
+
* @example
|
|
1305
|
+
* const harness = new AfButtonHarness(fixture.nativeElement);
|
|
1306
|
+
* expect(harness.getText()).toBe('Save');
|
|
1307
|
+
* expect(harness.isDisabled()).toBe(false);
|
|
1308
|
+
* harness.click();
|
|
1309
|
+
*/
|
|
1310
|
+
class AfButtonHarness {
|
|
1311
|
+
hostEl;
|
|
1312
|
+
constructor(container) {
|
|
1313
|
+
const el = container.querySelector('af-button');
|
|
1314
|
+
if (!el) {
|
|
1315
|
+
throw new Error('AfButtonHarness: af-button element not found in container.');
|
|
1316
|
+
}
|
|
1317
|
+
this.hostEl = el;
|
|
1318
|
+
}
|
|
1319
|
+
/** Returns the inner native `<button>` element. */
|
|
1320
|
+
getButtonElement() {
|
|
1321
|
+
const btn = this.hostEl.querySelector('button');
|
|
1322
|
+
if (!btn) {
|
|
1323
|
+
throw new Error('AfButtonHarness: inner <button> element not found.');
|
|
1324
|
+
}
|
|
1325
|
+
return btn;
|
|
1326
|
+
}
|
|
1327
|
+
/** Returns the trimmed text content of the button. */
|
|
1328
|
+
getText() {
|
|
1329
|
+
return this.getButtonElement().textContent?.trim() ?? '';
|
|
1330
|
+
}
|
|
1331
|
+
/** Returns whether the button is disabled. */
|
|
1332
|
+
isDisabled() {
|
|
1333
|
+
return this.getButtonElement().disabled;
|
|
1334
|
+
}
|
|
1335
|
+
/** Clicks the inner button element. */
|
|
1336
|
+
click() {
|
|
1337
|
+
this.getButtonElement().click();
|
|
1338
|
+
}
|
|
1339
|
+
/** Returns the `aria-label` attribute value, or `null` if absent. */
|
|
1340
|
+
getAriaLabel() {
|
|
1341
|
+
return this.getButtonElement().getAttribute('aria-label');
|
|
1342
|
+
}
|
|
1343
|
+
/** Returns the `title` attribute value, or `null` if absent. */
|
|
1344
|
+
getTitle() {
|
|
1345
|
+
return this.getButtonElement().getAttribute('title');
|
|
1346
|
+
}
|
|
1347
|
+
/** Returns the `type` attribute of the button. */
|
|
1348
|
+
getType() {
|
|
1349
|
+
return this.getButtonElement().type;
|
|
1350
|
+
}
|
|
1351
|
+
/** Returns the full `class` attribute string of the inner button. */
|
|
1352
|
+
getClasses() {
|
|
1353
|
+
return this.getButtonElement().className;
|
|
1354
|
+
}
|
|
1355
|
+
/** Returns whether the inner button has the given CSS class. */
|
|
1356
|
+
hasClass(className) {
|
|
1357
|
+
return this.getButtonElement().classList.contains(className);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Injection token to override input screen-reader announcements.
|
|
1363
|
+
*
|
|
1364
|
+
* @example
|
|
1365
|
+
* providers: [{
|
|
1366
|
+
* provide: AF_INPUT_I18N,
|
|
1367
|
+
* useValue: { required: 'Pflichtfeld' },
|
|
1368
|
+
* }]
|
|
1369
|
+
*/
|
|
1370
|
+
const AF_INPUT_I18N = new InjectionToken('AfInputI18n', {
|
|
1371
|
+
factory: () => ({
|
|
1372
|
+
required: 'required',
|
|
1373
|
+
}),
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Input field component with form control support.
|
|
1378
|
+
*
|
|
1379
|
+
* Wraps a native `<input>` element with label, hint, error, and icon slots.
|
|
1380
|
+
* Implements `ControlValueAccessor` for seamless `ngModel` and `formControl` integration.
|
|
987
1381
|
*
|
|
988
1382
|
* @example
|
|
989
1383
|
* <af-input
|
|
@@ -992,40 +1386,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
992
1386
|
* placeholder="name@company.com"
|
|
993
1387
|
* [(ngModel)]="email"
|
|
994
1388
|
* hint="We will not share this."
|
|
995
|
-
*
|
|
1389
|
+
* />
|
|
996
1390
|
*
|
|
997
1391
|
* @example
|
|
998
1392
|
* <af-input
|
|
999
1393
|
* label="Name"
|
|
1000
1394
|
* [error]="nameError"
|
|
1001
1395
|
* required
|
|
1002
|
-
*
|
|
1396
|
+
* />
|
|
1003
1397
|
*/
|
|
1004
1398
|
class AfInputComponent {
|
|
1005
1399
|
static nextId = 0;
|
|
1006
|
-
|
|
1400
|
+
i18n = inject(AF_INPUT_I18N);
|
|
1401
|
+
/** Input label. */
|
|
1007
1402
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
1008
|
-
/** Input type */
|
|
1403
|
+
/** Input type. */
|
|
1009
1404
|
type = input('text', ...(ngDevMode ? [{ debugName: "type" }] : []));
|
|
1010
|
-
/** Placeholder text */
|
|
1405
|
+
/** Placeholder text. */
|
|
1011
1406
|
placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
1012
|
-
/** Hint text shown below input */
|
|
1407
|
+
/** Hint text shown below input. */
|
|
1013
1408
|
hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
|
|
1014
|
-
/** Error message
|
|
1409
|
+
/** Error message — shows error state and message. */
|
|
1015
1410
|
error = input('', ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
1016
|
-
/** Whether input is required */
|
|
1017
|
-
required = input(false, ...(ngDevMode ?
|
|
1018
|
-
/** Whether input is disabled */
|
|
1411
|
+
/** Whether input is required. */
|
|
1412
|
+
required = input(false, { ...(ngDevMode ? { debugName: "required" } : {}), transform: booleanAttribute });
|
|
1413
|
+
/** Whether input is disabled. */
|
|
1019
1414
|
disabled = model(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
1020
|
-
/** Icon position (if icon content is projected) */
|
|
1415
|
+
/** Icon position (if icon content is projected). */
|
|
1021
1416
|
iconPosition = input(null, ...(ngDevMode ? [{ debugName: "iconPosition" }] : []));
|
|
1022
|
-
/** Unique input ID */
|
|
1417
|
+
/** Unique input ID. */
|
|
1023
1418
|
inputId = input(`af-input-${AfInputComponent.nextId++}`, ...(ngDevMode ? [{ debugName: "inputId" }] : []));
|
|
1024
|
-
value
|
|
1419
|
+
/** @docs-private — internal form value managed by CVA. */
|
|
1420
|
+
value = signal('', ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1025
1421
|
onChange = () => { };
|
|
1026
1422
|
onTouched = () => { };
|
|
1423
|
+
/** Computed hint element ID. */
|
|
1027
1424
|
hintId = computed(() => `${this.inputId()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
|
|
1425
|
+
/** Computed error element ID. */
|
|
1028
1426
|
errorId = computed(() => `${this.inputId()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
|
|
1427
|
+
/** Computed CSS classes for the inner input. */
|
|
1029
1428
|
inputClasses = computed(() => {
|
|
1030
1429
|
const classes = ['ct-input'];
|
|
1031
1430
|
if (this.iconPosition()) {
|
|
@@ -1033,45 +1432,49 @@ class AfInputComponent {
|
|
|
1033
1432
|
}
|
|
1034
1433
|
return classes.join(' ');
|
|
1035
1434
|
}, ...(ngDevMode ? [{ debugName: "inputClasses" }] : []));
|
|
1036
|
-
|
|
1435
|
+
/** Computed `aria-describedby` value linking to hint or error. */
|
|
1436
|
+
ariaDescribedBy = computed(() => {
|
|
1037
1437
|
if (this.error())
|
|
1038
1438
|
return this.errorId();
|
|
1039
1439
|
if (this.hint())
|
|
1040
1440
|
return this.hintId();
|
|
1041
1441
|
return null;
|
|
1042
|
-
}
|
|
1442
|
+
}, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
|
|
1043
1443
|
onInput(event) {
|
|
1044
1444
|
const target = event.target;
|
|
1045
|
-
this.value
|
|
1046
|
-
this.onChange(this.value);
|
|
1445
|
+
this.value.set(target.value);
|
|
1446
|
+
this.onChange(this.value());
|
|
1047
1447
|
}
|
|
1048
|
-
/**
|
|
1448
|
+
/** @docs-private */
|
|
1049
1449
|
writeValue(value) {
|
|
1050
|
-
this.value
|
|
1450
|
+
this.value.set(value || '');
|
|
1051
1451
|
}
|
|
1452
|
+
/** @docs-private */
|
|
1052
1453
|
registerOnChange(fn) {
|
|
1053
1454
|
this.onChange = fn;
|
|
1054
1455
|
}
|
|
1456
|
+
/** @docs-private */
|
|
1055
1457
|
registerOnTouched(fn) {
|
|
1056
1458
|
this.onTouched = fn;
|
|
1057
1459
|
}
|
|
1460
|
+
/** @docs-private */
|
|
1058
1461
|
setDisabledState(isDisabled) {
|
|
1059
1462
|
this.disabled.set(isDisabled);
|
|
1060
1463
|
}
|
|
1061
1464
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1062
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfInputComponent, isStandalone: true, selector: "af-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, iconPosition: { classPropertyName: "iconPosition", publicName: "iconPosition", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange" }, providers: [
|
|
1465
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfInputComponent, isStandalone: true, selector: "af-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, iconPosition: { classPropertyName: "iconPosition", publicName: "iconPosition", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange" }, host: { styleAttribute: "display: block" }, providers: [
|
|
1063
1466
|
{
|
|
1064
1467
|
provide: NG_VALUE_ACCESSOR,
|
|
1065
1468
|
useExisting: forwardRef(() => AfInputComponent),
|
|
1066
|
-
multi: true
|
|
1067
|
-
}
|
|
1469
|
+
multi: true,
|
|
1470
|
+
},
|
|
1068
1471
|
], ngImport: i0, template: `
|
|
1069
1472
|
<div class="ct-field" [class.ct-field--error]="error()">
|
|
1070
1473
|
@if (label()) {
|
|
1071
1474
|
<label class="ct-field__label" [attr.for]="inputId()">
|
|
1072
1475
|
{{ label() }}
|
|
1073
1476
|
@if (required()) {
|
|
1074
|
-
<span aria-label="required"> *</span>
|
|
1477
|
+
<span [attr.aria-label]="i18n.required"> *</span>
|
|
1075
1478
|
}
|
|
1076
1479
|
</label>
|
|
1077
1480
|
}
|
|
@@ -1080,7 +1483,7 @@ class AfInputComponent {
|
|
|
1080
1483
|
<div class="ct-input-wrap">
|
|
1081
1484
|
@if (iconPosition() === 'left') {
|
|
1082
1485
|
<span class="ct-input__icon" aria-hidden="true">
|
|
1083
|
-
<ng-content select="[icon]"
|
|
1486
|
+
<ng-content select="[icon]" />
|
|
1084
1487
|
</span>
|
|
1085
1488
|
}
|
|
1086
1489
|
<input
|
|
@@ -1090,21 +1493,19 @@ class AfInputComponent {
|
|
|
1090
1493
|
[disabled]="disabled()"
|
|
1091
1494
|
[required]="required()"
|
|
1092
1495
|
[attr.aria-invalid]="error() ? true : null"
|
|
1093
|
-
[attr.aria-describedby]="
|
|
1496
|
+
[attr.aria-describedby]="ariaDescribedBy()"
|
|
1094
1497
|
[class]="inputClasses()"
|
|
1095
|
-
[value]="value"
|
|
1498
|
+
[value]="value()"
|
|
1096
1499
|
(input)="onInput($event)"
|
|
1097
1500
|
(blur)="onTouched()"
|
|
1098
1501
|
/>
|
|
1099
1502
|
@if (iconPosition() === 'right') {
|
|
1100
1503
|
<span class="ct-input__icon" aria-hidden="true">
|
|
1101
|
-
<ng-content select="[icon]"
|
|
1504
|
+
<ng-content select="[icon]" />
|
|
1102
1505
|
</span>
|
|
1103
1506
|
}
|
|
1104
1507
|
</div>
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
@if (!iconPosition()) {
|
|
1508
|
+
} @else {
|
|
1108
1509
|
<input
|
|
1109
1510
|
[id]="inputId()"
|
|
1110
1511
|
[type]="type()"
|
|
@@ -1112,9 +1513,9 @@ class AfInputComponent {
|
|
|
1112
1513
|
[disabled]="disabled()"
|
|
1113
1514
|
[required]="required()"
|
|
1114
1515
|
[attr.aria-invalid]="error() ? true : null"
|
|
1115
|
-
[attr.aria-describedby]="
|
|
1116
|
-
class="
|
|
1117
|
-
[value]="value"
|
|
1516
|
+
[attr.aria-describedby]="ariaDescribedBy()"
|
|
1517
|
+
[class]="inputClasses()"
|
|
1518
|
+
[value]="value()"
|
|
1118
1519
|
(input)="onInput($event)"
|
|
1119
1520
|
(blur)="onTouched()"
|
|
1120
1521
|
/>
|
|
@@ -1127,28 +1528,35 @@ class AfInputComponent {
|
|
|
1127
1528
|
}
|
|
1128
1529
|
|
|
1129
1530
|
@if (error()) {
|
|
1130
|
-
<div class="ct-field__error" [id]="errorId()">
|
|
1531
|
+
<div class="ct-field__error" role="alert" [id]="errorId()">
|
|
1131
1532
|
{{ error() }}
|
|
1132
1533
|
</div>
|
|
1133
1534
|
}
|
|
1134
1535
|
</div>
|
|
1135
|
-
`, isInline: true,
|
|
1536
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1136
1537
|
}
|
|
1137
1538
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfInputComponent, decorators: [{
|
|
1138
1539
|
type: Component,
|
|
1139
|
-
args: [{
|
|
1540
|
+
args: [{
|
|
1541
|
+
selector: 'af-input',
|
|
1542
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1543
|
+
providers: [
|
|
1140
1544
|
{
|
|
1141
1545
|
provide: NG_VALUE_ACCESSOR,
|
|
1142
1546
|
useExisting: forwardRef(() => AfInputComponent),
|
|
1143
|
-
multi: true
|
|
1144
|
-
}
|
|
1145
|
-
],
|
|
1547
|
+
multi: true,
|
|
1548
|
+
},
|
|
1549
|
+
],
|
|
1550
|
+
host: {
|
|
1551
|
+
style: 'display: block',
|
|
1552
|
+
},
|
|
1553
|
+
template: `
|
|
1146
1554
|
<div class="ct-field" [class.ct-field--error]="error()">
|
|
1147
1555
|
@if (label()) {
|
|
1148
1556
|
<label class="ct-field__label" [attr.for]="inputId()">
|
|
1149
1557
|
{{ label() }}
|
|
1150
1558
|
@if (required()) {
|
|
1151
|
-
<span aria-label="required"> *</span>
|
|
1559
|
+
<span [attr.aria-label]="i18n.required"> *</span>
|
|
1152
1560
|
}
|
|
1153
1561
|
</label>
|
|
1154
1562
|
}
|
|
@@ -1157,7 +1565,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
1157
1565
|
<div class="ct-input-wrap">
|
|
1158
1566
|
@if (iconPosition() === 'left') {
|
|
1159
1567
|
<span class="ct-input__icon" aria-hidden="true">
|
|
1160
|
-
<ng-content select="[icon]"
|
|
1568
|
+
<ng-content select="[icon]" />
|
|
1161
1569
|
</span>
|
|
1162
1570
|
}
|
|
1163
1571
|
<input
|
|
@@ -1167,21 +1575,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
1167
1575
|
[disabled]="disabled()"
|
|
1168
1576
|
[required]="required()"
|
|
1169
1577
|
[attr.aria-invalid]="error() ? true : null"
|
|
1170
|
-
[attr.aria-describedby]="
|
|
1578
|
+
[attr.aria-describedby]="ariaDescribedBy()"
|
|
1171
1579
|
[class]="inputClasses()"
|
|
1172
|
-
[value]="value"
|
|
1580
|
+
[value]="value()"
|
|
1173
1581
|
(input)="onInput($event)"
|
|
1174
1582
|
(blur)="onTouched()"
|
|
1175
1583
|
/>
|
|
1176
1584
|
@if (iconPosition() === 'right') {
|
|
1177
1585
|
<span class="ct-input__icon" aria-hidden="true">
|
|
1178
|
-
<ng-content select="[icon]"
|
|
1586
|
+
<ng-content select="[icon]" />
|
|
1179
1587
|
</span>
|
|
1180
1588
|
}
|
|
1181
1589
|
</div>
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
@if (!iconPosition()) {
|
|
1590
|
+
} @else {
|
|
1185
1591
|
<input
|
|
1186
1592
|
[id]="inputId()"
|
|
1187
1593
|
[type]="type()"
|
|
@@ -1189,9 +1595,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
1189
1595
|
[disabled]="disabled()"
|
|
1190
1596
|
[required]="required()"
|
|
1191
1597
|
[attr.aria-invalid]="error() ? true : null"
|
|
1192
|
-
[attr.aria-describedby]="
|
|
1193
|
-
class="
|
|
1194
|
-
[value]="value"
|
|
1598
|
+
[attr.aria-describedby]="ariaDescribedBy()"
|
|
1599
|
+
[class]="inputClasses()"
|
|
1600
|
+
[value]="value()"
|
|
1195
1601
|
(input)="onInput($event)"
|
|
1196
1602
|
(blur)="onTouched()"
|
|
1197
1603
|
/>
|
|
@@ -1204,14 +1610,123 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
1204
1610
|
}
|
|
1205
1611
|
|
|
1206
1612
|
@if (error()) {
|
|
1207
|
-
<div class="ct-field__error" [id]="errorId()">
|
|
1613
|
+
<div class="ct-field__error" role="alert" [id]="errorId()">
|
|
1208
1614
|
{{ error() }}
|
|
1209
1615
|
</div>
|
|
1210
1616
|
}
|
|
1211
1617
|
</div>
|
|
1212
|
-
`,
|
|
1618
|
+
`,
|
|
1619
|
+
}]
|
|
1213
1620
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], iconPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconPosition", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }] } });
|
|
1214
1621
|
|
|
1622
|
+
/**
|
|
1623
|
+
* Test harness for AfInputComponent.
|
|
1624
|
+
*
|
|
1625
|
+
* Provides a semantic API for interacting with the input in tests,
|
|
1626
|
+
* abstracting DOM details behind readable method names.
|
|
1627
|
+
*
|
|
1628
|
+
* @example
|
|
1629
|
+
* const harness = new AfInputHarness(fixture.nativeElement);
|
|
1630
|
+
* expect(harness.getValue()).toBe('');
|
|
1631
|
+
* harness.setValue('hello');
|
|
1632
|
+
* expect(harness.getValue()).toBe('hello');
|
|
1633
|
+
*/
|
|
1634
|
+
class AfInputHarness {
|
|
1635
|
+
hostEl;
|
|
1636
|
+
constructor(container) {
|
|
1637
|
+
const el = container.querySelector('af-input');
|
|
1638
|
+
if (!el) {
|
|
1639
|
+
throw new Error('AfInputHarness: af-input element not found in container.');
|
|
1640
|
+
}
|
|
1641
|
+
this.hostEl = el;
|
|
1642
|
+
}
|
|
1643
|
+
/** Returns the inner native `<input>` element. */
|
|
1644
|
+
getInputElement() {
|
|
1645
|
+
const input = this.hostEl.querySelector('input');
|
|
1646
|
+
if (!input) {
|
|
1647
|
+
throw new Error('AfInputHarness: inner <input> element not found.');
|
|
1648
|
+
}
|
|
1649
|
+
return input;
|
|
1650
|
+
}
|
|
1651
|
+
/** Returns the current value of the input. */
|
|
1652
|
+
getValue() {
|
|
1653
|
+
return this.getInputElement().value;
|
|
1654
|
+
}
|
|
1655
|
+
/** Sets the input value and dispatches an `input` event. */
|
|
1656
|
+
setValue(value) {
|
|
1657
|
+
const input = this.getInputElement();
|
|
1658
|
+
input.value = value;
|
|
1659
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1660
|
+
}
|
|
1661
|
+
/** Returns the label text, or `null` if no label is rendered. */
|
|
1662
|
+
getLabel() {
|
|
1663
|
+
const label = this.hostEl.querySelector('.ct-field__label');
|
|
1664
|
+
return label?.textContent?.trim() ?? null;
|
|
1665
|
+
}
|
|
1666
|
+
/** Returns the hint text, or `null` if no hint is rendered. */
|
|
1667
|
+
getHint() {
|
|
1668
|
+
const hint = this.hostEl.querySelector('.ct-field__hint');
|
|
1669
|
+
return hint?.textContent?.trim() ?? null;
|
|
1670
|
+
}
|
|
1671
|
+
/** Returns the error text, or `null` if no error is rendered. */
|
|
1672
|
+
getError() {
|
|
1673
|
+
const error = this.hostEl.querySelector('.ct-field__error');
|
|
1674
|
+
return error?.textContent?.trim() ?? null;
|
|
1675
|
+
}
|
|
1676
|
+
/** Returns whether the input is disabled. */
|
|
1677
|
+
isDisabled() {
|
|
1678
|
+
return this.getInputElement().disabled;
|
|
1679
|
+
}
|
|
1680
|
+
/** Returns whether the input is required. */
|
|
1681
|
+
isRequired() {
|
|
1682
|
+
return this.getInputElement().required;
|
|
1683
|
+
}
|
|
1684
|
+
/** Returns whether `aria-invalid` is set to `"true"`. */
|
|
1685
|
+
isInvalid() {
|
|
1686
|
+
return this.getInputElement().getAttribute('aria-invalid') === 'true';
|
|
1687
|
+
}
|
|
1688
|
+
/** Returns the `type` attribute of the input. */
|
|
1689
|
+
getType() {
|
|
1690
|
+
return this.getInputElement().type;
|
|
1691
|
+
}
|
|
1692
|
+
/** Returns the `placeholder` attribute of the input. */
|
|
1693
|
+
getPlaceholder() {
|
|
1694
|
+
return this.getInputElement().placeholder;
|
|
1695
|
+
}
|
|
1696
|
+
/** Returns the `aria-describedby` attribute value, or `null` if absent. */
|
|
1697
|
+
getAriaDescribedBy() {
|
|
1698
|
+
return this.getInputElement().getAttribute('aria-describedby');
|
|
1699
|
+
}
|
|
1700
|
+
/** Focuses the input element. */
|
|
1701
|
+
focus() {
|
|
1702
|
+
this.getInputElement().focus();
|
|
1703
|
+
}
|
|
1704
|
+
/** Blurs the input element and dispatches a `blur` event. */
|
|
1705
|
+
blur() {
|
|
1706
|
+
this.getInputElement().dispatchEvent(new Event('blur', { bubbles: true }));
|
|
1707
|
+
}
|
|
1708
|
+
/** Returns the `id` attribute of the input. */
|
|
1709
|
+
getId() {
|
|
1710
|
+
return this.getInputElement().id;
|
|
1711
|
+
}
|
|
1712
|
+
/** Returns whether the field wrapper has the error modifier class. */
|
|
1713
|
+
hasFieldError() {
|
|
1714
|
+
return this.hostEl.querySelector('.ct-field--error') !== null;
|
|
1715
|
+
}
|
|
1716
|
+
/** Returns whether an icon wrapper is rendered. */
|
|
1717
|
+
hasIcon() {
|
|
1718
|
+
return this.hostEl.querySelector('.ct-input__icon') !== null;
|
|
1719
|
+
}
|
|
1720
|
+
/** Returns the full `class` attribute string of the inner input. */
|
|
1721
|
+
getClasses() {
|
|
1722
|
+
return this.getInputElement().className;
|
|
1723
|
+
}
|
|
1724
|
+
/** Returns whether the inner input has the given CSS class. */
|
|
1725
|
+
hasClass(className) {
|
|
1726
|
+
return this.getInputElement().classList.contains(className);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1215
1730
|
/**
|
|
1216
1731
|
* Select dropdown component with form control support
|
|
1217
1732
|
*
|
|
@@ -3579,164 +4094,493 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
3579
4094
|
`, styles: [":host{display:block}\n"] }]
|
|
3580
4095
|
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }] } });
|
|
3581
4096
|
|
|
4097
|
+
const MONTH_NAMES = [
|
|
4098
|
+
'January',
|
|
4099
|
+
'February',
|
|
4100
|
+
'March',
|
|
4101
|
+
'April',
|
|
4102
|
+
'May',
|
|
4103
|
+
'June',
|
|
4104
|
+
'July',
|
|
4105
|
+
'August',
|
|
4106
|
+
'September',
|
|
4107
|
+
'October',
|
|
4108
|
+
'November',
|
|
4109
|
+
'December',
|
|
4110
|
+
];
|
|
4111
|
+
const MONTH_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
4112
|
+
const WEEKDAY_LABELS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
|
3582
4113
|
/**
|
|
3583
|
-
* Datepicker component with calendar popup
|
|
4114
|
+
* Datepicker component with calendar popup, month/year views, range selection,
|
|
4115
|
+
* min/max constraints, disabled dates, and full keyboard navigation.
|
|
4116
|
+
*
|
|
4117
|
+
* Implements WAI-ARIA Date Picker Dialog pattern with roving tabindex.
|
|
3584
4118
|
*
|
|
3585
4119
|
* @example
|
|
3586
4120
|
* <af-datepicker
|
|
3587
|
-
* label="
|
|
4121
|
+
* label="Start date"
|
|
3588
4122
|
* placeholder="Pick a date"
|
|
3589
|
-
* [(ngModel)]="selectedDate"
|
|
3590
|
-
*
|
|
4123
|
+
* [(ngModel)]="selectedDate"
|
|
4124
|
+
* [min]="minDate"
|
|
4125
|
+
* [max]="maxDate"
|
|
4126
|
+
* hint="Choose a date within the project timeline"
|
|
4127
|
+
* />
|
|
4128
|
+
*
|
|
4129
|
+
* @example
|
|
4130
|
+
* <af-datepicker
|
|
4131
|
+
* label="Period"
|
|
4132
|
+
* mode="range"
|
|
4133
|
+
* [(ngModel)]="dateRange"
|
|
4134
|
+
* valueFormat="iso"
|
|
4135
|
+
* />
|
|
3591
4136
|
*/
|
|
3592
4137
|
class AfDatepickerComponent {
|
|
3593
4138
|
static nextId = 0;
|
|
3594
|
-
|
|
4139
|
+
// ── Public Inputs ────────────────────────────────────────────
|
|
4140
|
+
/** Field label */
|
|
3595
4141
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
3596
|
-
/**
|
|
4142
|
+
/** Input placeholder text */
|
|
3597
4143
|
placeholder = input('Select date', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
3598
|
-
/** Whether datepicker is disabled */
|
|
4144
|
+
/** Whether the datepicker is disabled */
|
|
3599
4145
|
disabled = model(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
3600
|
-
/**
|
|
4146
|
+
/** Display format for the selected date */
|
|
3601
4147
|
dateFormat = input('MMM dd, yyyy', ...(ngDevMode ? [{ debugName: "dateFormat" }] : []));
|
|
3602
|
-
/** Unique input
|
|
4148
|
+
/** Unique ID for the input element */
|
|
3603
4149
|
inputId = input(`af-datepicker-${AfDatepickerComponent.nextId++}`, ...(ngDevMode ? [{ debugName: "inputId" }] : []));
|
|
3604
|
-
/**
|
|
4150
|
+
/** Minimum selectable date */
|
|
4151
|
+
min = input(null, ...(ngDevMode ? [{ debugName: "min" }] : []));
|
|
4152
|
+
/** Maximum selectable date */
|
|
4153
|
+
max = input(null, ...(ngDevMode ? [{ debugName: "max" }] : []));
|
|
4154
|
+
/** Array of specific dates that cannot be selected */
|
|
4155
|
+
disabledDates = input([], ...(ngDevMode ? [{ debugName: "disabledDates" }] : []));
|
|
4156
|
+
/**
|
|
4157
|
+
* Predicate function to determine if a date is selectable.
|
|
4158
|
+
* Return `true` to allow selection, `false` to disable.
|
|
4159
|
+
*/
|
|
4160
|
+
dateFilter = input(null, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
|
|
4161
|
+
/** Hint text displayed below the input */
|
|
4162
|
+
hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
|
|
4163
|
+
/** Error message — displays error state and message */
|
|
4164
|
+
error = input('', ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
4165
|
+
/** Whether the field is required */
|
|
4166
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
4167
|
+
/** Selection mode: single date or date range */
|
|
4168
|
+
mode = input('single', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
4169
|
+
/**
|
|
4170
|
+
* Value format for ControlValueAccessor.
|
|
4171
|
+
* `'date'` emits `Date` objects, `'iso'` emits ISO date strings (`yyyy-MM-dd`).
|
|
4172
|
+
*/
|
|
4173
|
+
valueFormat = input('date', ...(ngDevMode ? [{ debugName: "valueFormat" }] : []));
|
|
4174
|
+
/** Emitted when a single date is selected */
|
|
3605
4175
|
dateChange = output();
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
4176
|
+
/** Emitted when a date range is selected (range mode only) */
|
|
4177
|
+
rangeChange = output();
|
|
4178
|
+
// ── Template References ──────────────────────────────────────
|
|
4179
|
+
inputRef = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputRef" }] : []));
|
|
4180
|
+
popoverRef = viewChild('popoverEl', ...(ngDevMode ? [{ debugName: "popoverRef" }] : []));
|
|
4181
|
+
// ── Constants ────────────────────────────────────────────────
|
|
4182
|
+
weekdayLabels = WEEKDAY_LABELS;
|
|
4183
|
+
weekdayFullLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
4184
|
+
// ── Internal State ───────────────────────────────────────────
|
|
3613
4185
|
selectedDate = signal(null, ...(ngDevMode ? [{ debugName: "selectedDate" }] : []));
|
|
4186
|
+
rangeStart = signal(null, ...(ngDevMode ? [{ debugName: "rangeStart" }] : []));
|
|
4187
|
+
rangeEnd = signal(null, ...(ngDevMode ? [{ debugName: "rangeEnd" }] : []));
|
|
4188
|
+
rangeSelecting = signal(false, ...(ngDevMode ? [{ debugName: "rangeSelecting" }] : []));
|
|
3614
4189
|
currentMonth = signal(new Date().getMonth(), ...(ngDevMode ? [{ debugName: "currentMonth" }] : []));
|
|
3615
4190
|
currentYear = signal(new Date().getFullYear(), ...(ngDevMode ? [{ debugName: "currentYear" }] : []));
|
|
3616
4191
|
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
3617
4192
|
focusedDate = signal(null, ...(ngDevMode ? [{ debugName: "focusedDate" }] : []));
|
|
4193
|
+
currentView = signal('days', ...(ngDevMode ? [{ debugName: "currentView" }] : []));
|
|
4194
|
+
focusedMonth = signal(new Date().getMonth(), ...(ngDevMode ? [{ debugName: "focusedMonth" }] : []));
|
|
4195
|
+
focusedYear = signal(new Date().getFullYear(), ...(ngDevMode ? [{ debugName: "focusedYear" }] : []));
|
|
4196
|
+
yearPageStart = signal(Math.floor(new Date().getFullYear() / 12) * 12, ...(ngDevMode ? [{ debugName: "yearPageStart" }] : []));
|
|
3618
4197
|
onChange = () => { };
|
|
3619
4198
|
onTouched = () => { };
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
const date = this.selectedDate();
|
|
3625
|
-
if (!date)
|
|
3626
|
-
return '';
|
|
3627
|
-
return this.formatDate(date);
|
|
3628
|
-
}, ...(ngDevMode ? [{ debugName: "formattedDate" }] : []));
|
|
4199
|
+
onValidatorChange = () => { };
|
|
4200
|
+
// ── Computed ─────────────────────────────────────────────────
|
|
4201
|
+
parsedMin = computed(() => this.parseDate(this.min()), ...(ngDevMode ? [{ debugName: "parsedMin" }] : []));
|
|
4202
|
+
parsedMax = computed(() => this.parseDate(this.max()), ...(ngDevMode ? [{ debugName: "parsedMax" }] : []));
|
|
3629
4203
|
popoverId = computed(() => `${this.inputId()}-popover`, ...(ngDevMode ? [{ debugName: "popoverId" }] : []));
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
4204
|
+
hintId = computed(() => `${this.inputId()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
|
|
4205
|
+
errorId = computed(() => `${this.inputId()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
|
|
4206
|
+
ariaDescribedBy = computed(() => {
|
|
4207
|
+
if (this.error())
|
|
4208
|
+
return this.errorId();
|
|
4209
|
+
if (this.hint())
|
|
4210
|
+
return this.hintId();
|
|
4211
|
+
return null;
|
|
4212
|
+
}, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
|
|
4213
|
+
dialogAriaLabel = computed(() => {
|
|
4214
|
+
if (this.mode() === 'range')
|
|
4215
|
+
return 'Choose date range';
|
|
4216
|
+
return 'Choose date';
|
|
4217
|
+
}, ...(ngDevMode ? [{ debugName: "dialogAriaLabel" }] : []));
|
|
4218
|
+
headerTitle = computed(() => {
|
|
4219
|
+
switch (this.currentView()) {
|
|
4220
|
+
case 'days':
|
|
4221
|
+
return `${MONTH_NAMES[this.currentMonth()]} ${this.currentYear()}`;
|
|
4222
|
+
case 'months':
|
|
4223
|
+
return `${this.currentYear()}`;
|
|
4224
|
+
case 'years':
|
|
4225
|
+
return `${this.yearPageStart()} – ${this.yearPageStart() + 11}`;
|
|
4226
|
+
}
|
|
4227
|
+
}, ...(ngDevMode ? [{ debugName: "headerTitle" }] : []));
|
|
4228
|
+
titleAriaLabel = computed(() => {
|
|
4229
|
+
switch (this.currentView()) {
|
|
4230
|
+
case 'days':
|
|
4231
|
+
return 'Switch to month view';
|
|
4232
|
+
case 'months':
|
|
4233
|
+
return 'Switch to year view';
|
|
4234
|
+
case 'years':
|
|
4235
|
+
return null;
|
|
4236
|
+
}
|
|
4237
|
+
}, ...(ngDevMode ? [{ debugName: "titleAriaLabel" }] : []));
|
|
4238
|
+
prevButtonAriaLabel = computed(() => {
|
|
4239
|
+
switch (this.currentView()) {
|
|
4240
|
+
case 'days':
|
|
4241
|
+
return 'Previous month';
|
|
4242
|
+
case 'months':
|
|
4243
|
+
return 'Previous year';
|
|
4244
|
+
case 'years':
|
|
4245
|
+
return 'Previous 12 years';
|
|
4246
|
+
}
|
|
4247
|
+
}, ...(ngDevMode ? [{ debugName: "prevButtonAriaLabel" }] : []));
|
|
4248
|
+
nextButtonAriaLabel = computed(() => {
|
|
4249
|
+
switch (this.currentView()) {
|
|
4250
|
+
case 'days':
|
|
4251
|
+
return 'Next month';
|
|
4252
|
+
case 'months':
|
|
4253
|
+
return 'Next year';
|
|
4254
|
+
case 'years':
|
|
4255
|
+
return 'Next 12 years';
|
|
4256
|
+
}
|
|
4257
|
+
}, ...(ngDevMode ? [{ debugName: "nextButtonAriaLabel" }] : []));
|
|
4258
|
+
gridAriaLabel = computed(() => `${MONTH_NAMES[this.currentMonth()]} ${this.currentYear()}`, ...(ngDevMode ? [{ debugName: "gridAriaLabel" }] : []));
|
|
4259
|
+
hasClearableValue = computed(() => {
|
|
4260
|
+
if (this.mode() === 'range') {
|
|
4261
|
+
return this.rangeStart() !== null || this.rangeEnd() !== null;
|
|
4262
|
+
}
|
|
4263
|
+
return this.selectedDate() !== null;
|
|
4264
|
+
}, ...(ngDevMode ? [{ debugName: "hasClearableValue" }] : []));
|
|
4265
|
+
formattedValue = computed(() => {
|
|
4266
|
+
if (this.mode() === 'range') {
|
|
4267
|
+
const start = this.rangeStart();
|
|
4268
|
+
const end = this.rangeEnd();
|
|
4269
|
+
if (!start && !end)
|
|
4270
|
+
return '';
|
|
4271
|
+
const startStr = start ? this.formatDate(start) : '...';
|
|
4272
|
+
const endStr = end ? this.formatDate(end) : '...';
|
|
4273
|
+
return `${startStr} – ${endStr}`;
|
|
3636
4274
|
}
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
this.dateChange.emit(date);
|
|
3643
|
-
this.close(true);
|
|
3644
|
-
}
|
|
3645
|
-
previousMonth() {
|
|
3646
|
-
this.shiftMonth(-1);
|
|
3647
|
-
}
|
|
3648
|
-
nextMonth() {
|
|
3649
|
-
this.shiftMonth(1);
|
|
3650
|
-
}
|
|
3651
|
-
generateCalendarDays() {
|
|
4275
|
+
const date = this.selectedDate();
|
|
4276
|
+
return date ? this.formatDate(date) : '';
|
|
4277
|
+
}, ...(ngDevMode ? [{ debugName: "formattedValue" }] : []));
|
|
4278
|
+
isTodayDisabled = computed(() => this.isDateDisabled(new Date()), ...(ngDevMode ? [{ debugName: "isTodayDisabled" }] : []));
|
|
4279
|
+
calendarDays = computed(() => {
|
|
3652
4280
|
const year = this.currentYear();
|
|
3653
4281
|
const month = this.currentMonth();
|
|
3654
|
-
const firstDay = new Date(year, month, 1);
|
|
3655
|
-
const lastDay = new Date(year, month + 1, 0);
|
|
3656
4282
|
const today = new Date();
|
|
3657
4283
|
const selected = this.selectedDate();
|
|
4284
|
+
const rStart = this.rangeStart();
|
|
4285
|
+
const rEnd = this.rangeEnd();
|
|
4286
|
+
const previewEnd = this.mode() === 'range' && this.rangeSelecting() ? this.focusedDate() : null;
|
|
4287
|
+
const effectiveEnd = rEnd ?? previewEnd;
|
|
4288
|
+
const firstDay = new Date(year, month, 1);
|
|
4289
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
3658
4290
|
const days = [];
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
for (let i = startDay - 1; i >= 0; i--) {
|
|
3666
|
-
const date = new Date(year, month - 1, prevMonthLastDay - i);
|
|
3667
|
-
days.push({
|
|
3668
|
-
date,
|
|
3669
|
-
isCurrentMonth: false,
|
|
3670
|
-
isToday: this.isSameDay(date, today),
|
|
3671
|
-
isSelected: selected ? this.isSameDay(date, selected) : false
|
|
3672
|
-
});
|
|
4291
|
+
let startPad = firstDay.getDay() - 1;
|
|
4292
|
+
if (startPad < 0)
|
|
4293
|
+
startPad = 6;
|
|
4294
|
+
const prevLast = new Date(year, month, 0).getDate();
|
|
4295
|
+
for (let i = startPad - 1; i >= 0; i--) {
|
|
4296
|
+
days.push(this.buildDay(new Date(year, month - 1, prevLast - i), false, today, selected, rStart, effectiveEnd));
|
|
3673
4297
|
}
|
|
3674
|
-
// Current month days
|
|
3675
4298
|
for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
3676
|
-
|
|
3677
|
-
days.push({
|
|
3678
|
-
date,
|
|
3679
|
-
isCurrentMonth: true,
|
|
3680
|
-
isToday: this.isSameDay(date, today),
|
|
3681
|
-
isSelected: selected ? this.isSameDay(date, selected) : false
|
|
3682
|
-
});
|
|
4299
|
+
days.push(this.buildDay(new Date(year, month, i), true, today, selected, rStart, effectiveEnd));
|
|
3683
4300
|
}
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
const date = new Date(year, month + 1, i);
|
|
3688
|
-
days.push({
|
|
3689
|
-
date,
|
|
3690
|
-
isCurrentMonth: false,
|
|
3691
|
-
isToday: this.isSameDay(date, today),
|
|
3692
|
-
isSelected: selected ? this.isSameDay(date, selected) : false
|
|
3693
|
-
});
|
|
4301
|
+
const remaining = 42 - days.length;
|
|
4302
|
+
for (let i = 1; i <= remaining; i++) {
|
|
4303
|
+
days.push(this.buildDay(new Date(year, month + 1, i), false, today, selected, rStart, effectiveEnd));
|
|
3694
4304
|
}
|
|
3695
4305
|
return days;
|
|
4306
|
+
}, ...(ngDevMode ? [{ debugName: "calendarDays" }] : []));
|
|
4307
|
+
monthItems = computed(() => {
|
|
4308
|
+
const year = this.currentYear();
|
|
4309
|
+
const selected = this.selectedDate();
|
|
4310
|
+
const min = this.parsedMin();
|
|
4311
|
+
const max = this.parsedMax();
|
|
4312
|
+
return Array.from({ length: 12 }, (_, i) => {
|
|
4313
|
+
const isDisabled = this.isMonthDisabled(year, i, min, max);
|
|
4314
|
+
return {
|
|
4315
|
+
index: i,
|
|
4316
|
+
label: MONTH_NAMES[i],
|
|
4317
|
+
shortLabel: MONTH_SHORT[i],
|
|
4318
|
+
isSelected: selected ? selected.getMonth() === i && selected.getFullYear() === year : false,
|
|
4319
|
+
isDisabled,
|
|
4320
|
+
};
|
|
4321
|
+
});
|
|
4322
|
+
}, ...(ngDevMode ? [{ debugName: "monthItems" }] : []));
|
|
4323
|
+
yearItems = computed(() => {
|
|
4324
|
+
const start = this.yearPageStart();
|
|
4325
|
+
const selected = this.selectedDate();
|
|
4326
|
+
const min = this.parsedMin();
|
|
4327
|
+
const max = this.parsedMax();
|
|
4328
|
+
return Array.from({ length: 12 }, (_, i) => {
|
|
4329
|
+
const value = start + i;
|
|
4330
|
+
return {
|
|
4331
|
+
value,
|
|
4332
|
+
isSelected: selected ? selected.getFullYear() === value : false,
|
|
4333
|
+
isDisabled: this.isYearDisabled(value, min, max),
|
|
4334
|
+
};
|
|
4335
|
+
});
|
|
4336
|
+
}, ...(ngDevMode ? [{ debugName: "yearItems" }] : []));
|
|
4337
|
+
// ── Open / Close ─────────────────────────────────────────────
|
|
4338
|
+
toggle() {
|
|
4339
|
+
if (this.isOpen()) {
|
|
4340
|
+
this.close(true);
|
|
4341
|
+
}
|
|
4342
|
+
else {
|
|
4343
|
+
this.open();
|
|
4344
|
+
}
|
|
3696
4345
|
}
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
if (
|
|
3700
|
-
return 0;
|
|
3701
|
-
if (!focused && day.isSelected)
|
|
3702
|
-
return 0;
|
|
3703
|
-
return -1;
|
|
3704
|
-
}
|
|
3705
|
-
getDateKey(date) {
|
|
3706
|
-
const year = date.getFullYear();
|
|
3707
|
-
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
3708
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
3709
|
-
return `${year}-${month}-${day}`;
|
|
3710
|
-
}
|
|
3711
|
-
onInputKeydown(event) {
|
|
3712
|
-
if (this.disabled())
|
|
4346
|
+
/** Opens the calendar popover */
|
|
4347
|
+
open() {
|
|
4348
|
+
if (this.disabled() || this.isOpen())
|
|
3713
4349
|
return;
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
4350
|
+
const selected = this.selectedDate();
|
|
4351
|
+
const focusDate = selected ?? new Date();
|
|
4352
|
+
this.currentMonth.set(focusDate.getMonth());
|
|
4353
|
+
this.currentYear.set(focusDate.getFullYear());
|
|
4354
|
+
this.focusedDate.set(focusDate);
|
|
4355
|
+
this.focusedMonth.set(focusDate.getMonth());
|
|
4356
|
+
this.focusedYear.set(focusDate.getFullYear());
|
|
4357
|
+
this.currentView.set('days');
|
|
4358
|
+
this.isOpen.set(true);
|
|
4359
|
+
queueMicrotask(() => this.focusDayButton(focusDate));
|
|
4360
|
+
}
|
|
4361
|
+
/** Closes the calendar popover */
|
|
4362
|
+
close(returnFocus = false) {
|
|
4363
|
+
if (!this.isOpen())
|
|
4364
|
+
return;
|
|
4365
|
+
this.isOpen.set(false);
|
|
4366
|
+
this.onTouched();
|
|
4367
|
+
if (returnFocus) {
|
|
4368
|
+
this.inputRef()?.nativeElement.focus();
|
|
3717
4369
|
}
|
|
3718
|
-
|
|
3719
|
-
|
|
4370
|
+
}
|
|
4371
|
+
// ── Day Selection ────────────────────────────────────────────
|
|
4372
|
+
/** Handles click on a calendar day */
|
|
4373
|
+
onDayClick(day) {
|
|
4374
|
+
if (day.isDisabled || day.isUnavailable)
|
|
4375
|
+
return;
|
|
4376
|
+
if (this.mode() === 'range') {
|
|
4377
|
+
this.selectRangeDate(day.date);
|
|
4378
|
+
}
|
|
4379
|
+
else {
|
|
4380
|
+
this.selectSingleDate(day.date);
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
/** Selects a single date and closes the popover */
|
|
4384
|
+
selectSingleDate(date) {
|
|
4385
|
+
this.selectedDate.set(date);
|
|
4386
|
+
this.focusedDate.set(date);
|
|
4387
|
+
this.emitSingleValue(date);
|
|
4388
|
+
this.dateChange.emit(date);
|
|
4389
|
+
this.close(true);
|
|
4390
|
+
}
|
|
4391
|
+
/** Handles range date selection (two-click: start then end) */
|
|
4392
|
+
selectRangeDate(date) {
|
|
4393
|
+
if (!this.rangeSelecting()) {
|
|
4394
|
+
this.rangeStart.set(date);
|
|
4395
|
+
this.rangeEnd.set(null);
|
|
4396
|
+
this.rangeSelecting.set(true);
|
|
4397
|
+
this.focusedDate.set(date);
|
|
4398
|
+
}
|
|
4399
|
+
else {
|
|
4400
|
+
let start = this.rangeStart();
|
|
4401
|
+
let end = date;
|
|
4402
|
+
if (this.compareDays(end, start) < 0) {
|
|
4403
|
+
[start, end] = [end, start];
|
|
4404
|
+
}
|
|
4405
|
+
this.rangeStart.set(start);
|
|
4406
|
+
this.rangeEnd.set(end);
|
|
4407
|
+
this.rangeSelecting.set(false);
|
|
4408
|
+
this.emitRangeValue(start, end);
|
|
4409
|
+
this.rangeChange.emit({ start, end });
|
|
3720
4410
|
this.close(true);
|
|
3721
4411
|
}
|
|
3722
4412
|
}
|
|
3723
|
-
|
|
4413
|
+
/** Navigates to today and selects it (single mode) or focuses it */
|
|
4414
|
+
goToToday() {
|
|
4415
|
+
const today = new Date();
|
|
4416
|
+
if (this.isDateDisabled(today))
|
|
4417
|
+
return;
|
|
4418
|
+
this.currentMonth.set(today.getMonth());
|
|
4419
|
+
this.currentYear.set(today.getFullYear());
|
|
4420
|
+
this.focusedDate.set(today);
|
|
4421
|
+
if (this.mode() === 'single') {
|
|
4422
|
+
this.selectSingleDate(today);
|
|
4423
|
+
}
|
|
4424
|
+
else {
|
|
4425
|
+
this.selectRangeDate(today);
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
/** Clears the selected value */
|
|
4429
|
+
clearValue(event) {
|
|
4430
|
+
event.stopPropagation();
|
|
4431
|
+
if (this.mode() === 'range') {
|
|
4432
|
+
this.rangeStart.set(null);
|
|
4433
|
+
this.rangeEnd.set(null);
|
|
4434
|
+
this.rangeSelecting.set(false);
|
|
4435
|
+
this.emitRangeValue(null, null);
|
|
4436
|
+
}
|
|
4437
|
+
else {
|
|
4438
|
+
this.selectedDate.set(null);
|
|
4439
|
+
this.emitSingleValue(null);
|
|
4440
|
+
}
|
|
4441
|
+
this.inputRef()?.nativeElement.focus();
|
|
4442
|
+
}
|
|
4443
|
+
// ── View Navigation ──────────────────────────────────────────
|
|
4444
|
+
/** Drills up: days -> months -> years */
|
|
4445
|
+
drillUp() {
|
|
4446
|
+
const view = this.currentView();
|
|
4447
|
+
if (view === 'days') {
|
|
4448
|
+
this.focusedMonth.set(this.currentMonth());
|
|
4449
|
+
this.currentView.set('months');
|
|
4450
|
+
queueMicrotask(() => this.focusMonthButton(this.focusedMonth()));
|
|
4451
|
+
}
|
|
4452
|
+
else if (view === 'months') {
|
|
4453
|
+
this.focusedYear.set(this.currentYear());
|
|
4454
|
+
this.yearPageStart.set(Math.floor(this.currentYear() / 12) * 12);
|
|
4455
|
+
this.currentView.set('years');
|
|
4456
|
+
queueMicrotask(() => this.focusYearButton(this.focusedYear()));
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
/** Navigates to previous period based on current view */
|
|
4460
|
+
navigatePrevious() {
|
|
4461
|
+
switch (this.currentView()) {
|
|
4462
|
+
case 'days':
|
|
4463
|
+
this.shiftMonth(-1);
|
|
4464
|
+
break;
|
|
4465
|
+
case 'months':
|
|
4466
|
+
this.currentYear.update((y) => y - 1);
|
|
4467
|
+
break;
|
|
4468
|
+
case 'years':
|
|
4469
|
+
this.yearPageStart.update((s) => s - 12);
|
|
4470
|
+
break;
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
/** Navigates to next period based on current view */
|
|
4474
|
+
navigateNext() {
|
|
4475
|
+
switch (this.currentView()) {
|
|
4476
|
+
case 'days':
|
|
4477
|
+
this.shiftMonth(1);
|
|
4478
|
+
break;
|
|
4479
|
+
case 'months':
|
|
4480
|
+
this.currentYear.update((y) => y + 1);
|
|
4481
|
+
break;
|
|
4482
|
+
case 'years':
|
|
4483
|
+
this.yearPageStart.update((s) => s + 12);
|
|
4484
|
+
break;
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
/** Selects a month from the month view and switches to days */
|
|
4488
|
+
selectMonth(monthIndex) {
|
|
4489
|
+
this.currentMonth.set(monthIndex);
|
|
4490
|
+
this.focusedMonth.set(monthIndex);
|
|
4491
|
+
this.currentView.set('days');
|
|
4492
|
+
const focusDate = new Date(this.currentYear(), monthIndex, 1);
|
|
4493
|
+
this.focusedDate.set(focusDate);
|
|
4494
|
+
queueMicrotask(() => this.focusDayButton(focusDate));
|
|
4495
|
+
}
|
|
4496
|
+
/** Selects a year from the year view and switches to months */
|
|
4497
|
+
selectYear(year) {
|
|
4498
|
+
this.currentYear.set(year);
|
|
4499
|
+
this.focusedYear.set(year);
|
|
4500
|
+
this.currentView.set('months');
|
|
4501
|
+
queueMicrotask(() => this.focusMonthButton(this.focusedMonth()));
|
|
4502
|
+
}
|
|
4503
|
+
// ── Day Helpers for Template ─────────────────────────────────
|
|
4504
|
+
/** Returns true if the given day matches the keyboard-focused date */
|
|
4505
|
+
isDayHighlighted(day) {
|
|
4506
|
+
const focused = this.focusedDate();
|
|
4507
|
+
return focused !== null && this.isSameDay(day.date, focused);
|
|
4508
|
+
}
|
|
4509
|
+
/** Returns the tabindex for a day button (roving tabindex pattern) */
|
|
4510
|
+
getDayTabIndex(day) {
|
|
4511
|
+
const focused = this.focusedDate();
|
|
4512
|
+
if (focused && this.isSameDay(day.date, focused))
|
|
4513
|
+
return 0;
|
|
4514
|
+
if (!focused && day.isSelected)
|
|
4515
|
+
return 0;
|
|
4516
|
+
return -1;
|
|
4517
|
+
}
|
|
4518
|
+
/** Returns true if the given month index matches the keyboard-focused month */
|
|
4519
|
+
isMonthHighlighted(monthIndex) {
|
|
4520
|
+
return this.currentView() === 'months' && this.focusedMonth() === monthIndex;
|
|
4521
|
+
}
|
|
4522
|
+
/** Returns the tabindex for a month button */
|
|
4523
|
+
getMonthTabIndex(monthIndex) {
|
|
4524
|
+
return this.focusedMonth() === monthIndex ? 0 : -1;
|
|
4525
|
+
}
|
|
4526
|
+
/** Returns true if the given year matches the keyboard-focused year */
|
|
4527
|
+
isYearHighlighted(year) {
|
|
4528
|
+
return this.currentView() === 'years' && this.focusedYear() === year;
|
|
4529
|
+
}
|
|
4530
|
+
/** Returns the tabindex for a year button */
|
|
4531
|
+
getYearTabIndex(year) {
|
|
4532
|
+
return this.focusedYear() === year ? 0 : -1;
|
|
4533
|
+
}
|
|
4534
|
+
/** Returns a date key string for DOM identification */
|
|
4535
|
+
getDateKey(date) {
|
|
4536
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
4537
|
+
}
|
|
4538
|
+
// ── Keyboard Handlers ────────────────────────────────────────
|
|
4539
|
+
onInputKeydown(event) {
|
|
4540
|
+
if (this.disabled())
|
|
4541
|
+
return;
|
|
4542
|
+
switch (event.key) {
|
|
4543
|
+
case 'ArrowDown':
|
|
4544
|
+
case 'Enter':
|
|
4545
|
+
case ' ':
|
|
4546
|
+
event.preventDefault();
|
|
4547
|
+
this.open();
|
|
4548
|
+
break;
|
|
4549
|
+
case 'Escape':
|
|
4550
|
+
if (this.isOpen()) {
|
|
4551
|
+
event.preventDefault();
|
|
4552
|
+
this.close(true);
|
|
4553
|
+
}
|
|
4554
|
+
break;
|
|
4555
|
+
case 'Delete':
|
|
4556
|
+
case 'Backspace':
|
|
4557
|
+
if (this.hasClearableValue()) {
|
|
4558
|
+
event.preventDefault();
|
|
4559
|
+
this.clearValue(event);
|
|
4560
|
+
}
|
|
4561
|
+
break;
|
|
4562
|
+
}
|
|
4563
|
+
}
|
|
4564
|
+
/** Keyboard navigation within the day grid */
|
|
4565
|
+
onDayGridKeydown(event) {
|
|
3724
4566
|
if (!this.isOpen())
|
|
3725
4567
|
return;
|
|
3726
|
-
const focused = this.focusedDate()
|
|
4568
|
+
const focused = this.focusedDate() ??
|
|
4569
|
+
this.selectedDate() ??
|
|
4570
|
+
new Date(this.currentYear(), this.currentMonth(), 1);
|
|
3727
4571
|
let nextDate = null;
|
|
3728
4572
|
switch (event.key) {
|
|
3729
4573
|
case 'ArrowRight':
|
|
3730
|
-
nextDate = this.
|
|
4574
|
+
nextDate = this.findNextEnabledDate(focused, 1);
|
|
3731
4575
|
break;
|
|
3732
4576
|
case 'ArrowLeft':
|
|
3733
|
-
nextDate = this.
|
|
4577
|
+
nextDate = this.findNextEnabledDate(focused, -1);
|
|
3734
4578
|
break;
|
|
3735
4579
|
case 'ArrowDown':
|
|
3736
|
-
nextDate = this.
|
|
4580
|
+
nextDate = this.findNextEnabledDate(focused, 7);
|
|
3737
4581
|
break;
|
|
3738
4582
|
case 'ArrowUp':
|
|
3739
|
-
nextDate = this.
|
|
4583
|
+
nextDate = this.findNextEnabledDate(focused, -7);
|
|
3740
4584
|
break;
|
|
3741
4585
|
case 'Home':
|
|
3742
4586
|
nextDate = this.addDays(focused, -this.getWeekdayIndex(focused));
|
|
@@ -3745,15 +4589,17 @@ class AfDatepickerComponent {
|
|
|
3745
4589
|
nextDate = this.addDays(focused, 6 - this.getWeekdayIndex(focused));
|
|
3746
4590
|
break;
|
|
3747
4591
|
case 'PageUp':
|
|
3748
|
-
nextDate = this.addMonths(focused, -1);
|
|
4592
|
+
nextDate = event.shiftKey ? this.addYears(focused, -1) : this.addMonths(focused, -1);
|
|
3749
4593
|
break;
|
|
3750
4594
|
case 'PageDown':
|
|
3751
|
-
nextDate = this.addMonths(focused, 1);
|
|
4595
|
+
nextDate = event.shiftKey ? this.addYears(focused, 1) : this.addMonths(focused, 1);
|
|
3752
4596
|
break;
|
|
3753
4597
|
case 'Enter':
|
|
3754
4598
|
case ' ':
|
|
3755
4599
|
event.preventDefault();
|
|
3756
|
-
this.
|
|
4600
|
+
if (!this.isDateDisabled(focused)) {
|
|
4601
|
+
this.onDayClick({ date: focused });
|
|
4602
|
+
}
|
|
3757
4603
|
return;
|
|
3758
4604
|
case 'Escape':
|
|
3759
4605
|
event.preventDefault();
|
|
@@ -3767,10 +4613,100 @@ class AfDatepickerComponent {
|
|
|
3767
4613
|
this.setFocusedDate(nextDate);
|
|
3768
4614
|
}
|
|
3769
4615
|
}
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
4616
|
+
/** Keyboard navigation within the month grid */
|
|
4617
|
+
onMonthGridKeydown(event) {
|
|
4618
|
+
let nextMonth = this.focusedMonth();
|
|
4619
|
+
switch (event.key) {
|
|
4620
|
+
case 'ArrowRight':
|
|
4621
|
+
nextMonth = Math.min(11, nextMonth + 1);
|
|
4622
|
+
break;
|
|
4623
|
+
case 'ArrowLeft':
|
|
4624
|
+
nextMonth = Math.max(0, nextMonth - 1);
|
|
4625
|
+
break;
|
|
4626
|
+
case 'ArrowDown':
|
|
4627
|
+
nextMonth = Math.min(11, nextMonth + 3);
|
|
4628
|
+
break;
|
|
4629
|
+
case 'ArrowUp':
|
|
4630
|
+
nextMonth = Math.max(0, nextMonth - 3);
|
|
4631
|
+
break;
|
|
4632
|
+
case 'Home':
|
|
4633
|
+
nextMonth = 0;
|
|
4634
|
+
break;
|
|
4635
|
+
case 'End':
|
|
4636
|
+
nextMonth = 11;
|
|
4637
|
+
break;
|
|
4638
|
+
case 'Enter':
|
|
4639
|
+
case ' ':
|
|
4640
|
+
event.preventDefault();
|
|
4641
|
+
if (!this.isMonthDisabled(this.currentYear(), nextMonth, this.parsedMin(), this.parsedMax())) {
|
|
4642
|
+
this.selectMonth(nextMonth);
|
|
4643
|
+
}
|
|
4644
|
+
return;
|
|
4645
|
+
case 'Escape':
|
|
4646
|
+
event.preventDefault();
|
|
4647
|
+
this.currentView.set('days');
|
|
4648
|
+
queueMicrotask(() => this.focusDayButton(this.focusedDate() ?? new Date()));
|
|
4649
|
+
return;
|
|
4650
|
+
default:
|
|
4651
|
+
return;
|
|
4652
|
+
}
|
|
4653
|
+
event.preventDefault();
|
|
4654
|
+
this.focusedMonth.set(nextMonth);
|
|
4655
|
+
queueMicrotask(() => this.focusMonthButton(nextMonth));
|
|
4656
|
+
}
|
|
4657
|
+
/** Keyboard navigation within the year grid */
|
|
4658
|
+
onYearGridKeydown(event) {
|
|
4659
|
+
let nextYear = this.focusedYear();
|
|
4660
|
+
const pageStart = this.yearPageStart();
|
|
4661
|
+
switch (event.key) {
|
|
4662
|
+
case 'ArrowRight':
|
|
4663
|
+
nextYear += 1;
|
|
4664
|
+
break;
|
|
4665
|
+
case 'ArrowLeft':
|
|
4666
|
+
nextYear -= 1;
|
|
4667
|
+
break;
|
|
4668
|
+
case 'ArrowDown':
|
|
4669
|
+
nextYear += 3;
|
|
4670
|
+
break;
|
|
4671
|
+
case 'ArrowUp':
|
|
4672
|
+
nextYear -= 3;
|
|
4673
|
+
break;
|
|
4674
|
+
case 'Home':
|
|
4675
|
+
nextYear = pageStart;
|
|
4676
|
+
break;
|
|
4677
|
+
case 'End':
|
|
4678
|
+
nextYear = pageStart + 11;
|
|
4679
|
+
break;
|
|
4680
|
+
case 'PageUp':
|
|
4681
|
+
nextYear -= 12;
|
|
4682
|
+
break;
|
|
4683
|
+
case 'PageDown':
|
|
4684
|
+
nextYear += 12;
|
|
4685
|
+
break;
|
|
4686
|
+
case 'Enter':
|
|
4687
|
+
case ' ':
|
|
4688
|
+
event.preventDefault();
|
|
4689
|
+
if (!this.isYearDisabled(nextYear, this.parsedMin(), this.parsedMax())) {
|
|
4690
|
+
this.selectYear(nextYear);
|
|
4691
|
+
}
|
|
4692
|
+
return;
|
|
4693
|
+
case 'Escape':
|
|
4694
|
+
event.preventDefault();
|
|
4695
|
+
this.currentView.set('months');
|
|
4696
|
+
queueMicrotask(() => this.focusMonthButton(this.focusedMonth()));
|
|
4697
|
+
return;
|
|
4698
|
+
default:
|
|
4699
|
+
return;
|
|
3773
4700
|
}
|
|
4701
|
+
event.preventDefault();
|
|
4702
|
+
if (nextYear < pageStart) {
|
|
4703
|
+
this.yearPageStart.update((s) => s - 12);
|
|
4704
|
+
}
|
|
4705
|
+
else if (nextYear >= pageStart + 12) {
|
|
4706
|
+
this.yearPageStart.update((s) => s + 12);
|
|
4707
|
+
}
|
|
4708
|
+
this.focusedYear.set(nextYear);
|
|
4709
|
+
queueMicrotask(() => this.focusYearButton(nextYear));
|
|
3774
4710
|
}
|
|
3775
4711
|
onDocumentClick(event) {
|
|
3776
4712
|
const target = event.target;
|
|
@@ -3778,24 +4714,204 @@ class AfDatepickerComponent {
|
|
|
3778
4714
|
this.close(false);
|
|
3779
4715
|
}
|
|
3780
4716
|
}
|
|
3781
|
-
|
|
3782
|
-
|
|
4717
|
+
// ── ControlValueAccessor ─────────────────────────────────────
|
|
4718
|
+
writeValue(value) {
|
|
4719
|
+
if (this.mode() === 'range') {
|
|
4720
|
+
this.writeRangeValue(value);
|
|
4721
|
+
}
|
|
4722
|
+
else {
|
|
4723
|
+
this.writeSingleValue(value);
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
registerOnChange(fn) {
|
|
4727
|
+
this.onChange = fn;
|
|
4728
|
+
}
|
|
4729
|
+
registerOnTouched(fn) {
|
|
4730
|
+
this.onTouched = fn;
|
|
4731
|
+
}
|
|
4732
|
+
setDisabledState(isDisabled) {
|
|
4733
|
+
this.disabled.set(isDisabled);
|
|
4734
|
+
}
|
|
4735
|
+
// ── Validator ────────────────────────────────────────────────
|
|
4736
|
+
validate(control) {
|
|
4737
|
+
const value = control.value;
|
|
4738
|
+
if (this.mode() === 'range') {
|
|
4739
|
+
return this.validateRange(value);
|
|
4740
|
+
}
|
|
4741
|
+
return this.validateSingle(value);
|
|
4742
|
+
}
|
|
4743
|
+
registerOnValidatorChange(fn) {
|
|
4744
|
+
this.onValidatorChange = fn;
|
|
4745
|
+
}
|
|
4746
|
+
// ── Private: Value Handling ──────────────────────────────────
|
|
4747
|
+
writeSingleValue(value) {
|
|
4748
|
+
const date = this.coerceToDate(value);
|
|
4749
|
+
this.selectedDate.set(date);
|
|
4750
|
+
if (date) {
|
|
4751
|
+
this.currentMonth.set(date.getMonth());
|
|
4752
|
+
this.currentYear.set(date.getFullYear());
|
|
4753
|
+
this.focusedDate.set(date);
|
|
4754
|
+
}
|
|
4755
|
+
else {
|
|
4756
|
+
this.focusedDate.set(null);
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
writeRangeValue(value) {
|
|
4760
|
+
if (!value || typeof value !== 'object') {
|
|
4761
|
+
this.rangeStart.set(null);
|
|
4762
|
+
this.rangeEnd.set(null);
|
|
4763
|
+
this.rangeSelecting.set(false);
|
|
3783
4764
|
return;
|
|
3784
|
-
|
|
3785
|
-
const
|
|
3786
|
-
this.
|
|
3787
|
-
this.
|
|
3788
|
-
this.
|
|
3789
|
-
this.
|
|
3790
|
-
|
|
4765
|
+
}
|
|
4766
|
+
const range = value;
|
|
4767
|
+
this.rangeStart.set(this.coerceToDate(range['start']));
|
|
4768
|
+
this.rangeEnd.set(this.coerceToDate(range['end']));
|
|
4769
|
+
this.rangeSelecting.set(false);
|
|
4770
|
+
const focus = this.rangeStart() ?? this.rangeEnd();
|
|
4771
|
+
if (focus) {
|
|
4772
|
+
this.currentMonth.set(focus.getMonth());
|
|
4773
|
+
this.currentYear.set(focus.getFullYear());
|
|
4774
|
+
}
|
|
3791
4775
|
}
|
|
3792
|
-
|
|
3793
|
-
this.
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
4776
|
+
emitSingleValue(date) {
|
|
4777
|
+
if (this.valueFormat() === 'iso') {
|
|
4778
|
+
this.onChange(date ? this.toIsoString(date) : null);
|
|
4779
|
+
}
|
|
4780
|
+
else {
|
|
4781
|
+
this.onChange(date);
|
|
4782
|
+
}
|
|
4783
|
+
this.onValidatorChange();
|
|
4784
|
+
}
|
|
4785
|
+
emitRangeValue(start, end) {
|
|
4786
|
+
if (this.valueFormat() === 'iso') {
|
|
4787
|
+
this.onChange({
|
|
4788
|
+
start: start ? this.toIsoString(start) : null,
|
|
4789
|
+
end: end ? this.toIsoString(end) : null,
|
|
4790
|
+
});
|
|
4791
|
+
}
|
|
4792
|
+
else {
|
|
4793
|
+
this.onChange({ start, end });
|
|
4794
|
+
}
|
|
4795
|
+
this.onValidatorChange();
|
|
4796
|
+
}
|
|
4797
|
+
// ── Private: Validation ──────────────────────────────────────
|
|
4798
|
+
validateSingle(value) {
|
|
4799
|
+
if (!value)
|
|
4800
|
+
return null;
|
|
4801
|
+
const date = this.coerceToDate(value);
|
|
4802
|
+
if (!date)
|
|
4803
|
+
return null;
|
|
4804
|
+
const min = this.parsedMin();
|
|
4805
|
+
const max = this.parsedMax();
|
|
4806
|
+
if (min && this.compareDays(date, min) < 0) {
|
|
4807
|
+
return { afDatepickerMin: { min, actual: date } };
|
|
4808
|
+
}
|
|
4809
|
+
if (max && this.compareDays(date, max) > 0) {
|
|
4810
|
+
return { afDatepickerMax: { max, actual: date } };
|
|
4811
|
+
}
|
|
4812
|
+
if (this.isDateUnavailable(date)) {
|
|
4813
|
+
return { afDatepickerFilter: true };
|
|
4814
|
+
}
|
|
4815
|
+
return null;
|
|
4816
|
+
}
|
|
4817
|
+
validateRange(value) {
|
|
4818
|
+
if (!value || typeof value !== 'object')
|
|
4819
|
+
return null;
|
|
4820
|
+
const range = value;
|
|
4821
|
+
const start = this.coerceToDate(range['start']);
|
|
4822
|
+
const end = this.coerceToDate(range['end']);
|
|
4823
|
+
const errors = {};
|
|
4824
|
+
const min = this.parsedMin();
|
|
4825
|
+
const max = this.parsedMax();
|
|
4826
|
+
if (start && min && this.compareDays(start, min) < 0) {
|
|
4827
|
+
errors['afDatepickerMin'] = { min, actual: start };
|
|
4828
|
+
}
|
|
4829
|
+
if (end && max && this.compareDays(end, max) > 0) {
|
|
4830
|
+
errors['afDatepickerMax'] = { max, actual: end };
|
|
4831
|
+
}
|
|
4832
|
+
if (start && end && this.compareDays(start, end) > 0) {
|
|
4833
|
+
errors['afDatepickerRange'] = { start, end };
|
|
4834
|
+
}
|
|
4835
|
+
return Object.keys(errors).length > 0 ? errors : null;
|
|
4836
|
+
}
|
|
4837
|
+
// ── Private: Calendar Building ───────────────────────────────
|
|
4838
|
+
buildDay(date, isCurrentMonth, today, selected, rangeStart, rangeEnd) {
|
|
4839
|
+
const isToday = this.isSameDay(date, today);
|
|
4840
|
+
const isSelected = this.mode() === 'single' && selected !== null ? this.isSameDay(date, selected) : false;
|
|
4841
|
+
const isDisabled = this.isDateDisabled(date);
|
|
4842
|
+
const isUnavailable = this.isDateUnavailable(date);
|
|
4843
|
+
let isInRange = false;
|
|
4844
|
+
let isRangeStart = false;
|
|
4845
|
+
let isRangeEnd = false;
|
|
4846
|
+
if (this.mode() === 'range' && rangeStart) {
|
|
4847
|
+
isRangeStart = this.isSameDay(date, rangeStart);
|
|
4848
|
+
if (rangeEnd) {
|
|
4849
|
+
const [lo, hi] = this.compareDays(rangeStart, rangeEnd) <= 0
|
|
4850
|
+
? [rangeStart, rangeEnd]
|
|
4851
|
+
: [rangeEnd, rangeStart];
|
|
4852
|
+
isRangeStart = this.isSameDay(date, lo);
|
|
4853
|
+
isRangeEnd = this.isSameDay(date, hi);
|
|
4854
|
+
isInRange =
|
|
4855
|
+
!isRangeStart &&
|
|
4856
|
+
!isRangeEnd &&
|
|
4857
|
+
this.compareDays(date, lo) > 0 &&
|
|
4858
|
+
this.compareDays(date, hi) < 0;
|
|
4859
|
+
}
|
|
3797
4860
|
}
|
|
4861
|
+
return {
|
|
4862
|
+
date,
|
|
4863
|
+
isCurrentMonth,
|
|
4864
|
+
isToday,
|
|
4865
|
+
isSelected,
|
|
4866
|
+
isDisabled,
|
|
4867
|
+
isUnavailable,
|
|
4868
|
+
isInRange,
|
|
4869
|
+
isRangeStart,
|
|
4870
|
+
isRangeEnd,
|
|
4871
|
+
};
|
|
4872
|
+
}
|
|
4873
|
+
// ── Private: Disability Checks ───────────────────────────────
|
|
4874
|
+
/** Checks whether a date falls outside min/max bounds */
|
|
4875
|
+
isDateDisabled(date) {
|
|
4876
|
+
const min = this.parsedMin();
|
|
4877
|
+
const max = this.parsedMax();
|
|
4878
|
+
if (min && this.compareDays(date, min) < 0)
|
|
4879
|
+
return true;
|
|
4880
|
+
if (max && this.compareDays(date, max) > 0)
|
|
4881
|
+
return true;
|
|
4882
|
+
return false;
|
|
4883
|
+
}
|
|
4884
|
+
/** Checks whether a date is explicitly unavailable (disabledDates or dateFilter) */
|
|
4885
|
+
isDateUnavailable(date) {
|
|
4886
|
+
const disabled = this.disabledDates();
|
|
4887
|
+
if (disabled.some((d) => this.isSameDay(d, date)))
|
|
4888
|
+
return true;
|
|
4889
|
+
const filter = this.dateFilter();
|
|
4890
|
+
if (filter && !filter(date))
|
|
4891
|
+
return true;
|
|
4892
|
+
return false;
|
|
4893
|
+
}
|
|
4894
|
+
isMonthDisabled(year, month, min, max) {
|
|
4895
|
+
if (min) {
|
|
4896
|
+
const minMonth = new Date(min.getFullYear(), min.getMonth(), 1);
|
|
4897
|
+
if (new Date(year, month + 1, 0) < minMonth)
|
|
4898
|
+
return true;
|
|
4899
|
+
}
|
|
4900
|
+
if (max) {
|
|
4901
|
+
const maxMonth = new Date(max.getFullYear(), max.getMonth() + 1, 0);
|
|
4902
|
+
if (new Date(year, month, 1) > maxMonth)
|
|
4903
|
+
return true;
|
|
4904
|
+
}
|
|
4905
|
+
return false;
|
|
3798
4906
|
}
|
|
4907
|
+
isYearDisabled(year, min, max) {
|
|
4908
|
+
if (min && year < min.getFullYear())
|
|
4909
|
+
return true;
|
|
4910
|
+
if (max && year > max.getFullYear())
|
|
4911
|
+
return true;
|
|
4912
|
+
return false;
|
|
4913
|
+
}
|
|
4914
|
+
// ── Private: Focus Management ────────────────────────────────
|
|
3799
4915
|
setFocusedDate(date) {
|
|
3800
4916
|
this.currentMonth.set(date.getMonth());
|
|
3801
4917
|
this.currentYear.set(date.getFullYear());
|
|
@@ -3810,6 +4926,41 @@ class AfDatepickerComponent {
|
|
|
3810
4926
|
const button = popover.querySelector(`.ct-datepicker__day[data-date="${key}"]`);
|
|
3811
4927
|
button?.focus();
|
|
3812
4928
|
}
|
|
4929
|
+
focusMonthButton(monthIndex) {
|
|
4930
|
+
const popover = this.popoverRef()?.nativeElement;
|
|
4931
|
+
if (!popover)
|
|
4932
|
+
return;
|
|
4933
|
+
const buttons = popover.querySelectorAll('.ct-datepicker__month');
|
|
4934
|
+
buttons[monthIndex]?.focus();
|
|
4935
|
+
}
|
|
4936
|
+
focusYearButton(year) {
|
|
4937
|
+
const popover = this.popoverRef()?.nativeElement;
|
|
4938
|
+
if (!popover)
|
|
4939
|
+
return;
|
|
4940
|
+
const buttons = popover.querySelectorAll('.ct-datepicker__year');
|
|
4941
|
+
const pageStart = this.yearPageStart();
|
|
4942
|
+
const index = year - pageStart;
|
|
4943
|
+
if (index >= 0 && index < buttons.length) {
|
|
4944
|
+
buttons[index]?.focus();
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
/** Finds the next non-disabled date in the given direction */
|
|
4948
|
+
findNextEnabledDate(from, step) {
|
|
4949
|
+
let next = this.addDays(from, step);
|
|
4950
|
+
let attempts = 0;
|
|
4951
|
+
const singleStep = step > 0 ? 1 : -1;
|
|
4952
|
+
while (this.isDateDisabled(next) && attempts < 365) {
|
|
4953
|
+
next = this.addDays(next, singleStep);
|
|
4954
|
+
attempts++;
|
|
4955
|
+
}
|
|
4956
|
+
return next;
|
|
4957
|
+
}
|
|
4958
|
+
// ── Private: Date Arithmetic ─────────────────────────────────
|
|
4959
|
+
shiftMonth(delta) {
|
|
4960
|
+
const base = this.focusedDate() ?? new Date(this.currentYear(), this.currentMonth(), 1);
|
|
4961
|
+
const next = this.addMonths(base, delta);
|
|
4962
|
+
this.setFocusedDate(next);
|
|
4963
|
+
}
|
|
3813
4964
|
addDays(date, delta) {
|
|
3814
4965
|
const next = new Date(date);
|
|
3815
4966
|
next.setDate(date.getDate() + delta);
|
|
@@ -3820,135 +4971,255 @@ class AfDatepickerComponent {
|
|
|
3820
4971
|
next.setMonth(date.getMonth() + delta);
|
|
3821
4972
|
return next;
|
|
3822
4973
|
}
|
|
4974
|
+
addYears(date, delta) {
|
|
4975
|
+
const next = new Date(date);
|
|
4976
|
+
next.setFullYear(date.getFullYear() + delta);
|
|
4977
|
+
return next;
|
|
4978
|
+
}
|
|
3823
4979
|
getWeekdayIndex(date) {
|
|
3824
4980
|
const day = date.getDay();
|
|
3825
4981
|
return day === 0 ? 6 : day - 1;
|
|
3826
4982
|
}
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
4983
|
+
// ── Private: Date Comparison & Parsing ───────────────────────
|
|
4984
|
+
isSameDay(a, b) {
|
|
4985
|
+
return (a.getFullYear() === b.getFullYear() &&
|
|
4986
|
+
a.getMonth() === b.getMonth() &&
|
|
4987
|
+
a.getDate() === b.getDate());
|
|
4988
|
+
}
|
|
4989
|
+
compareDays(a, b) {
|
|
4990
|
+
const da = new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime();
|
|
4991
|
+
const db = new Date(b.getFullYear(), b.getMonth(), b.getDate()).getTime();
|
|
4992
|
+
return da - db;
|
|
3831
4993
|
}
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
4994
|
+
parseDate(value) {
|
|
4995
|
+
if (!value)
|
|
4996
|
+
return null;
|
|
4997
|
+
if (value instanceof Date)
|
|
4998
|
+
return value;
|
|
4999
|
+
if (typeof value === 'string') {
|
|
5000
|
+
const parsed = new Date(value);
|
|
5001
|
+
return isNaN(parsed.getTime()) ? null : parsed;
|
|
5002
|
+
}
|
|
5003
|
+
return null;
|
|
5004
|
+
}
|
|
5005
|
+
coerceToDate(value) {
|
|
5006
|
+
if (!value)
|
|
5007
|
+
return null;
|
|
5008
|
+
if (value instanceof Date)
|
|
5009
|
+
return value;
|
|
5010
|
+
if (typeof value === 'string') {
|
|
5011
|
+
const parsed = new Date(value);
|
|
5012
|
+
return isNaN(parsed.getTime()) ? null : parsed;
|
|
5013
|
+
}
|
|
5014
|
+
return null;
|
|
5015
|
+
}
|
|
5016
|
+
toIsoString(date) {
|
|
5017
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
3836
5018
|
}
|
|
3837
5019
|
formatDate(date) {
|
|
3838
5020
|
const tokens = {
|
|
3839
5021
|
yyyy: String(date.getFullYear()),
|
|
3840
|
-
MMM:
|
|
5022
|
+
MMM: MONTH_SHORT[date.getMonth()],
|
|
3841
5023
|
MM: String(date.getMonth() + 1).padStart(2, '0'),
|
|
3842
5024
|
dd: String(date.getDate()).padStart(2, '0'),
|
|
3843
|
-
d: String(date.getDate())
|
|
5025
|
+
d: String(date.getDate()),
|
|
3844
5026
|
};
|
|
3845
|
-
return this.dateFormat().replace(/yyyy|MMM|MM|dd|d/g, token => tokens[token] ?? token);
|
|
3846
|
-
}
|
|
3847
|
-
// ControlValueAccessor implementation
|
|
3848
|
-
writeValue(value) {
|
|
3849
|
-
if (value) {
|
|
3850
|
-
this.selectedDate.set(value);
|
|
3851
|
-
this.currentMonth.set(value.getMonth());
|
|
3852
|
-
this.currentYear.set(value.getFullYear());
|
|
3853
|
-
this.focusedDate.set(value);
|
|
3854
|
-
}
|
|
3855
|
-
else {
|
|
3856
|
-
this.selectedDate.set(null);
|
|
3857
|
-
this.focusedDate.set(null);
|
|
3858
|
-
}
|
|
3859
|
-
}
|
|
3860
|
-
registerOnChange(fn) {
|
|
3861
|
-
this.onChange = fn;
|
|
3862
|
-
}
|
|
3863
|
-
registerOnTouched(fn) {
|
|
3864
|
-
this.onTouched = fn;
|
|
3865
|
-
}
|
|
3866
|
-
setDisabledState(isDisabled) {
|
|
3867
|
-
this.disabled.set(isDisabled);
|
|
5027
|
+
return this.dateFormat().replace(/yyyy|MMM|MM|dd|d/g, (token) => tokens[token] ?? token);
|
|
3868
5028
|
}
|
|
3869
5029
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfDatepickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3870
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfDatepickerComponent, isStandalone: true, selector: "af-datepicker", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dateFormat: { classPropertyName: "dateFormat", publicName: "dateFormat", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null } },
|
|
5030
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfDatepickerComponent, isStandalone: true, selector: "af-datepicker", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dateFormat: { classPropertyName: "dateFormat", publicName: "dateFormat", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", dateChange: "dateChange", rangeChange: "rangeChange" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, providers: [
|
|
3871
5031
|
{
|
|
3872
5032
|
provide: NG_VALUE_ACCESSOR,
|
|
3873
5033
|
useExisting: forwardRef(() => AfDatepickerComponent),
|
|
3874
|
-
multi: true
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
|
-
|
|
5034
|
+
multi: true,
|
|
5035
|
+
},
|
|
5036
|
+
{
|
|
5037
|
+
provide: NG_VALIDATORS,
|
|
5038
|
+
useExisting: forwardRef(() => AfDatepickerComponent),
|
|
5039
|
+
multi: true,
|
|
5040
|
+
},
|
|
5041
|
+
], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "popoverRef", first: true, predicate: ["popoverEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
5042
|
+
<div class="ct-field" [class.ct-field--error]="error()">
|
|
3878
5043
|
@if (label()) {
|
|
3879
|
-
<label class="ct-field__label" [attr.for]="inputId()">
|
|
5044
|
+
<label class="ct-field__label" [attr.for]="inputId()">
|
|
5045
|
+
{{ label() }}
|
|
5046
|
+
@if (required()) {
|
|
5047
|
+
<span aria-label="required"> *</span>
|
|
5048
|
+
}
|
|
5049
|
+
</label>
|
|
3880
5050
|
}
|
|
3881
5051
|
|
|
3882
5052
|
<div class="ct-datepicker" [attr.data-state]="isOpen() ? 'open' : 'closed'">
|
|
3883
|
-
<
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
5053
|
+
<div class="ct-datepicker__input-wrap">
|
|
5054
|
+
<input
|
|
5055
|
+
#inputEl
|
|
5056
|
+
class="ct-input"
|
|
5057
|
+
type="text"
|
|
5058
|
+
[id]="inputId()"
|
|
5059
|
+
[placeholder]="placeholder()"
|
|
5060
|
+
[value]="formattedValue()"
|
|
5061
|
+
[disabled]="disabled()"
|
|
5062
|
+
[required]="required()"
|
|
5063
|
+
[attr.aria-haspopup]="'dialog'"
|
|
5064
|
+
[attr.aria-expanded]="isOpen()"
|
|
5065
|
+
[attr.aria-controls]="popoverId()"
|
|
5066
|
+
[attr.aria-invalid]="error() ? true : null"
|
|
5067
|
+
[attr.aria-describedby]="ariaDescribedBy()"
|
|
5068
|
+
[attr.aria-label]="label() ? null : (placeholder() || 'Select date')"
|
|
5069
|
+
(click)="toggle()"
|
|
5070
|
+
(keydown)="onInputKeydown($event)"
|
|
5071
|
+
(blur)="onTouched()"
|
|
5072
|
+
readonly
|
|
5073
|
+
/>
|
|
5074
|
+
@if (hasClearableValue() && !disabled()) {
|
|
5075
|
+
<button
|
|
5076
|
+
class="ct-datepicker__clear"
|
|
5077
|
+
type="button"
|
|
5078
|
+
aria-label="Clear date"
|
|
5079
|
+
(click)="clearValue($event)">
|
|
5080
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
|
5081
|
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
5082
|
+
stroke-linejoin="round" aria-hidden="true">
|
|
5083
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
5084
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
5085
|
+
</svg>
|
|
5086
|
+
</button>
|
|
5087
|
+
}
|
|
5088
|
+
</div>
|
|
3900
5089
|
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
5090
|
+
<div
|
|
5091
|
+
#popoverEl
|
|
5092
|
+
class="ct-datepicker__popover"
|
|
5093
|
+
role="dialog"
|
|
5094
|
+
[id]="popoverId()"
|
|
5095
|
+
[attr.aria-label]="dialogAriaLabel()"
|
|
5096
|
+
[attr.aria-hidden]="!isOpen() || null">
|
|
5097
|
+
|
|
5098
|
+
<div class="ct-datepicker__header">
|
|
5099
|
+
<button
|
|
5100
|
+
class="ct-button ct-button--ghost ct-button--icon"
|
|
5101
|
+
type="button"
|
|
5102
|
+
[attr.aria-label]="prevButtonAriaLabel()"
|
|
5103
|
+
(click)="navigatePrevious()">
|
|
5104
|
+
‹
|
|
5105
|
+
</button>
|
|
5106
|
+
<button
|
|
5107
|
+
class="ct-datepicker__title"
|
|
5108
|
+
type="button"
|
|
5109
|
+
[attr.aria-label]="titleAriaLabel()"
|
|
5110
|
+
(click)="drillUp()">
|
|
5111
|
+
{{ headerTitle() }}
|
|
5112
|
+
</button>
|
|
5113
|
+
<button
|
|
5114
|
+
class="ct-button ct-button--ghost ct-button--icon"
|
|
5115
|
+
type="button"
|
|
5116
|
+
[attr.aria-label]="nextButtonAriaLabel()"
|
|
5117
|
+
(click)="navigateNext()">
|
|
5118
|
+
›
|
|
5119
|
+
</button>
|
|
5120
|
+
</div>
|
|
3927
5121
|
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
5122
|
+
@switch (currentView()) {
|
|
5123
|
+
@case ('days') {
|
|
5124
|
+
<div
|
|
5125
|
+
class="ct-datepicker__grid"
|
|
5126
|
+
role="grid"
|
|
5127
|
+
[attr.aria-label]="gridAriaLabel()"
|
|
5128
|
+
(keydown)="onDayGridKeydown($event)">
|
|
5129
|
+
<div class="ct-datepicker__row" role="row">
|
|
5130
|
+
@for (day of weekdayLabels; track $index) {
|
|
5131
|
+
<div class="ct-datepicker__weekday" role="columnheader" [attr.aria-label]="weekdayFullLabels[$index]">
|
|
5132
|
+
{{ day }}
|
|
5133
|
+
</div>
|
|
5134
|
+
}
|
|
5135
|
+
</div>
|
|
5136
|
+
@for (day of calendarDays(); track day.date.getTime()) {
|
|
5137
|
+
<button
|
|
5138
|
+
class="ct-datepicker__day"
|
|
5139
|
+
[attr.data-date]="getDateKey(day.date)"
|
|
5140
|
+
[attr.data-outside]="!day.isCurrentMonth || null"
|
|
5141
|
+
[attr.data-today]="day.isToday || null"
|
|
5142
|
+
[attr.data-unavailable]="day.isUnavailable || null"
|
|
5143
|
+
[attr.data-highlighted]="isDayHighlighted(day) || null"
|
|
5144
|
+
[attr.data-in-range]="day.isInRange || null"
|
|
5145
|
+
[attr.data-range-start]="day.isRangeStart || null"
|
|
5146
|
+
[attr.data-range-end]="day.isRangeEnd || null"
|
|
5147
|
+
[attr.aria-selected]="day.isSelected ? 'true' : null"
|
|
5148
|
+
[attr.aria-current]="day.isToday ? 'date' : null"
|
|
5149
|
+
[attr.aria-disabled]="day.isDisabled || day.isUnavailable ? 'true' : null"
|
|
5150
|
+
[disabled]="day.isDisabled || day.isUnavailable"
|
|
5151
|
+
[attr.tabindex]="getDayTabIndex(day)"
|
|
5152
|
+
role="gridcell"
|
|
5153
|
+
type="button"
|
|
5154
|
+
(click)="onDayClick(day)">
|
|
5155
|
+
{{ day.date.getDate() }}
|
|
5156
|
+
</button>
|
|
5157
|
+
}
|
|
5158
|
+
</div>
|
|
5159
|
+
<div class="ct-datepicker__footer">
|
|
3933
5160
|
<button
|
|
3934
|
-
class="ct-
|
|
3935
|
-
[attr.data-date]="getDateKey(day.date)"
|
|
3936
|
-
[attr.data-outside]="!day.isCurrentMonth"
|
|
3937
|
-
[attr.data-today]="day.isToday"
|
|
3938
|
-
[attr.aria-selected]="day.isSelected ? 'true' : null"
|
|
3939
|
-
[attr.aria-current]="day.isToday ? 'date' : null"
|
|
3940
|
-
[attr.tabindex]="getDayTabIndex(day)"
|
|
5161
|
+
class="ct-datepicker__today"
|
|
3941
5162
|
type="button"
|
|
3942
|
-
|
|
3943
|
-
|
|
5163
|
+
[disabled]="isTodayDisabled()"
|
|
5164
|
+
(click)="goToToday()">
|
|
5165
|
+
Today
|
|
3944
5166
|
</button>
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
5167
|
+
</div>
|
|
5168
|
+
}
|
|
5169
|
+
@case ('months') {
|
|
5170
|
+
<div
|
|
5171
|
+
class="ct-datepicker__month-grid"
|
|
5172
|
+
role="grid"
|
|
5173
|
+
aria-label="Select month"
|
|
5174
|
+
(keydown)="onMonthGridKeydown($event)">
|
|
5175
|
+
@for (m of monthItems(); track m.index) {
|
|
5176
|
+
<button
|
|
5177
|
+
class="ct-datepicker__month"
|
|
5178
|
+
[attr.aria-selected]="m.isSelected ? 'true' : null"
|
|
5179
|
+
[attr.data-highlighted]="isMonthHighlighted(m.index) || null"
|
|
5180
|
+
[disabled]="m.isDisabled"
|
|
5181
|
+
[attr.tabindex]="getMonthTabIndex(m.index)"
|
|
5182
|
+
role="gridcell"
|
|
5183
|
+
type="button"
|
|
5184
|
+
(click)="selectMonth(m.index)">
|
|
5185
|
+
{{ m.shortLabel }}
|
|
5186
|
+
</button>
|
|
5187
|
+
}
|
|
5188
|
+
</div>
|
|
5189
|
+
}
|
|
5190
|
+
@case ('years') {
|
|
5191
|
+
<div
|
|
5192
|
+
class="ct-datepicker__year-grid"
|
|
5193
|
+
role="grid"
|
|
5194
|
+
aria-label="Select year"
|
|
5195
|
+
(keydown)="onYearGridKeydown($event)">
|
|
5196
|
+
@for (y of yearItems(); track y.value) {
|
|
5197
|
+
<button
|
|
5198
|
+
class="ct-datepicker__year"
|
|
5199
|
+
[attr.aria-selected]="y.isSelected ? 'true' : null"
|
|
5200
|
+
[attr.data-highlighted]="isYearHighlighted(y.value) || null"
|
|
5201
|
+
[disabled]="y.isDisabled"
|
|
5202
|
+
[attr.tabindex]="getYearTabIndex(y.value)"
|
|
5203
|
+
role="gridcell"
|
|
5204
|
+
type="button"
|
|
5205
|
+
(click)="selectYear(y.value)">
|
|
5206
|
+
{{ y.value }}
|
|
5207
|
+
</button>
|
|
5208
|
+
}
|
|
5209
|
+
</div>
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
</div>
|
|
3949
5213
|
</div>
|
|
5214
|
+
|
|
5215
|
+
@if (hint() && !error()) {
|
|
5216
|
+
<div class="ct-field__hint" [id]="hintId()">{{ hint() }}</div>
|
|
5217
|
+
}
|
|
5218
|
+
@if (error()) {
|
|
5219
|
+
<div class="ct-field__error" [id]="errorId()">{{ error() }}</div>
|
|
5220
|
+
}
|
|
3950
5221
|
</div>
|
|
3951
|
-
`, isInline: true, styles: [":host{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5222
|
+
`, isInline: true, styles: [":host{display:block}.ct-datepicker__input-wrap{position:relative;display:flex;align-items:center}.ct-datepicker__input-wrap .ct-input{width:100%;padding-inline-end:2.25rem}.ct-datepicker__clear{position:absolute;inset-inline-end:.5rem;display:inline-flex;align-items:center;justify-content:center;border:none;background:transparent;cursor:pointer;color:var(--color-text-muted);padding:.25rem;border-radius:var(--radius-sm)}.ct-datepicker__clear:hover{color:var(--color-text-primary)}.ct-datepicker__clear:focus-visible{outline:var(--border-medium) solid var(--color-focus-ring);outline-offset:2px}.ct-datepicker__footer{display:flex;justify-content:center}.ct-datepicker__today{appearance:none;border:none;background:transparent;color:var(--color-brand-primary);cursor:pointer;font-size:var(--font-size-sm);padding:var(--space-1) var(--space-3);border-radius:var(--radius-sm);font-weight:var(--font-weight-medium, 500)}.ct-datepicker__today:hover:not(:disabled){background:var(--color-bg-muted)}.ct-datepicker__today:focus-visible{outline:var(--border-medium) solid var(--color-focus-ring);outline-offset:2px}.ct-datepicker__today:disabled{opacity:var(--opacity-disabled, .5);cursor:not-allowed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3952
5223
|
}
|
|
3953
5224
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfDatepickerComponent, decorators: [{
|
|
3954
5225
|
type: Component,
|
|
@@ -3956,88 +5227,198 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
3956
5227
|
{
|
|
3957
5228
|
provide: NG_VALUE_ACCESSOR,
|
|
3958
5229
|
useExisting: forwardRef(() => AfDatepickerComponent),
|
|
3959
|
-
multi: true
|
|
3960
|
-
}
|
|
5230
|
+
multi: true,
|
|
5231
|
+
},
|
|
5232
|
+
{
|
|
5233
|
+
provide: NG_VALIDATORS,
|
|
5234
|
+
useExisting: forwardRef(() => AfDatepickerComponent),
|
|
5235
|
+
multi: true,
|
|
5236
|
+
},
|
|
3961
5237
|
], host: {
|
|
3962
|
-
'(document:keydown.escape)': 'onEscape()',
|
|
3963
5238
|
'(document:click)': 'onDocumentClick($event)',
|
|
3964
5239
|
}, template: `
|
|
3965
|
-
<div class="ct-field">
|
|
5240
|
+
<div class="ct-field" [class.ct-field--error]="error()">
|
|
3966
5241
|
@if (label()) {
|
|
3967
|
-
<label class="ct-field__label" [attr.for]="inputId()">
|
|
5242
|
+
<label class="ct-field__label" [attr.for]="inputId()">
|
|
5243
|
+
{{ label() }}
|
|
5244
|
+
@if (required()) {
|
|
5245
|
+
<span aria-label="required"> *</span>
|
|
5246
|
+
}
|
|
5247
|
+
</label>
|
|
3968
5248
|
}
|
|
3969
5249
|
|
|
3970
5250
|
<div class="ct-datepicker" [attr.data-state]="isOpen() ? 'open' : 'closed'">
|
|
3971
|
-
<
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
5251
|
+
<div class="ct-datepicker__input-wrap">
|
|
5252
|
+
<input
|
|
5253
|
+
#inputEl
|
|
5254
|
+
class="ct-input"
|
|
5255
|
+
type="text"
|
|
5256
|
+
[id]="inputId()"
|
|
5257
|
+
[placeholder]="placeholder()"
|
|
5258
|
+
[value]="formattedValue()"
|
|
5259
|
+
[disabled]="disabled()"
|
|
5260
|
+
[required]="required()"
|
|
5261
|
+
[attr.aria-haspopup]="'dialog'"
|
|
5262
|
+
[attr.aria-expanded]="isOpen()"
|
|
5263
|
+
[attr.aria-controls]="popoverId()"
|
|
5264
|
+
[attr.aria-invalid]="error() ? true : null"
|
|
5265
|
+
[attr.aria-describedby]="ariaDescribedBy()"
|
|
5266
|
+
[attr.aria-label]="label() ? null : (placeholder() || 'Select date')"
|
|
5267
|
+
(click)="toggle()"
|
|
5268
|
+
(keydown)="onInputKeydown($event)"
|
|
5269
|
+
(blur)="onTouched()"
|
|
5270
|
+
readonly
|
|
5271
|
+
/>
|
|
5272
|
+
@if (hasClearableValue() && !disabled()) {
|
|
5273
|
+
<button
|
|
5274
|
+
class="ct-datepicker__clear"
|
|
5275
|
+
type="button"
|
|
5276
|
+
aria-label="Clear date"
|
|
5277
|
+
(click)="clearValue($event)">
|
|
5278
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
|
5279
|
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
5280
|
+
stroke-linejoin="round" aria-hidden="true">
|
|
5281
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
5282
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
5283
|
+
</svg>
|
|
5284
|
+
</button>
|
|
5285
|
+
}
|
|
5286
|
+
</div>
|
|
3988
5287
|
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
5288
|
+
<div
|
|
5289
|
+
#popoverEl
|
|
5290
|
+
class="ct-datepicker__popover"
|
|
5291
|
+
role="dialog"
|
|
5292
|
+
[id]="popoverId()"
|
|
5293
|
+
[attr.aria-label]="dialogAriaLabel()"
|
|
5294
|
+
[attr.aria-hidden]="!isOpen() || null">
|
|
5295
|
+
|
|
5296
|
+
<div class="ct-datepicker__header">
|
|
5297
|
+
<button
|
|
5298
|
+
class="ct-button ct-button--ghost ct-button--icon"
|
|
5299
|
+
type="button"
|
|
5300
|
+
[attr.aria-label]="prevButtonAriaLabel()"
|
|
5301
|
+
(click)="navigatePrevious()">
|
|
5302
|
+
‹
|
|
5303
|
+
</button>
|
|
5304
|
+
<button
|
|
5305
|
+
class="ct-datepicker__title"
|
|
5306
|
+
type="button"
|
|
5307
|
+
[attr.aria-label]="titleAriaLabel()"
|
|
5308
|
+
(click)="drillUp()">
|
|
5309
|
+
{{ headerTitle() }}
|
|
5310
|
+
</button>
|
|
5311
|
+
<button
|
|
5312
|
+
class="ct-button ct-button--ghost ct-button--icon"
|
|
5313
|
+
type="button"
|
|
5314
|
+
[attr.aria-label]="nextButtonAriaLabel()"
|
|
5315
|
+
(click)="navigateNext()">
|
|
5316
|
+
›
|
|
5317
|
+
</button>
|
|
5318
|
+
</div>
|
|
4015
5319
|
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
5320
|
+
@switch (currentView()) {
|
|
5321
|
+
@case ('days') {
|
|
5322
|
+
<div
|
|
5323
|
+
class="ct-datepicker__grid"
|
|
5324
|
+
role="grid"
|
|
5325
|
+
[attr.aria-label]="gridAriaLabel()"
|
|
5326
|
+
(keydown)="onDayGridKeydown($event)">
|
|
5327
|
+
<div class="ct-datepicker__row" role="row">
|
|
5328
|
+
@for (day of weekdayLabels; track $index) {
|
|
5329
|
+
<div class="ct-datepicker__weekday" role="columnheader" [attr.aria-label]="weekdayFullLabels[$index]">
|
|
5330
|
+
{{ day }}
|
|
5331
|
+
</div>
|
|
5332
|
+
}
|
|
5333
|
+
</div>
|
|
5334
|
+
@for (day of calendarDays(); track day.date.getTime()) {
|
|
5335
|
+
<button
|
|
5336
|
+
class="ct-datepicker__day"
|
|
5337
|
+
[attr.data-date]="getDateKey(day.date)"
|
|
5338
|
+
[attr.data-outside]="!day.isCurrentMonth || null"
|
|
5339
|
+
[attr.data-today]="day.isToday || null"
|
|
5340
|
+
[attr.data-unavailable]="day.isUnavailable || null"
|
|
5341
|
+
[attr.data-highlighted]="isDayHighlighted(day) || null"
|
|
5342
|
+
[attr.data-in-range]="day.isInRange || null"
|
|
5343
|
+
[attr.data-range-start]="day.isRangeStart || null"
|
|
5344
|
+
[attr.data-range-end]="day.isRangeEnd || null"
|
|
5345
|
+
[attr.aria-selected]="day.isSelected ? 'true' : null"
|
|
5346
|
+
[attr.aria-current]="day.isToday ? 'date' : null"
|
|
5347
|
+
[attr.aria-disabled]="day.isDisabled || day.isUnavailable ? 'true' : null"
|
|
5348
|
+
[disabled]="day.isDisabled || day.isUnavailable"
|
|
5349
|
+
[attr.tabindex]="getDayTabIndex(day)"
|
|
5350
|
+
role="gridcell"
|
|
5351
|
+
type="button"
|
|
5352
|
+
(click)="onDayClick(day)">
|
|
5353
|
+
{{ day.date.getDate() }}
|
|
5354
|
+
</button>
|
|
5355
|
+
}
|
|
5356
|
+
</div>
|
|
5357
|
+
<div class="ct-datepicker__footer">
|
|
4021
5358
|
<button
|
|
4022
|
-
class="ct-
|
|
4023
|
-
[attr.data-date]="getDateKey(day.date)"
|
|
4024
|
-
[attr.data-outside]="!day.isCurrentMonth"
|
|
4025
|
-
[attr.data-today]="day.isToday"
|
|
4026
|
-
[attr.aria-selected]="day.isSelected ? 'true' : null"
|
|
4027
|
-
[attr.aria-current]="day.isToday ? 'date' : null"
|
|
4028
|
-
[attr.tabindex]="getDayTabIndex(day)"
|
|
5359
|
+
class="ct-datepicker__today"
|
|
4029
5360
|
type="button"
|
|
4030
|
-
|
|
4031
|
-
|
|
5361
|
+
[disabled]="isTodayDisabled()"
|
|
5362
|
+
(click)="goToToday()">
|
|
5363
|
+
Today
|
|
4032
5364
|
</button>
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
5365
|
+
</div>
|
|
5366
|
+
}
|
|
5367
|
+
@case ('months') {
|
|
5368
|
+
<div
|
|
5369
|
+
class="ct-datepicker__month-grid"
|
|
5370
|
+
role="grid"
|
|
5371
|
+
aria-label="Select month"
|
|
5372
|
+
(keydown)="onMonthGridKeydown($event)">
|
|
5373
|
+
@for (m of monthItems(); track m.index) {
|
|
5374
|
+
<button
|
|
5375
|
+
class="ct-datepicker__month"
|
|
5376
|
+
[attr.aria-selected]="m.isSelected ? 'true' : null"
|
|
5377
|
+
[attr.data-highlighted]="isMonthHighlighted(m.index) || null"
|
|
5378
|
+
[disabled]="m.isDisabled"
|
|
5379
|
+
[attr.tabindex]="getMonthTabIndex(m.index)"
|
|
5380
|
+
role="gridcell"
|
|
5381
|
+
type="button"
|
|
5382
|
+
(click)="selectMonth(m.index)">
|
|
5383
|
+
{{ m.shortLabel }}
|
|
5384
|
+
</button>
|
|
5385
|
+
}
|
|
5386
|
+
</div>
|
|
5387
|
+
}
|
|
5388
|
+
@case ('years') {
|
|
5389
|
+
<div
|
|
5390
|
+
class="ct-datepicker__year-grid"
|
|
5391
|
+
role="grid"
|
|
5392
|
+
aria-label="Select year"
|
|
5393
|
+
(keydown)="onYearGridKeydown($event)">
|
|
5394
|
+
@for (y of yearItems(); track y.value) {
|
|
5395
|
+
<button
|
|
5396
|
+
class="ct-datepicker__year"
|
|
5397
|
+
[attr.aria-selected]="y.isSelected ? 'true' : null"
|
|
5398
|
+
[attr.data-highlighted]="isYearHighlighted(y.value) || null"
|
|
5399
|
+
[disabled]="y.isDisabled"
|
|
5400
|
+
[attr.tabindex]="getYearTabIndex(y.value)"
|
|
5401
|
+
role="gridcell"
|
|
5402
|
+
type="button"
|
|
5403
|
+
(click)="selectYear(y.value)">
|
|
5404
|
+
{{ y.value }}
|
|
5405
|
+
</button>
|
|
5406
|
+
}
|
|
5407
|
+
</div>
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
</div>
|
|
4037
5411
|
</div>
|
|
5412
|
+
|
|
5413
|
+
@if (hint() && !error()) {
|
|
5414
|
+
<div class="ct-field__hint" [id]="hintId()">{{ hint() }}</div>
|
|
5415
|
+
}
|
|
5416
|
+
@if (error()) {
|
|
5417
|
+
<div class="ct-field__error" [id]="errorId()">{{ error() }}</div>
|
|
5418
|
+
}
|
|
4038
5419
|
</div>
|
|
4039
|
-
`, styles: [":host{display:block}\n"] }]
|
|
4040
|
-
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], dateFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFormat", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], dateChange: [{ type: i0.Output, args: ["dateChange"] }], inputRef: [{ type: i0.ViewChild, args: ['
|
|
5420
|
+
`, styles: [":host{display:block}.ct-datepicker__input-wrap{position:relative;display:flex;align-items:center}.ct-datepicker__input-wrap .ct-input{width:100%;padding-inline-end:2.25rem}.ct-datepicker__clear{position:absolute;inset-inline-end:.5rem;display:inline-flex;align-items:center;justify-content:center;border:none;background:transparent;cursor:pointer;color:var(--color-text-muted);padding:.25rem;border-radius:var(--radius-sm)}.ct-datepicker__clear:hover{color:var(--color-text-primary)}.ct-datepicker__clear:focus-visible{outline:var(--border-medium) solid var(--color-focus-ring);outline-offset:2px}.ct-datepicker__footer{display:flex;justify-content:center}.ct-datepicker__today{appearance:none;border:none;background:transparent;color:var(--color-brand-primary);cursor:pointer;font-size:var(--font-size-sm);padding:var(--space-1) var(--space-3);border-radius:var(--radius-sm);font-weight:var(--font-weight-medium, 500)}.ct-datepicker__today:hover:not(:disabled){background:var(--color-bg-muted)}.ct-datepicker__today:focus-visible{outline:var(--border-medium) solid var(--color-focus-ring);outline-offset:2px}.ct-datepicker__today:disabled{opacity:var(--opacity-disabled, .5);cursor:not-allowed}\n"] }]
|
|
5421
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], dateFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFormat", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], dateChange: [{ type: i0.Output, args: ["dateChange"] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], inputRef: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], popoverRef: [{ type: i0.ViewChild, args: ['popoverEl', { isSignal: true }] }] } });
|
|
4041
5422
|
|
|
4042
5423
|
/**
|
|
4043
5424
|
* Chip component for labels, tags, statuses, and interactive filters.
|
|
@@ -4648,21 +6029,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
4648
6029
|
}], ctorParameters: () => [], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "afTooltip", required: false }] }], afTooltipPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "afTooltipPosition", required: false }] }], afTooltipDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "afTooltipDelay", required: false }] }], afTooltipDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "afTooltipDisabled", required: false }] }] } });
|
|
4649
6030
|
|
|
4650
6031
|
/**
|
|
4651
|
-
* Badge component for status indicators
|
|
6032
|
+
* Badge component for status indicators, labels, and counts.
|
|
4652
6033
|
*
|
|
4653
|
-
*
|
|
4654
|
-
*
|
|
4655
|
-
*
|
|
6034
|
+
* Wraps the Construct Design System `ct-badge` with semantic color
|
|
6035
|
+
* variants and optional icon or dot indicators.
|
|
6036
|
+
*
|
|
6037
|
+
* @example Basic usage
|
|
6038
|
+
* <af-badge variant="success">Approved</af-badge>
|
|
6039
|
+
*
|
|
6040
|
+
* @example With icon
|
|
6041
|
+
* <af-badge variant="danger" icon="+">Blocked</af-badge>
|
|
6042
|
+
*
|
|
6043
|
+
* @example With dot indicator
|
|
6044
|
+
* <af-badge variant="info" dot>Online</af-badge>
|
|
6045
|
+
*
|
|
6046
|
+
* @example Status badge for screen readers
|
|
6047
|
+
* <af-badge variant="warning" role="status" ariaLabel="Build status: failing">
|
|
6048
|
+
* Failing
|
|
6049
|
+
* </af-badge>
|
|
6050
|
+
*
|
|
6051
|
+
* @accessibility
|
|
6052
|
+
* - Non-interactive element — no keyboard navigation required.
|
|
6053
|
+
* - Decorative elements (icon, dot) are hidden from screen readers via `aria-hidden`.
|
|
6054
|
+
* - Use `ariaLabel` when the badge has no visible text or when the visual content
|
|
6055
|
+
* alone does not convey the full meaning.
|
|
6056
|
+
* - Set `role="status"` when the badge reflects a live value that screen readers
|
|
6057
|
+
* should announce on change.
|
|
4656
6058
|
*/
|
|
4657
6059
|
class AfBadgeComponent {
|
|
6060
|
+
/** ARIA role, e.g. `"status"` for live status badges. */
|
|
6061
|
+
role = input('', ...(ngDevMode ? [{ debugName: "role" }] : []));
|
|
4658
6062
|
/** Accessible label, useful when the badge has no visible text. */
|
|
4659
6063
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
4660
|
-
/**
|
|
6064
|
+
/** Semantic color variant. */
|
|
4661
6065
|
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
4662
|
-
/** Icon character to display */
|
|
6066
|
+
/** Icon character to display before the label. */
|
|
4663
6067
|
icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : []));
|
|
4664
|
-
/** Show a dot indicator instead of icon */
|
|
4665
|
-
dot = input(false, ...(ngDevMode ?
|
|
6068
|
+
/** Show a dot indicator instead of an icon. */
|
|
6069
|
+
dot = input(false, { ...(ngDevMode ? { debugName: "dot" } : {}), transform: booleanAttribute });
|
|
6070
|
+
/** Computed CSS classes combining base class and variant/icon modifiers. */
|
|
4666
6071
|
badgeClasses = computed(() => {
|
|
4667
6072
|
const classes = ['ct-badge'];
|
|
4668
6073
|
if (this.variant() !== 'default') {
|
|
@@ -4674,32 +6079,91 @@ class AfBadgeComponent {
|
|
|
4674
6079
|
return classes.join(' ');
|
|
4675
6080
|
}, ...(ngDevMode ? [{ debugName: "badgeClasses" }] : []));
|
|
4676
6081
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4677
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfBadgeComponent, isStandalone: true, selector: "af-badge", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
</span>
|
|
4687
|
-
`, isInline: true, styles: [":host{display:inline-block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6082
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfBadgeComponent, isStandalone: true, selector: "af-badge", inputs: { role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "badgeClasses()", "attr.role": "role() || null", "attr.aria-label": "ariaLabel() || null" } }, ngImport: i0, template: `
|
|
6083
|
+
@if (icon()) {
|
|
6084
|
+
<span class="ct-badge__icon" aria-hidden="true">{{ icon() }}</span>
|
|
6085
|
+
}
|
|
6086
|
+
@if (dot()) {
|
|
6087
|
+
<span class="ct-badge__dot" aria-hidden="true"></span>
|
|
6088
|
+
}
|
|
6089
|
+
<ng-content />
|
|
6090
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4688
6091
|
}
|
|
4689
6092
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfBadgeComponent, decorators: [{
|
|
4690
6093
|
type: Component,
|
|
4691
|
-
args: [{
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
6094
|
+
args: [{
|
|
6095
|
+
selector: 'af-badge',
|
|
6096
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6097
|
+
host: {
|
|
6098
|
+
'[class]': 'badgeClasses()',
|
|
6099
|
+
'[attr.role]': 'role() || null',
|
|
6100
|
+
'[attr.aria-label]': 'ariaLabel() || null',
|
|
6101
|
+
},
|
|
6102
|
+
template: `
|
|
6103
|
+
@if (icon()) {
|
|
6104
|
+
<span class="ct-badge__icon" aria-hidden="true">{{ icon() }}</span>
|
|
6105
|
+
}
|
|
6106
|
+
@if (dot()) {
|
|
6107
|
+
<span class="ct-badge__dot" aria-hidden="true"></span>
|
|
6108
|
+
}
|
|
6109
|
+
<ng-content />
|
|
6110
|
+
`,
|
|
6111
|
+
}]
|
|
6112
|
+
}], propDecorators: { role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }] } });
|
|
6113
|
+
|
|
6114
|
+
/**
|
|
6115
|
+
* Test harness for AfBadgeComponent.
|
|
6116
|
+
*
|
|
6117
|
+
* Provides a semantic API for querying badge state in tests,
|
|
6118
|
+
* abstracting DOM details behind readable method names.
|
|
6119
|
+
*
|
|
6120
|
+
* @example
|
|
6121
|
+
* const harness = new AfBadgeHarness(fixture.nativeElement);
|
|
6122
|
+
* expect(harness.getText()).toBe('Approved');
|
|
6123
|
+
* expect(harness.hasClass('ct-badge--success')).toBe(true);
|
|
6124
|
+
*/
|
|
6125
|
+
class AfBadgeHarness {
|
|
6126
|
+
hostEl;
|
|
6127
|
+
constructor(container) {
|
|
6128
|
+
const el = container.querySelector('af-badge');
|
|
6129
|
+
if (!el) {
|
|
6130
|
+
throw new Error('AfBadgeHarness: af-badge element not found in container.');
|
|
6131
|
+
}
|
|
6132
|
+
this.hostEl = el;
|
|
6133
|
+
}
|
|
6134
|
+
/** Returns the trimmed text content of the badge (projected content only). */
|
|
6135
|
+
getText() {
|
|
6136
|
+
return this.hostEl.textContent?.trim() ?? '';
|
|
6137
|
+
}
|
|
6138
|
+
/** Returns the full `class` attribute string of the host element. */
|
|
6139
|
+
getClasses() {
|
|
6140
|
+
return this.hostEl.className;
|
|
6141
|
+
}
|
|
6142
|
+
/** Returns whether the host element has the given CSS class. */
|
|
6143
|
+
hasClass(className) {
|
|
6144
|
+
return this.hostEl.classList.contains(className);
|
|
6145
|
+
}
|
|
6146
|
+
/** Returns the `aria-label` attribute value, or `null` if absent. */
|
|
6147
|
+
getAriaLabel() {
|
|
6148
|
+
return this.hostEl.getAttribute('aria-label');
|
|
6149
|
+
}
|
|
6150
|
+
/** Returns the `role` attribute value, or `null` if absent. */
|
|
6151
|
+
getRole() {
|
|
6152
|
+
return this.hostEl.getAttribute('role');
|
|
6153
|
+
}
|
|
6154
|
+
/** Returns whether the badge contains an icon element. */
|
|
6155
|
+
hasIcon() {
|
|
6156
|
+
return this.hostEl.querySelector('.ct-badge__icon') !== null;
|
|
6157
|
+
}
|
|
6158
|
+
/** Returns whether the badge contains a dot indicator. */
|
|
6159
|
+
hasDot() {
|
|
6160
|
+
return this.hostEl.querySelector('.ct-badge__dot') !== null;
|
|
6161
|
+
}
|
|
6162
|
+
/** Returns the text content of the icon element, or empty string if absent. */
|
|
6163
|
+
getIconText() {
|
|
6164
|
+
return this.hostEl.querySelector('.ct-badge__icon')?.textContent?.trim() ?? '';
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
4703
6167
|
|
|
4704
6168
|
/**
|
|
4705
6169
|
* Progress bar for showing completion state
|
|
@@ -6366,7 +7830,7 @@ class AfFileUploadComponent {
|
|
|
6366
7830
|
{{ liveAnnouncement() }}
|
|
6367
7831
|
</span>
|
|
6368
7832
|
</div>
|
|
6369
|
-
`, isInline: true, styles: [":host{display:block}.af-file-upload__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: AfButtonComponent, selector: "af-button", inputs: ["variant", "size", "type", "disabled", "iconOnly", "ariaLabel", "title"], outputs: ["clicked"] }, { kind: "component", type: AfBadgeComponent, selector: "af-badge", inputs: ["ariaLabel", "variant", "icon", "dot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7833
|
+
`, isInline: true, styles: [":host{display:block}.af-file-upload__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: AfButtonComponent, selector: "af-button", inputs: ["variant", "size", "type", "disabled", "iconOnly", "ariaLabel", "title"], outputs: ["clicked"] }, { kind: "component", type: AfBadgeComponent, selector: "af-badge", inputs: ["role", "ariaLabel", "variant", "icon", "dot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6370
7834
|
}
|
|
6371
7835
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfFileUploadComponent, decorators: [{
|
|
6372
7836
|
type: Component,
|
|
@@ -6465,6 +7929,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
6465
7929
|
`, styles: [":host{display:block}.af-file-upload__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
6466
7930
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], validationErrors: [{ type: i0.Output, args: ["validationErrors"] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }] } });
|
|
6467
7931
|
|
|
7932
|
+
/**
|
|
7933
|
+
* Injection token to override select-menu screen-reader announcements
|
|
7934
|
+
* and the fallback `aria-label`.
|
|
7935
|
+
*
|
|
7936
|
+
* @example
|
|
7937
|
+
* providers: [{
|
|
7938
|
+
* provide: AF_SELECT_MENU_I18N,
|
|
7939
|
+
* useValue: {
|
|
7940
|
+
* selectOption: 'Option auswählen',
|
|
7941
|
+
* opened: '{count} Optionen verfügbar',
|
|
7942
|
+
* closed: 'Auswahl geschlossen',
|
|
7943
|
+
* selected: '{label} ausgewählt',
|
|
7944
|
+
* deselected: '{label} abgewählt',
|
|
7945
|
+
* countSelected: '{count} Optionen ausgewählt',
|
|
7946
|
+
* },
|
|
7947
|
+
* }]
|
|
7948
|
+
*/
|
|
7949
|
+
const AF_SELECT_MENU_I18N = new InjectionToken('AfSelectMenuI18n', {
|
|
7950
|
+
factory: () => ({
|
|
7951
|
+
selectOption: 'Select option',
|
|
7952
|
+
opened: '{count} options available',
|
|
7953
|
+
closed: 'Selection closed',
|
|
7954
|
+
selected: '{label} selected',
|
|
7955
|
+
deselected: '{label} deselected',
|
|
7956
|
+
countSelected: '{count} options selected',
|
|
7957
|
+
}),
|
|
7958
|
+
});
|
|
7959
|
+
|
|
6468
7960
|
/**
|
|
6469
7961
|
* Custom dropdown select component with keyboard navigation
|
|
6470
7962
|
* and full ARIA listbox pattern for single and multi-select.
|
|
@@ -6485,9 +7977,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
6485
7977
|
* [multiple]="true"
|
|
6486
7978
|
* [formControl]="rolesControl">
|
|
6487
7979
|
* </af-select-menu>
|
|
7980
|
+
*
|
|
7981
|
+
* @accessibility
|
|
7982
|
+
* - Implements the WAI-ARIA Listbox pattern with a combobox trigger.
|
|
7983
|
+
* - Keyboard: ArrowDown/Up to move highlight, Enter/Space to select,
|
|
7984
|
+
* Escape to close, Home/End to jump, Tab to select-and-close (single) or close (multi).
|
|
7985
|
+
* - Focus stays on the combobox trigger; `aria-activedescendant` tracks the highlighted option.
|
|
7986
|
+
* - Screen-reader announcements via {@link AriaLiveAnnouncer} for open/close/selection changes.
|
|
7987
|
+
* - All user-facing strings are configurable via {@link AF_SELECT_MENU_I18N} for i18n.
|
|
7988
|
+
* - Uses CSS logical properties for RTL layout support.
|
|
7989
|
+
* - `aria-describedby` links to hint or error text; `aria-invalid` is set on error state.
|
|
7990
|
+
* - Disabled options are marked with `aria-disabled` and skipped during keyboard navigation.
|
|
6488
7991
|
*/
|
|
6489
7992
|
class AfSelectMenuComponent {
|
|
6490
7993
|
static nextId = 0;
|
|
7994
|
+
i18n = inject(AF_SELECT_MENU_I18N);
|
|
7995
|
+
announcer = inject(AriaLiveAnnouncer);
|
|
6491
7996
|
/** Label shown above the select */
|
|
6492
7997
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
6493
7998
|
/** Placeholder text when nothing is selected */
|
|
@@ -6605,6 +8110,8 @@ class AfSelectMenuComponent {
|
|
|
6605
8110
|
this.isOpen.set(true);
|
|
6606
8111
|
this.highlightedIndex.set(this.findSelectedOrFirstIndex());
|
|
6607
8112
|
this.scrollHighlightedIntoView();
|
|
8113
|
+
const enabledCount = this.options().filter((o) => !o.disabled).length;
|
|
8114
|
+
this.announcer.announce(this.i18n.opened.replace('{count}', String(enabledCount)));
|
|
6608
8115
|
}
|
|
6609
8116
|
/** Closes the listbox */
|
|
6610
8117
|
closeListbox() {
|
|
@@ -6612,6 +8119,7 @@ class AfSelectMenuComponent {
|
|
|
6612
8119
|
return;
|
|
6613
8120
|
this.isOpen.set(false);
|
|
6614
8121
|
this.highlightedIndex.set(-1);
|
|
8122
|
+
this.announcer.announce(this.i18n.closed);
|
|
6615
8123
|
}
|
|
6616
8124
|
/** Selects or toggles an option */
|
|
6617
8125
|
selectOption(option) {
|
|
@@ -6621,15 +8129,21 @@ class AfSelectMenuComponent {
|
|
|
6621
8129
|
if (this.multiple()) {
|
|
6622
8130
|
const current = this.value() ?? [];
|
|
6623
8131
|
const idx = current.findIndex((v) => cmp(v, option.value));
|
|
6624
|
-
const
|
|
8132
|
+
const wasSelected = idx >= 0;
|
|
8133
|
+
const next = wasSelected
|
|
6625
8134
|
? [...current.slice(0, idx), ...current.slice(idx + 1)]
|
|
6626
8135
|
: [...current, option.value];
|
|
6627
8136
|
this.value.set(next);
|
|
6628
8137
|
this.onChange(next);
|
|
8138
|
+
const msg = wasSelected
|
|
8139
|
+
? this.i18n.deselected.replace('{label}', option.label)
|
|
8140
|
+
: this.i18n.selected.replace('{label}', option.label);
|
|
8141
|
+
this.announcer.announce(`${msg}, ${this.i18n.countSelected.replace('{count}', String(next.length))}`);
|
|
6629
8142
|
}
|
|
6630
8143
|
else {
|
|
6631
8144
|
this.value.set(option.value);
|
|
6632
8145
|
this.onChange(option.value);
|
|
8146
|
+
this.announcer.announce(this.i18n.selected.replace('{label}', option.label));
|
|
6633
8147
|
this.closeListbox();
|
|
6634
8148
|
this.triggerRef()?.nativeElement.focus();
|
|
6635
8149
|
}
|
|
@@ -6821,7 +8335,7 @@ class AfSelectMenuComponent {
|
|
|
6821
8335
|
aria-haspopup="listbox"
|
|
6822
8336
|
[attr.aria-controls]="listboxId"
|
|
6823
8337
|
[attr.aria-labelledby]="label() ? labelId : null"
|
|
6824
|
-
[attr.aria-label]="label() ? null :
|
|
8338
|
+
[attr.aria-label]="label() ? null : i18n.selectOption"
|
|
6825
8339
|
[attr.aria-activedescendant]="activeDescendantId()"
|
|
6826
8340
|
[attr.aria-invalid]="error() ? true : null"
|
|
6827
8341
|
[attr.aria-describedby]="getAriaDescribedBy()"
|
|
@@ -6928,7 +8442,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
6928
8442
|
aria-haspopup="listbox"
|
|
6929
8443
|
[attr.aria-controls]="listboxId"
|
|
6930
8444
|
[attr.aria-labelledby]="label() ? labelId : null"
|
|
6931
|
-
[attr.aria-label]="label() ? null :
|
|
8445
|
+
[attr.aria-label]="label() ? null : i18n.selectOption"
|
|
6932
8446
|
[attr.aria-activedescendant]="activeDescendantId()"
|
|
6933
8447
|
[attr.aria-invalid]="error() ? true : null"
|
|
6934
8448
|
[attr.aria-describedby]="getAriaDescribedBy()"
|
|
@@ -7002,6 +8516,141 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
7002
8516
|
`, styles: [":host{display:block}\n"] }]
|
|
7003
8517
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], componentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "componentId", required: false }] }], triggerRef: [{ type: i0.ViewChild, args: ['triggerEl', { isSignal: true }] }], listboxRef: [{ type: i0.ViewChild, args: ['listboxEl', { isSignal: true }] }], menuRef: [{ type: i0.ViewChild, args: ['menuEl', { isSignal: true }] }] } });
|
|
7004
8518
|
|
|
8519
|
+
/**
|
|
8520
|
+
* Test harness for AfSelectMenuComponent.
|
|
8521
|
+
*
|
|
8522
|
+
* Provides a semantic API for interacting with the select-menu in tests,
|
|
8523
|
+
* abstracting DOM details behind readable method names.
|
|
8524
|
+
*
|
|
8525
|
+
* @example
|
|
8526
|
+
* const harness = new AfSelectMenuHarness(fixture.nativeElement);
|
|
8527
|
+
* harness.click();
|
|
8528
|
+
* expect(harness.isOpen()).toBe(true);
|
|
8529
|
+
* expect(harness.getOptionCount()).toBe(4);
|
|
8530
|
+
* harness.clickOption(1);
|
|
8531
|
+
* expect(harness.getTriggerText()).toBe('Banana');
|
|
8532
|
+
*/
|
|
8533
|
+
class AfSelectMenuHarness {
|
|
8534
|
+
hostEl;
|
|
8535
|
+
constructor(container) {
|
|
8536
|
+
const el = container.querySelector('af-select-menu');
|
|
8537
|
+
if (!el) {
|
|
8538
|
+
throw new Error('AfSelectMenuHarness: af-select-menu element not found in container.');
|
|
8539
|
+
}
|
|
8540
|
+
this.hostEl = el;
|
|
8541
|
+
}
|
|
8542
|
+
/** Returns the trigger `<button>` element. */
|
|
8543
|
+
getTriggerElement() {
|
|
8544
|
+
const btn = this.hostEl.querySelector('.ct-select-menu__trigger');
|
|
8545
|
+
if (!btn) {
|
|
8546
|
+
throw new Error('AfSelectMenuHarness: trigger button not found.');
|
|
8547
|
+
}
|
|
8548
|
+
return btn;
|
|
8549
|
+
}
|
|
8550
|
+
/** Returns the trimmed display text of the trigger. */
|
|
8551
|
+
getTriggerText() {
|
|
8552
|
+
const value = this.hostEl.querySelector('.ct-select-menu__value');
|
|
8553
|
+
return value?.textContent?.trim() ?? '';
|
|
8554
|
+
}
|
|
8555
|
+
/** Clicks the trigger button. */
|
|
8556
|
+
click() {
|
|
8557
|
+
this.getTriggerElement().click();
|
|
8558
|
+
}
|
|
8559
|
+
/** Returns whether the trigger is disabled. */
|
|
8560
|
+
isDisabled() {
|
|
8561
|
+
return this.getTriggerElement().disabled;
|
|
8562
|
+
}
|
|
8563
|
+
/** Returns whether the listbox is currently open. */
|
|
8564
|
+
isOpen() {
|
|
8565
|
+
const menu = this.hostEl.querySelector('.ct-select-menu');
|
|
8566
|
+
return menu?.getAttribute('data-state') === 'open';
|
|
8567
|
+
}
|
|
8568
|
+
/** Returns the `aria-expanded` attribute value. */
|
|
8569
|
+
getAriaExpanded() {
|
|
8570
|
+
return this.getTriggerElement().getAttribute('aria-expanded');
|
|
8571
|
+
}
|
|
8572
|
+
/** Returns the `aria-label` attribute value. */
|
|
8573
|
+
getAriaLabel() {
|
|
8574
|
+
return this.getTriggerElement().getAttribute('aria-label');
|
|
8575
|
+
}
|
|
8576
|
+
/** Returns the `aria-labelledby` attribute value. */
|
|
8577
|
+
getAriaLabelledBy() {
|
|
8578
|
+
return this.getTriggerElement().getAttribute('aria-labelledby');
|
|
8579
|
+
}
|
|
8580
|
+
/** Returns the `aria-activedescendant` attribute value. */
|
|
8581
|
+
getAriaActiveDescendant() {
|
|
8582
|
+
return this.getTriggerElement().getAttribute('aria-activedescendant');
|
|
8583
|
+
}
|
|
8584
|
+
/** Returns the `aria-invalid` attribute value. */
|
|
8585
|
+
getAriaInvalid() {
|
|
8586
|
+
return this.getTriggerElement().getAttribute('aria-invalid');
|
|
8587
|
+
}
|
|
8588
|
+
/** Returns the `aria-required` attribute value. */
|
|
8589
|
+
getAriaRequired() {
|
|
8590
|
+
return this.getTriggerElement().getAttribute('aria-required');
|
|
8591
|
+
}
|
|
8592
|
+
/** Returns the `aria-describedby` attribute value. */
|
|
8593
|
+
getAriaDescribedBy() {
|
|
8594
|
+
return this.getTriggerElement().getAttribute('aria-describedby');
|
|
8595
|
+
}
|
|
8596
|
+
/** Returns all option elements inside the listbox. */
|
|
8597
|
+
getOptions() {
|
|
8598
|
+
return Array.from(this.hostEl.querySelectorAll('[role="option"]'));
|
|
8599
|
+
}
|
|
8600
|
+
/** Returns the trimmed text of the option at the given index. */
|
|
8601
|
+
getOptionText(index) {
|
|
8602
|
+
const options = this.getOptions();
|
|
8603
|
+
if (index < 0 || index >= options.length) {
|
|
8604
|
+
throw new Error(`AfSelectMenuHarness: option index ${index} out of bounds (${options.length} options).`);
|
|
8605
|
+
}
|
|
8606
|
+
return options[index].textContent?.trim() ?? '';
|
|
8607
|
+
}
|
|
8608
|
+
/** Returns the number of options. */
|
|
8609
|
+
getOptionCount() {
|
|
8610
|
+
return this.getOptions().length;
|
|
8611
|
+
}
|
|
8612
|
+
/** Returns whether the option at the given index is selected. */
|
|
8613
|
+
isOptionSelected(index) {
|
|
8614
|
+
return this.getOptions()[index]?.getAttribute('aria-selected') === 'true';
|
|
8615
|
+
}
|
|
8616
|
+
/** Returns whether the option at the given index is disabled. */
|
|
8617
|
+
isOptionDisabled(index) {
|
|
8618
|
+
return this.getOptions()[index]?.getAttribute('aria-disabled') === 'true';
|
|
8619
|
+
}
|
|
8620
|
+
/** Returns whether the option at the given index is highlighted. */
|
|
8621
|
+
isOptionHighlighted(index) {
|
|
8622
|
+
return this.getOptions()[index]?.hasAttribute('data-highlighted') ?? false;
|
|
8623
|
+
}
|
|
8624
|
+
/** Clicks the option at the given index. */
|
|
8625
|
+
clickOption(index) {
|
|
8626
|
+
const options = this.getOptions();
|
|
8627
|
+
if (index < 0 || index >= options.length) {
|
|
8628
|
+
throw new Error(`AfSelectMenuHarness: option index ${index} out of bounds (${options.length} options).`);
|
|
8629
|
+
}
|
|
8630
|
+
options[index].click();
|
|
8631
|
+
}
|
|
8632
|
+
/** Returns the trimmed label text, or empty string if no label. */
|
|
8633
|
+
getLabelText() {
|
|
8634
|
+
const label = this.hostEl.querySelector('.ct-field__label');
|
|
8635
|
+
return label?.textContent?.trim() ?? '';
|
|
8636
|
+
}
|
|
8637
|
+
/** Returns the trimmed hint text, or empty string if no hint. */
|
|
8638
|
+
getHintText() {
|
|
8639
|
+
const hint = this.hostEl.querySelector('.ct-field__hint');
|
|
8640
|
+
return hint?.textContent?.trim() ?? '';
|
|
8641
|
+
}
|
|
8642
|
+
/** Returns the trimmed error text, or empty string if no error. */
|
|
8643
|
+
getErrorText() {
|
|
8644
|
+
const error = this.hostEl.querySelector('.ct-field__error');
|
|
8645
|
+
return error?.textContent?.trim() ?? '';
|
|
8646
|
+
}
|
|
8647
|
+
/** Returns whether the select-menu wrapper has the given CSS class. */
|
|
8648
|
+
hasClass(className) {
|
|
8649
|
+
const menu = this.hostEl.querySelector('.ct-select-menu');
|
|
8650
|
+
return menu?.classList.contains(className) ?? false;
|
|
8651
|
+
}
|
|
8652
|
+
}
|
|
8653
|
+
|
|
7005
8654
|
let nextId = 0;
|
|
7006
8655
|
/**
|
|
7007
8656
|
* Individual navigation item used within af-navbar or af-toolbar.
|
|
@@ -8483,5 +10132,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
8483
10132
|
* Generated bundle index. Do not edit.
|
|
8484
10133
|
*/
|
|
8485
10134
|
|
|
8486
|
-
export { AfAccordionComponent, AfAccordionItemComponent, AfAlertComponent, AfAppShellComponent, AfAppShellPageHeaderComponent, AfAppShellV2Component, AfAppShellV2ToolbarComponent, AfAvatarComponent, AfBadgeComponent, AfBannerComponent, AfBreadcrumbsComponent, AfButtonComponent, AfCardComponent, AfCellDefDirective, AfCheckboxComponent, AfChipComponent, AfChipInputComponent, AfComboboxComponent, AfDataTableComponent, AfDatepickerComponent, AfDividerComponent, AfDrawerComponent, AfDropdownComponent, AfEmptyStateComponent, AfFieldComponent, AfFileUploadComponent, AfFormatLabelPipe, AfIconComponent, AfInputComponent, AfModalComponent, AfNavItemComponent, AfNavTabsComponent, AfNavbarComponent, AfPaginationComponent, AfPopoverComponent, AfPopoverTriggerDirective, AfProgressBarComponent, AfRadioComponent, AfRadioGroupComponent, AfSelectComponent, AfSelectMenuComponent, AfSidebarComponent, AfSkeletonComponent, AfSkipLinkComponent, AfSliderComponent, AfSpinnerComponent, AfSwitchComponent, AfTabPanelComponent, AfTableBodyComponent, AfTableCellComponent, AfTableComponent, AfTableHeaderCellComponent, AfTableHeaderComponent, AfTableRowComponent, AfTabsComponent, AfTextareaComponent, AfToastContainerComponent, AfToastService, AfToggleGroupComponent, AfToolbarComponent, AfTooltipDirective };
|
|
10135
|
+
export { AF_ACCORDION_I18N, AF_ALERT_I18N, AF_INPUT_I18N, AF_SELECT_MENU_I18N, AfAccordionComponent, AfAccordionHarness, AfAccordionItemComponent, AfAccordionItemHarness, AfAlertComponent, AfAlertHarness, AfAppShellComponent, AfAppShellPageHeaderComponent, AfAppShellV2Component, AfAppShellV2ToolbarComponent, AfAvatarComponent, AfBadgeComponent, AfBadgeHarness, AfBannerComponent, AfBreadcrumbsComponent, AfButtonComponent, AfButtonHarness, AfCardComponent, AfCellDefDirective, AfCheckboxComponent, AfChipComponent, AfChipInputComponent, AfComboboxComponent, AfDataTableComponent, AfDatepickerComponent, AfDividerComponent, AfDrawerComponent, AfDropdownComponent, AfEmptyStateComponent, AfFieldComponent, AfFileUploadComponent, AfFormatLabelPipe, AfIconComponent, AfInputComponent, AfInputHarness, AfModalComponent, AfNavItemComponent, AfNavTabsComponent, AfNavbarComponent, AfPaginationComponent, AfPopoverComponent, AfPopoverTriggerDirective, AfProgressBarComponent, AfRadioComponent, AfRadioGroupComponent, AfSelectComponent, AfSelectMenuComponent, AfSelectMenuHarness, AfSidebarComponent, AfSkeletonComponent, AfSkipLinkComponent, AfSliderComponent, AfSpinnerComponent, AfSwitchComponent, AfTabPanelComponent, AfTableBodyComponent, AfTableCellComponent, AfTableComponent, AfTableHeaderCellComponent, AfTableHeaderComponent, AfTableRowComponent, AfTabsComponent, AfTextareaComponent, AfToastContainerComponent, AfToastService, AfToggleGroupComponent, AfToolbarComponent, AfTooltipDirective };
|
|
8487
10136
|
//# sourceMappingURL=neuravision-ng-construct.mjs.map
|