forty-cdk 0.2.0 → 0.3.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.
- package/accordion/README.md +122 -0
- package/aspect-ratio/README.md +76 -0
- package/avatar/README.md +100 -0
- package/breadcrumbs/README.md +49 -0
- package/breakpoints/README.md +81 -0
- package/button/README.md +49 -0
- package/calendar/README.md +458 -0
- package/carousel/README.md +358 -0
- package/checkbox/README.md +146 -0
- package/combobox/README.md +535 -0
- package/context-menu/README.md +139 -0
- package/date-field/README.md +184 -0
- package/date-picker/README.md +338 -0
- package/dialog/README.md +388 -0
- package/disclosure/README.md +114 -0
- package/drag-drop/README.md +359 -0
- package/drawer/README.md +560 -0
- package/dropdown-menu/README.md +176 -0
- package/fesm2022/forty-cdk-accordion.mjs +348 -0
- package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
- package/fesm2022/forty-cdk-avatar.mjs +308 -0
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
- package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
- package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
- package/fesm2022/forty-cdk-button.mjs +134 -0
- package/fesm2022/forty-cdk-button.mjs.map +1 -0
- package/fesm2022/forty-cdk-calendar.mjs +2034 -0
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
- package/fesm2022/forty-cdk-carousel.mjs +968 -0
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
- package/fesm2022/forty-cdk-checkbox.mjs +226 -0
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-combobox.mjs +2596 -0
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
- package/fesm2022/forty-cdk-context-menu.mjs +413 -0
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-core.mjs +9022 -0
- package/fesm2022/forty-cdk-core.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-field.mjs +744 -0
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-dialog.mjs +707 -0
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
- package/fesm2022/forty-cdk-disclosure.mjs +190 -0
- package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
- package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
- package/fesm2022/forty-cdk-drawer.mjs +1641 -0
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-field.mjs +425 -0
- package/fesm2022/forty-cdk-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-fieldset.mjs +164 -0
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
- package/fesm2022/forty-cdk-file-upload.mjs +221 -0
- package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
- package/fesm2022/forty-cdk-hover-card.mjs +496 -0
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
- package/fesm2022/forty-cdk-input.mjs +274 -0
- package/fesm2022/forty-cdk-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
- package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +1279 -0
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-menu.mjs +1439 -0
- package/fesm2022/forty-cdk-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-menubar.mjs +787 -0
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
- package/fesm2022/forty-cdk-meter.mjs +211 -0
- package/fesm2022/forty-cdk-meter.mjs.map +1 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-number-input.mjs +559 -0
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-otp-input.mjs +527 -0
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-pagination.mjs +323 -0
- package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
- package/fesm2022/forty-cdk-popover.mjs +698 -0
- package/fesm2022/forty-cdk-popover.mjs.map +1 -0
- package/fesm2022/forty-cdk-progress.mjs +226 -0
- package/fesm2022/forty-cdk-progress.mjs.map +1 -0
- package/fesm2022/forty-cdk-radio-group.mjs +378 -0
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
- package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
- package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
- package/fesm2022/forty-cdk-search.mjs +205 -0
- package/fesm2022/forty-cdk-search.mjs.map +1 -0
- package/fesm2022/forty-cdk-select.mjs +1661 -0
- package/fesm2022/forty-cdk-select.mjs.map +1 -0
- package/fesm2022/forty-cdk-separator.mjs +82 -0
- package/fesm2022/forty-cdk-separator.mjs.map +1 -0
- package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
- package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
- package/fesm2022/forty-cdk-slider.mjs +803 -0
- package/fesm2022/forty-cdk-slider.mjs.map +1 -0
- package/fesm2022/forty-cdk-stepper.mjs +886 -0
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
- package/fesm2022/forty-cdk-switch.mjs +137 -0
- package/fesm2022/forty-cdk-switch.mjs.map +1 -0
- package/fesm2022/forty-cdk-table.mjs +1518 -0
- package/fesm2022/forty-cdk-table.mjs.map +1 -0
- package/fesm2022/forty-cdk-tabs.mjs +400 -0
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-field.mjs +593 -0
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-toast.mjs +1153 -0
- package/fesm2022/forty-cdk-toast.mjs.map +1 -0
- package/fesm2022/forty-cdk-toggle.mjs +516 -0
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
- package/fesm2022/forty-cdk-toolbar.mjs +374 -0
- package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
- package/fesm2022/forty-cdk-tooltip.mjs +672 -0
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
- package/fesm2022/forty-cdk-tree.mjs +2007 -0
- package/fesm2022/forty-cdk-tree.mjs.map +1 -0
- package/fesm2022/forty-cdk-virtualization.mjs +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/fesm2022/forty-cdk.mjs +0 -43310
- package/fesm2022/forty-cdk.mjs.map +1 -1
- package/field/README.md +97 -0
- package/fieldset/README.md +86 -0
- package/file-upload/README.md +73 -0
- package/hover-card/README.md +171 -0
- package/input/README.md +156 -0
- package/listbox/README.md +424 -0
- package/menu/README.md +181 -0
- package/menubar/README.md +140 -0
- package/meter/README.md +128 -0
- package/navigation-menu/README.md +253 -0
- package/number-input/README.md +171 -0
- package/otp-input/README.md +198 -0
- package/package.json +213 -1
- package/pagination/README.md +61 -0
- package/pane-resizer/README.md +136 -0
- package/popover/README.md +262 -0
- package/progress/README.md +115 -0
- package/radio-group/README.md +129 -0
- package/scroll-area/README.md +184 -0
- package/search/README.md +42 -0
- package/select/README.md +488 -0
- package/separator/README.md +84 -0
- package/signal-forms/README.md +72 -0
- package/slider/README.md +152 -0
- package/stepper/README.md +292 -0
- package/switch/README.md +116 -0
- package/table/README.md +769 -0
- package/tabs/README.md +130 -0
- package/time-field/README.md +157 -0
- package/time-picker/README.md +172 -0
- package/toast/README.md +398 -0
- package/toggle/README.md +224 -0
- package/toolbar/README.md +109 -0
- package/tooltip/README.md +274 -0
- package/tree/README.md +708 -0
- package/types/forty-cdk-accordion.d.ts +242 -0
- package/types/forty-cdk-aspect-ratio.d.ts +59 -0
- package/types/forty-cdk-avatar.d.ts +133 -0
- package/types/forty-cdk-breadcrumbs.d.ts +92 -0
- package/types/forty-cdk-breakpoints.d.ts +141 -0
- package/types/forty-cdk-button.d.ts +80 -0
- package/types/forty-cdk-calendar.d.ts +914 -0
- package/types/forty-cdk-carousel.d.ts +530 -0
- package/types/forty-cdk-checkbox.d.ts +141 -0
- package/types/forty-cdk-combobox.d.ts +1259 -0
- package/types/forty-cdk-context-menu.d.ts +313 -0
- package/types/forty-cdk-core.d.ts +5774 -0
- package/types/forty-cdk-date-field.d.ts +307 -0
- package/types/forty-cdk-date-picker.d.ts +622 -0
- package/types/forty-cdk-dialog.d.ts +546 -0
- package/types/forty-cdk-disclosure.d.ts +127 -0
- package/types/forty-cdk-drag-drop.d.ts +456 -0
- package/types/forty-cdk-drawer.d.ts +871 -0
- package/types/forty-cdk-dropdown-menu.d.ts +242 -0
- package/types/forty-cdk-field.d.ts +236 -0
- package/types/forty-cdk-fieldset.d.ts +119 -0
- package/types/forty-cdk-file-upload.d.ts +124 -0
- package/types/forty-cdk-hover-card.d.ts +320 -0
- package/types/forty-cdk-input.d.ts +169 -0
- package/types/forty-cdk-internationalized-date.d.ts +1 -1
- package/types/forty-cdk-listbox.d.ts +513 -0
- package/types/forty-cdk-menu.d.ts +629 -0
- package/types/forty-cdk-menubar.d.ts +451 -0
- package/types/forty-cdk-meter.d.ts +122 -0
- package/types/forty-cdk-navigation-menu.d.ts +514 -0
- package/types/forty-cdk-number-input.d.ts +319 -0
- package/types/forty-cdk-otp-input.d.ts +248 -0
- package/types/forty-cdk-pagination.d.ts +214 -0
- package/types/forty-cdk-pane-resizer.d.ts +145 -0
- package/types/forty-cdk-popover.d.ts +509 -0
- package/types/forty-cdk-progress.d.ts +143 -0
- package/types/forty-cdk-radio-group.d.ts +222 -0
- package/types/forty-cdk-scroll-area.d.ts +258 -0
- package/types/forty-cdk-search.d.ts +142 -0
- package/types/forty-cdk-select.d.ts +899 -0
- package/types/forty-cdk-separator.d.ts +59 -0
- package/types/forty-cdk-signal-forms.d.ts +58 -0
- package/types/forty-cdk-slider.d.ts +379 -0
- package/types/forty-cdk-stepper.d.ts +650 -0
- package/types/forty-cdk-switch.d.ts +87 -0
- package/types/forty-cdk-table.d.ts +723 -0
- package/types/forty-cdk-tabs.d.ts +235 -0
- package/types/forty-cdk-time-field.d.ts +307 -0
- package/types/forty-cdk-time-picker.d.ts +578 -0
- package/types/forty-cdk-toast.d.ts +598 -0
- package/types/forty-cdk-toggle.d.ts +310 -0
- package/types/forty-cdk-toolbar.d.ts +217 -0
- package/types/forty-cdk-tooltip.d.ts +436 -0
- package/types/forty-cdk-tree.d.ts +688 -0
- package/types/forty-cdk.d.ts +1 -19743
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, ElementRef, model, input, computed, linkedSignal, effect, Directive } from '@angular/core';
|
|
3
|
+
import { createDefaults, FormUiControlBase, injectDateAdapter, injectTextDirection, RovingTabindex, resolveHourCycle, dayPeriodNames, SegmentEditor, injectHiddenInput, serializeISODate, assertTimeCapable, clampToBounds, ForDateTimeSegmentBase, ForDateTimeLiteralBase } from 'forty-cdk/core';
|
|
4
|
+
|
|
5
|
+
const DIGITS = {
|
|
6
|
+
day: 2,
|
|
7
|
+
month: 2,
|
|
8
|
+
year: 4,
|
|
9
|
+
hour: 2,
|
|
10
|
+
minute: 2,
|
|
11
|
+
second: 2,
|
|
12
|
+
dayPeriod: 0,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Derives the ordered segment list for a day-granularity date field from the
|
|
16
|
+
* runtime locale, using `Intl.DateTimeFormat(...).formatToParts`. The visible
|
|
17
|
+
* order of day / month / year (and the separator characters between them)
|
|
18
|
+
* follows the locale — `MM/DD/YYYY` for `en-US`, `DD.MM.YYYY` for `de-DE`,
|
|
19
|
+
* `YYYY/MM/DD` for `ja-JP` — so the field never has to parse ambiguous
|
|
20
|
+
* free text.
|
|
21
|
+
*
|
|
22
|
+
* Pure: the same `locale` always yields the same list, and it touches no
|
|
23
|
+
* signals or DOM. A fixed reference date with distinct day / month values is
|
|
24
|
+
* formatted purely to read back the part order; its actual value never
|
|
25
|
+
* surfaces.
|
|
26
|
+
*
|
|
27
|
+
* @param locale BCP 47 locale, or `undefined` for the runtime default.
|
|
28
|
+
*/
|
|
29
|
+
function buildSegments(locale) {
|
|
30
|
+
const parts = new Intl.DateTimeFormat(locale, {
|
|
31
|
+
year: 'numeric',
|
|
32
|
+
month: '2-digit',
|
|
33
|
+
day: '2-digit',
|
|
34
|
+
}).formatToParts(REFERENCE_DATE);
|
|
35
|
+
const segments = [];
|
|
36
|
+
for (const part of parts) {
|
|
37
|
+
switch (part.type) {
|
|
38
|
+
case 'day':
|
|
39
|
+
case 'month':
|
|
40
|
+
case 'year':
|
|
41
|
+
segments.push({ kind: 'editable', type: part.type, digits: DIGITS[part.type] });
|
|
42
|
+
break;
|
|
43
|
+
case 'literal':
|
|
44
|
+
segments.push({ kind: 'literal', literal: part.value });
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return segments;
|
|
51
|
+
}
|
|
52
|
+
// 1 Feb is deliberately distinct in day vs month so a locale that reuses a
|
|
53
|
+
// digit can't mask the part ordering. The year (2000) is irrelevant — only the
|
|
54
|
+
// `type` of each formatted part is read.
|
|
55
|
+
const REFERENCE_DATE = new Date(2000, 1, 1);
|
|
56
|
+
// 1 Feb 2000, 1:23:45 PM — distinct day / month and hour / minute / second, in
|
|
57
|
+
// the afternoon so the `dayPeriod` part is present to read back.
|
|
58
|
+
const REFERENCE_DATE_TIME = new Date(2000, 1, 1, 13, 23, 45);
|
|
59
|
+
/**
|
|
60
|
+
* Derives the ordered segment list for a date(-time) field. At
|
|
61
|
+
* `granularity === 'day'` this is exactly {@link buildSegments} (date-only,
|
|
62
|
+
* byte-for-byte). At a finer granularity it appends the locale-ordered time
|
|
63
|
+
* segments (and the date↔time separator) by formatting a reference date-time
|
|
64
|
+
* with both date and time options in a single `Intl.DateTimeFormat` pass, so
|
|
65
|
+
* the order of every part — and whether an AM/PM `dayPeriod` is present —
|
|
66
|
+
* follows the locale and the resolved hour cycle.
|
|
67
|
+
*
|
|
68
|
+
* Pure: the same arguments always yield the same list.
|
|
69
|
+
*
|
|
70
|
+
* @param locale BCP 47 locale, or `undefined` for the runtime default.
|
|
71
|
+
* @param granularity Smallest editable unit; `'day'` stays date-only.
|
|
72
|
+
* @param hourCycle Resolved hour cycle (`12` shows AM/PM, `24` does not).
|
|
73
|
+
*/
|
|
74
|
+
function buildDateTimeSegments(locale, granularity, hourCycle) {
|
|
75
|
+
if (granularity === 'day') {
|
|
76
|
+
return buildSegments(locale);
|
|
77
|
+
}
|
|
78
|
+
const options = {
|
|
79
|
+
year: 'numeric',
|
|
80
|
+
month: '2-digit',
|
|
81
|
+
day: '2-digit',
|
|
82
|
+
hour: '2-digit',
|
|
83
|
+
hour12: hourCycle === 12,
|
|
84
|
+
};
|
|
85
|
+
if (granularity !== 'hour') {
|
|
86
|
+
options.minute = '2-digit';
|
|
87
|
+
}
|
|
88
|
+
if (granularity === 'second') {
|
|
89
|
+
options.second = '2-digit';
|
|
90
|
+
}
|
|
91
|
+
const parts = new Intl.DateTimeFormat(locale, options).formatToParts(REFERENCE_DATE_TIME);
|
|
92
|
+
const segments = [];
|
|
93
|
+
for (const part of parts) {
|
|
94
|
+
switch (part.type) {
|
|
95
|
+
case 'day':
|
|
96
|
+
case 'month':
|
|
97
|
+
case 'year':
|
|
98
|
+
case 'hour':
|
|
99
|
+
case 'minute':
|
|
100
|
+
case 'second':
|
|
101
|
+
case 'dayPeriod':
|
|
102
|
+
segments.push({ kind: 'editable', type: part.type, digits: DIGITS[part.type] });
|
|
103
|
+
break;
|
|
104
|
+
case 'literal':
|
|
105
|
+
segments.push({ kind: 'literal', literal: part.value });
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return segments;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Injection token for the {@link ForDateFieldContext} provided by
|
|
116
|
+
* `[forDateField]`. Segment and literal children inject it to coordinate with
|
|
117
|
+
* the root.
|
|
118
|
+
*/
|
|
119
|
+
const FOR_DATE_FIELD_CONTEXT = new InjectionToken('FOR_DATE_FIELD_CONTEXT');
|
|
120
|
+
/**
|
|
121
|
+
* Resolves the surrounding {@link ForDateFieldContext}, throwing a descriptive,
|
|
122
|
+
* primitive-prefixed error when used outside a `[forDateField]`.
|
|
123
|
+
*
|
|
124
|
+
* @param piece Name of the calling directive, used in the error message.
|
|
125
|
+
*/
|
|
126
|
+
function injectDateFieldContext(piece) {
|
|
127
|
+
const ctx = inject(FOR_DATE_FIELD_CONTEXT, { optional: true });
|
|
128
|
+
if (!ctx) {
|
|
129
|
+
throw new Error(`[forty-cdk/date-field] ${piece} must be used inside a [forDateField].`);
|
|
130
|
+
}
|
|
131
|
+
return ctx;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Library default accessible name for each editable segment. Used when neither
|
|
136
|
+
* the segment's explicit `ariaLabel` nor a `provideForDateFieldDefaults`
|
|
137
|
+
* override supplies one for that part.
|
|
138
|
+
*/
|
|
139
|
+
const DEFAULT_DATE_FIELD_SEGMENT_LABELS = {
|
|
140
|
+
day: 'day',
|
|
141
|
+
month: 'month',
|
|
142
|
+
year: 'year',
|
|
143
|
+
hour: 'hour',
|
|
144
|
+
minute: 'minute',
|
|
145
|
+
second: 'second',
|
|
146
|
+
dayPeriod: 'AM/PM',
|
|
147
|
+
};
|
|
148
|
+
const FALLBACK = {
|
|
149
|
+
emptySegmentText: 'Empty',
|
|
150
|
+
segmentLabels: DEFAULT_DATE_FIELD_SEGMENT_LABELS,
|
|
151
|
+
};
|
|
152
|
+
const { token, provideDefaults } = createDefaults('FOR_DATE_FIELD_DEFAULTS', FALLBACK);
|
|
153
|
+
/** Token holding the resolved date-field defaults for the current scope. */
|
|
154
|
+
const FOR_DATE_FIELD_DEFAULTS = token;
|
|
155
|
+
/**
|
|
156
|
+
* Configures forty-cdk date-field defaults for this injector scope. Partial
|
|
157
|
+
* overrides inherit unspecified keys from the parent scope (or library
|
|
158
|
+
* defaults at the root).
|
|
159
|
+
*/
|
|
160
|
+
function provideForDateFieldDefaults(defaults = {}) {
|
|
161
|
+
return provideDefaults(defaults);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Year used to resolve day ranges (Feb length) while the year segment is empty.
|
|
166
|
+
* Deliberately a common (non-leap) year so an empty-year February reports
|
|
167
|
+
* `aria-valuemax="28"` rather than briefly jumping to 29.
|
|
168
|
+
*/
|
|
169
|
+
const RESOLVER_YEAR = 2001;
|
|
170
|
+
/**
|
|
171
|
+
* Headless, segmented, spin-editable date input — the keyboard-first
|
|
172
|
+
* counterpart to `ForCalendar`. There is no single WAI-ARIA APG pattern for a
|
|
173
|
+
* date field; it is a composition of
|
|
174
|
+
* [Spinbuttons](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/) inside a
|
|
175
|
+
* labelled `role="group"`.
|
|
176
|
+
* Each day / month / year part is an independent `role="spinbutton"` segment
|
|
177
|
+
* (`[forDateFieldSegment]`) so entry is unambiguous and locale-correct — no
|
|
178
|
+
* free-text parsing, no `03/04`-is-it-March-4th guesswork.
|
|
179
|
+
*
|
|
180
|
+
* `ForDateField` is the root: it owns the entered parts, composes them into the
|
|
181
|
+
* adapter's date type, resolves the locale-ordered segment list, and exposes
|
|
182
|
+
* everything to the segment / literal children through
|
|
183
|
+
* {@link FOR_DATE_FIELD_CONTEXT}. The spin-button engine (digit typing, stepping,
|
|
184
|
+
* Home/End, RTL focus moves, the segment registry) lives in the shared
|
|
185
|
+
* `_internal/datetime` {@link SegmentEditor}; the root supplies only the
|
|
186
|
+
* date-specific bits (segment bounds, the today-seed, the month-name
|
|
187
|
+
* `aria-valuetext`, and the create/clamp compose). All date math goes through
|
|
188
|
+
* the pluggable {@link DateAdapter} shared with `ForCalendar`
|
|
189
|
+
* (`provideInternationalizedDateAdapter()` / `provideNativeDateAdapter()`), so
|
|
190
|
+
* the field hard-depends on no date library.
|
|
191
|
+
*
|
|
192
|
+
* Set `granularity` coarser-than-a-day off (`'hour'` / `'minute'` / `'second'`)
|
|
193
|
+
* to make it a **date-time field**: time segments (hour / minute / second and,
|
|
194
|
+
* in 12-hour mode, an AM·PM `dayPeriod`) are appended after the date segments in
|
|
195
|
+
* the same `role="group"`, sharing the one roving tab stop. This needs a
|
|
196
|
+
* time-capable adapter (`provideNativeDateAdapter()` /
|
|
197
|
+
* `provideInternationalizedDateTimeAdapter()`); `granularity = 'day'` (default)
|
|
198
|
+
* is unchanged and works with any adapter.
|
|
199
|
+
*
|
|
200
|
+
* It implements `FormValueControl<D | null>` from `@angular/forms/signals`, so
|
|
201
|
+
* it auto-wires with `[formField]` and auto-associates inside a `[forField]`.
|
|
202
|
+
* The value stays `null` until every visible segment is filled.
|
|
203
|
+
*
|
|
204
|
+
* @typeParam D The adapter's immutable date (or, with `granularity > 'day'`,
|
|
205
|
+
* date-time) type.
|
|
206
|
+
*
|
|
207
|
+
* Note: the date bounds are named `minDate` / `maxDate`, not `min` / `max` —
|
|
208
|
+
* the latter are reserved `FormUiControl` members typed `number | undefined`
|
|
209
|
+
* for numeric validators, so a date-typed `min` / `max` would break the
|
|
210
|
+
* `FormValueControl` contract.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```html
|
|
214
|
+
* <div forDateField [(value)]="date" [minDate]="min" [maxDate]="max" name="dob"
|
|
215
|
+
* [ariaLabel]="'Date of birth'" #field="forDateField">
|
|
216
|
+
* @for (seg of field.segments(); track seg.id) {
|
|
217
|
+
* @if (seg.isLiteral) {
|
|
218
|
+
* <span forDateFieldLiteral>{{ seg.text }}</span>
|
|
219
|
+
* } @else {
|
|
220
|
+
* <span forDateFieldSegment [segment]="seg.type!">{{ seg.text }}</span>
|
|
221
|
+
* }
|
|
222
|
+
* }
|
|
223
|
+
* </div>
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
class ForDateField extends FormUiControlBase {
|
|
227
|
+
#host = inject(ElementRef);
|
|
228
|
+
#defaults = inject(FOR_DATE_FIELD_DEFAULTS);
|
|
229
|
+
/** The active date adapter, resolved from `FOR_DATE_ADAPTER` (shared with `ForCalendar`). */
|
|
230
|
+
adapter = injectDateAdapter('ForDateField');
|
|
231
|
+
/**
|
|
232
|
+
* Two-way bindable entered date, or `null` while any segment is empty.
|
|
233
|
+
* Required by `FormValueControl<D | null>`. The `model()` change emitter
|
|
234
|
+
* (`(valueChange)`) fires only when the field itself composes a new value,
|
|
235
|
+
* never on consumer writes via `[(value)]`.
|
|
236
|
+
*/
|
|
237
|
+
value = model(null, /* @ts-ignore */
|
|
238
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
239
|
+
/**
|
|
240
|
+
* Minimum selectable date (inclusive). A composed value below it is clamped
|
|
241
|
+
* up. The comparison is by the **full instant** (`DateAdapter.compare`), so at
|
|
242
|
+
* `granularity > 'day'` a non-midnight `minDate` clamps the time too, and the
|
|
243
|
+
* result is identical across time-capable adapters. Named `minDate` (not
|
|
244
|
+
* `min`) because `FormUiControl.min` is reserved for a numeric validator bound
|
|
245
|
+
* by `[formField]`.
|
|
246
|
+
*/
|
|
247
|
+
minDate = input(null, /* @ts-ignore */
|
|
248
|
+
...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
|
|
249
|
+
/**
|
|
250
|
+
* Maximum selectable date (inclusive). A composed value above it is clamped
|
|
251
|
+
* down. Compared by the **full instant** like {@link minDate}. Named `maxDate`
|
|
252
|
+
* (not `max`) for the same reason as {@link minDate}.
|
|
253
|
+
*/
|
|
254
|
+
maxDate = input(null, /* @ts-ignore */
|
|
255
|
+
...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
|
|
256
|
+
/**
|
|
257
|
+
* Date-time precision. `'day'` (default, **non-breaking**) is a pure date
|
|
258
|
+
* field. `'hour'` / `'minute'` / `'second'` append the matching time segments
|
|
259
|
+
* and the value carries a time component — which requires a time-capable
|
|
260
|
+
* adapter (`provideNativeDateAdapter()` /
|
|
261
|
+
* `provideInternationalizedDateTimeAdapter()`).
|
|
262
|
+
*/
|
|
263
|
+
granularity = input('day', /* @ts-ignore */
|
|
264
|
+
...(ngDevMode ? [{ debugName: "granularity" }] : /* istanbul ignore next */ []));
|
|
265
|
+
/**
|
|
266
|
+
* 12- or 24-hour cycle for the time segments. When `null` (default) it is
|
|
267
|
+
* derived from the locale. A 12-hour cycle adds the AM/PM `dayPeriod` segment.
|
|
268
|
+
* Only meaningful when `granularity > 'day'`.
|
|
269
|
+
*/
|
|
270
|
+
hourCycle = input(null, /* @ts-ignore */
|
|
271
|
+
...(ngDevMode ? [{ debugName: "hourCycle" }] : /* istanbul ignore next */ []));
|
|
272
|
+
/** BCP 47 locale driving segment order and separators. Defaults to the runtime locale. */
|
|
273
|
+
locale = input(null, /* @ts-ignore */
|
|
274
|
+
...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
275
|
+
/**
|
|
276
|
+
* Per-segment placeholder shown while empty. Unspecified parts fall back to a
|
|
277
|
+
* letter-repeat default (`dd` / `mm` / `yyyy` / `hh` / `mm` / `ss` / `--`).
|
|
278
|
+
*/
|
|
279
|
+
placeholder = input({}, /* @ts-ignore */
|
|
280
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
281
|
+
/** Accessible name for the field group. Emits no `aria-label` while `null`. */
|
|
282
|
+
ariaLabel = input(null, /* @ts-ignore */
|
|
283
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
284
|
+
/**
|
|
285
|
+
* Writing direction. When unset (default `null`), the inherited ambient
|
|
286
|
+
* direction is resolved from the nearest ancestor carrying a `dir` attribute
|
|
287
|
+
* (or `<html dir>`), defaulting to `'ltr'`. An explicit `[dir]` always wins.
|
|
288
|
+
* The resolved value is reflected to the host `dir` attribute and mirrors the
|
|
289
|
+
* ArrowLeft / ArrowRight segment navigation in RTL.
|
|
290
|
+
*/
|
|
291
|
+
_dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
|
|
292
|
+
dir = injectTextDirection(this._dirInput);
|
|
293
|
+
/** Shared roving-tabindex tracker: exactly one segment owns `tabindex=0`. */
|
|
294
|
+
roving = new RovingTabindex();
|
|
295
|
+
/** Resolved hour cycle (`12` shows AM/PM, `24` does not). */
|
|
296
|
+
#cycle = computed(() => resolveHourCycle(this.locale() ?? undefined, this.hourCycle()), /* @ts-ignore */
|
|
297
|
+
...(ngDevMode ? [{ debugName: "#cycle" }] : /* istanbul ignore next */ []));
|
|
298
|
+
#specs = computed(() => buildDateTimeSegments(this.locale() ?? undefined, this.granularity(), this.#cycle()), /* @ts-ignore */
|
|
299
|
+
...(ngDevMode ? [{ debugName: "#specs" }] : /* istanbul ignore next */ []));
|
|
300
|
+
#editableOrder = computed(() => this.#specs()
|
|
301
|
+
.filter((spec) => spec.kind === 'editable')
|
|
302
|
+
.map((spec) => spec.type), /* @ts-ignore */
|
|
303
|
+
...(ngDevMode ? [{ debugName: "#editableOrder" }] : /* istanbul ignore next */ []));
|
|
304
|
+
#periodNames = computed(() => dayPeriodNames(this.locale() ?? undefined), /* @ts-ignore */
|
|
305
|
+
...(ngDevMode ? [{ debugName: "#periodNames" }] : /* istanbul ignore next */ []));
|
|
306
|
+
/**
|
|
307
|
+
* The entered parts. A `linkedSignal` keyed on `value`: a non-null write
|
|
308
|
+
* (consumer, `[formField]`, or our own compose) rehydrates the segments from
|
|
309
|
+
* the date. A `null` transition is disambiguated by the prior parts: an
|
|
310
|
+
* *internal* edit clearing one segment always leaves the others, so
|
|
311
|
+
* `previous` still carries a filled part and is preserved (clearing the day
|
|
312
|
+
* never wipes the month / year); an *external* reset of a complete value
|
|
313
|
+
* leaves `previous` fully filled, so the field clears.
|
|
314
|
+
*/
|
|
315
|
+
#parts = linkedSignal({ ...(ngDevMode ? { debugName: "#parts" } : /* istanbul ignore next */ {}), source: this.value,
|
|
316
|
+
computation: (current, previous) => {
|
|
317
|
+
if (current !== null) {
|
|
318
|
+
const parts = {
|
|
319
|
+
day: this.adapter.getDate(current),
|
|
320
|
+
month: this.adapter.getMonth(current),
|
|
321
|
+
year: this.adapter.getYear(current),
|
|
322
|
+
hour: null,
|
|
323
|
+
minute: null,
|
|
324
|
+
second: null,
|
|
325
|
+
};
|
|
326
|
+
if (this.granularity() !== 'day') {
|
|
327
|
+
const time = this.#time();
|
|
328
|
+
parts.hour = time.getHours(current);
|
|
329
|
+
parts.minute = time.getMinutes(current);
|
|
330
|
+
parts.second = time.getSeconds(current);
|
|
331
|
+
}
|
|
332
|
+
return parts;
|
|
333
|
+
}
|
|
334
|
+
const prior = previous?.value;
|
|
335
|
+
if (prior && this.#someEditableEmpty(prior)) {
|
|
336
|
+
return prior;
|
|
337
|
+
}
|
|
338
|
+
return { day: null, month: null, year: null, hour: null, minute: null, second: null };
|
|
339
|
+
} });
|
|
340
|
+
#editor = new SegmentEditor({
|
|
341
|
+
disabled: this.effectiveDisabled,
|
|
342
|
+
readonly: this.readonly,
|
|
343
|
+
roving: this.roving,
|
|
344
|
+
cycle: this.#cycle,
|
|
345
|
+
specs: this.#specs,
|
|
346
|
+
editableOrder: this.#editableOrder,
|
|
347
|
+
periodNames: this.#periodNames,
|
|
348
|
+
parts: () => this.#parts(),
|
|
349
|
+
segmentMin: (type) => this.segmentMin(type),
|
|
350
|
+
segmentMax: (type) => this.segmentMax(type),
|
|
351
|
+
seed: (type) => this.#seed(type),
|
|
352
|
+
placeholderFor: (type) => this.#placeholderFor(type),
|
|
353
|
+
valueText: (type) => this.#valueText(type),
|
|
354
|
+
commit: (next) => this.#commitParts(next),
|
|
355
|
+
});
|
|
356
|
+
/**
|
|
357
|
+
* The ordered, locale-derived segments (editable + literals) to render. Each
|
|
358
|
+
* entry carries the text to display: the formatted value when filled, the
|
|
359
|
+
* placeholder while empty, or the literal separator.
|
|
360
|
+
*/
|
|
361
|
+
segments = this.#editor.segments;
|
|
362
|
+
constructor() {
|
|
363
|
+
super();
|
|
364
|
+
injectHiddenInput({
|
|
365
|
+
name: this.name,
|
|
366
|
+
values: computed(() => {
|
|
367
|
+
const current = this.value();
|
|
368
|
+
if (current === null) {
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
return [serializeISODate(this.adapter, current, this.granularity(), 'ForDateField')];
|
|
372
|
+
}),
|
|
373
|
+
disabled: this.effectiveDisabled,
|
|
374
|
+
});
|
|
375
|
+
// Eager validation: a date-time field needs a time-capable adapter. Fail
|
|
376
|
+
// loudly as soon as the granularity input settles. The throw is raised
|
|
377
|
+
// during change detection (inside this `effect`) and propagates through
|
|
378
|
+
// Angular's error handling, so a day-only adapter misconfiguration is
|
|
379
|
+
// surfaced — never silently swallowed.
|
|
380
|
+
effect(() => {
|
|
381
|
+
if (this.granularity() !== 'day') {
|
|
382
|
+
this.#time();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
segmentValue(type) {
|
|
387
|
+
return this.#editor.segmentValue(type);
|
|
388
|
+
}
|
|
389
|
+
segmentMin(type) {
|
|
390
|
+
if (type === 'hour') {
|
|
391
|
+
return this.#cycle() === 12 ? 1 : 0;
|
|
392
|
+
}
|
|
393
|
+
if (type === 'minute' || type === 'second' || type === 'dayPeriod') {
|
|
394
|
+
return 0;
|
|
395
|
+
}
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
segmentMax(type) {
|
|
399
|
+
switch (type) {
|
|
400
|
+
case 'month':
|
|
401
|
+
return 12;
|
|
402
|
+
case 'year':
|
|
403
|
+
return 9999;
|
|
404
|
+
case 'hour':
|
|
405
|
+
return this.#cycle() === 12 ? 12 : 23;
|
|
406
|
+
case 'minute':
|
|
407
|
+
case 'second':
|
|
408
|
+
return 59;
|
|
409
|
+
case 'dayPeriod':
|
|
410
|
+
return 1;
|
|
411
|
+
default: {
|
|
412
|
+
const parts = this.#parts();
|
|
413
|
+
const probe = this.adapter.createDate(parts.year ?? RESOLVER_YEAR, parts.month ?? 1, 1);
|
|
414
|
+
return this.adapter.getDaysInMonth(probe);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
segmentValueText(type) {
|
|
419
|
+
return this.#editor.segmentValueText(type);
|
|
420
|
+
}
|
|
421
|
+
segmentDisplayText(type) {
|
|
422
|
+
return this.#editor.segmentDisplayText(type);
|
|
423
|
+
}
|
|
424
|
+
isSegmentEmpty(type) {
|
|
425
|
+
return this.#editor.isSegmentEmpty(type);
|
|
426
|
+
}
|
|
427
|
+
isFirstSegmentType(type) {
|
|
428
|
+
return this.#editor.isFirstSegmentType(type);
|
|
429
|
+
}
|
|
430
|
+
registerSegment(handle) {
|
|
431
|
+
this.#editor.registerSegment(handle);
|
|
432
|
+
}
|
|
433
|
+
unregisterSegment(handle) {
|
|
434
|
+
this.#editor.unregisterSegment(handle);
|
|
435
|
+
}
|
|
436
|
+
focusSegment(type) {
|
|
437
|
+
this.#editor.focusSegment(type);
|
|
438
|
+
}
|
|
439
|
+
typeDigit(type, digit) {
|
|
440
|
+
this.#editor.typeDigit(type, digit);
|
|
441
|
+
}
|
|
442
|
+
step(type, delta) {
|
|
443
|
+
this.#editor.step(type, delta);
|
|
444
|
+
}
|
|
445
|
+
goToBound(type, bound) {
|
|
446
|
+
this.#editor.goToBound(type, bound);
|
|
447
|
+
}
|
|
448
|
+
setDayPeriod(period) {
|
|
449
|
+
this.#editor.setDayPeriod(period);
|
|
450
|
+
}
|
|
451
|
+
clear(type) {
|
|
452
|
+
this.#editor.clear(type);
|
|
453
|
+
}
|
|
454
|
+
focusSibling(type, step) {
|
|
455
|
+
this.#editor.focusSibling(type, step);
|
|
456
|
+
}
|
|
457
|
+
onFocusOut(event) {
|
|
458
|
+
const next = event.relatedTarget;
|
|
459
|
+
if (!next || !this.#host.nativeElement.contains(next)) {
|
|
460
|
+
this.markTouched();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** The active adapter, narrowed to a time-capable one; throws when it is day-only. */
|
|
464
|
+
#time() {
|
|
465
|
+
return assertTimeCapable(this.adapter, 'ForDateField');
|
|
466
|
+
}
|
|
467
|
+
/** Field-specific `aria-valuetext`: the empty marker or the localized month name. */
|
|
468
|
+
#valueText(type) {
|
|
469
|
+
if (this.isSegmentEmpty(type)) {
|
|
470
|
+
return this.#defaults.emptySegmentText;
|
|
471
|
+
}
|
|
472
|
+
if (type !== 'month') {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
const month = this.#parts().month;
|
|
476
|
+
if (month === null) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
// Route the month name through the adapter's `format` so it tracks the
|
|
480
|
+
// adapter's calendar system rather than assuming Gregorian month numbering.
|
|
481
|
+
// A probe date built with `createDate` maps the 1-12 number to a localized
|
|
482
|
+
// long name without involving the entered value.
|
|
483
|
+
return this.adapter.format(this.adapter.createDate(RESOLVER_YEAR, month, 1), {
|
|
484
|
+
month: 'long',
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/** Base value for stepping an empty segment: date parts from today, time parts from their minimum. */
|
|
488
|
+
#seed(type) {
|
|
489
|
+
if (type === 'hour') {
|
|
490
|
+
return this.#toInternalHour(this.segmentMin('hour'));
|
|
491
|
+
}
|
|
492
|
+
if (type === 'minute' || type === 'second' || type === 'dayPeriod') {
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
const today = this.adapter.today();
|
|
496
|
+
const base = type === 'day'
|
|
497
|
+
? this.adapter.getDate(today)
|
|
498
|
+
: type === 'month'
|
|
499
|
+
? this.adapter.getMonth(today)
|
|
500
|
+
: this.adapter.getYear(today);
|
|
501
|
+
return Math.min(this.segmentMax(type), Math.max(this.segmentMin(type), base));
|
|
502
|
+
}
|
|
503
|
+
/** Converts a 12-hour display hour to a 0-23 hour, preserving the entered AM/PM. */
|
|
504
|
+
#toInternalHour(display) {
|
|
505
|
+
if (this.#cycle() === 24) {
|
|
506
|
+
return display;
|
|
507
|
+
}
|
|
508
|
+
const hour = this.#parts().hour;
|
|
509
|
+
const pm = hour !== null && hour >= 12;
|
|
510
|
+
const base = display % 12;
|
|
511
|
+
return pm ? base + 12 : base;
|
|
512
|
+
}
|
|
513
|
+
#placeholderFor(type) {
|
|
514
|
+
const override = this.placeholder()[type];
|
|
515
|
+
if (override) {
|
|
516
|
+
return override;
|
|
517
|
+
}
|
|
518
|
+
switch (type) {
|
|
519
|
+
case 'day':
|
|
520
|
+
return 'dd';
|
|
521
|
+
case 'month':
|
|
522
|
+
return 'mm';
|
|
523
|
+
case 'year':
|
|
524
|
+
return 'yyyy';
|
|
525
|
+
case 'hour':
|
|
526
|
+
return 'hh';
|
|
527
|
+
case 'minute':
|
|
528
|
+
return 'mm';
|
|
529
|
+
case 'second':
|
|
530
|
+
return 'ss';
|
|
531
|
+
default:
|
|
532
|
+
return '--';
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
#someEditableEmpty(parts) {
|
|
536
|
+
return this.#editableOrder().some((type) => {
|
|
537
|
+
if (type === 'dayPeriod') {
|
|
538
|
+
return parts.hour === null;
|
|
539
|
+
}
|
|
540
|
+
return parts[type] === null;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Re-clamps `day` to the resolved month/year length so the day segment's
|
|
545
|
+
* `aria-valuenow` never exceeds its `aria-valuemax` after stepping the month
|
|
546
|
+
* or year (e.g. day 31 + step to February → 28/29). Only ever shrinks the
|
|
547
|
+
* day; an empty day is left untouched.
|
|
548
|
+
*/
|
|
549
|
+
#clampDay(parts) {
|
|
550
|
+
if (parts.day === null) {
|
|
551
|
+
return parts;
|
|
552
|
+
}
|
|
553
|
+
const probe = this.adapter.createDate(parts.year ?? RESOLVER_YEAR, parts.month ?? 1, 1);
|
|
554
|
+
const maxDay = this.adapter.getDaysInMonth(probe);
|
|
555
|
+
if (parts.day <= maxDay) {
|
|
556
|
+
return parts;
|
|
557
|
+
}
|
|
558
|
+
return { ...parts, day: maxDay };
|
|
559
|
+
}
|
|
560
|
+
#commitParts(rawNext) {
|
|
561
|
+
const next = this.#clampDay(rawNext);
|
|
562
|
+
this.#parts.set(next);
|
|
563
|
+
const granularity = this.granularity();
|
|
564
|
+
const needHour = granularity !== 'day';
|
|
565
|
+
const needMinute = granularity === 'minute' || granularity === 'second';
|
|
566
|
+
const needSecond = granularity === 'second';
|
|
567
|
+
const complete = next.day !== null &&
|
|
568
|
+
next.month !== null &&
|
|
569
|
+
next.year !== null &&
|
|
570
|
+
(!needHour || next.hour !== null) &&
|
|
571
|
+
(!needMinute || next.minute !== null) &&
|
|
572
|
+
(!needSecond || next.second !== null);
|
|
573
|
+
if (!complete) {
|
|
574
|
+
this.value.set(null);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
let created = this.adapter.createDate(next.year, next.month, next.day);
|
|
578
|
+
if (needHour) {
|
|
579
|
+
created = this.#time().setTime(created, next.hour, needMinute ? next.minute : 0, needSecond ? next.second : 0);
|
|
580
|
+
}
|
|
581
|
+
this.value.set(clampToBounds(this.adapter, created, this.minDate(), this.maxDate()));
|
|
582
|
+
}
|
|
583
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDateField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
584
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForDateField, isStandalone: true, selector: "[forDateField]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, granularity: { classPropertyName: "granularity", publicName: "granularity", isSignal: true, isRequired: false, transformFunction: null }, hourCycle: { classPropertyName: "hourCycle", publicName: "hourCycle", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "role": "group" }, listeners: { "focusout": "onFocusOut($event)" }, properties: { "attr.dir": "dir()", "attr.aria-label": "ariaLabel() || null", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.aria-readonly": "readonly() ? \"true\" : null", "attr.aria-required": "required() ? \"true\" : null", "attr.aria-invalid": "invalid() ? \"true\" : null", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.data-readonly": "readonly() ? \"\" : null", "attr.data-empty": "value() === null ? \"\" : null" } }, providers: [{ provide: FOR_DATE_FIELD_CONTEXT, useExisting: ForDateField }], exportAs: ["forDateField"], usesInheritance: true, ngImport: i0 });
|
|
585
|
+
}
|
|
586
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDateField, decorators: [{
|
|
587
|
+
type: Directive,
|
|
588
|
+
args: [{
|
|
589
|
+
selector: '[forDateField]',
|
|
590
|
+
exportAs: 'forDateField',
|
|
591
|
+
host: {
|
|
592
|
+
role: 'group',
|
|
593
|
+
'[attr.dir]': 'dir()',
|
|
594
|
+
'[attr.aria-label]': 'ariaLabel() || null',
|
|
595
|
+
'[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
|
|
596
|
+
'[attr.aria-readonly]': 'readonly() ? "true" : null',
|
|
597
|
+
'[attr.aria-required]': 'required() ? "true" : null',
|
|
598
|
+
'[attr.aria-invalid]': 'invalid() ? "true" : null',
|
|
599
|
+
'[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
|
|
600
|
+
'[attr.data-readonly]': 'readonly() ? "" : null',
|
|
601
|
+
'[attr.data-empty]': 'value() === null ? "" : null',
|
|
602
|
+
'(focusout)': 'onFocusOut($event)',
|
|
603
|
+
},
|
|
604
|
+
providers: [{ provide: FOR_DATE_FIELD_CONTEXT, useExisting: ForDateField }],
|
|
605
|
+
}]
|
|
606
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], granularity: [{ type: i0.Input, args: [{ isSignal: true, alias: "granularity", required: false }] }], hourCycle: [{ type: i0.Input, args: [{ isSignal: true, alias: "hourCycle", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }] } });
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* One editable spinbutton segment of a `[forDateField]` — the day, month, or
|
|
610
|
+
* year part (and, at `granularity > 'day'`, hour / minute / second / the AM·PM
|
|
611
|
+
* `dayPeriod`). Apply on a focusable element (typically a `<span>`); the shared
|
|
612
|
+
* {@link ForDateTimeSegmentBase} adds `role="spinbutton"`, the `aria-valuemin` /
|
|
613
|
+
* `aria-valuemax` / `aria-valuenow` / `aria-valuetext` reflection, the roving
|
|
614
|
+
* tabindex, and the full keyboard map:
|
|
615
|
+
*
|
|
616
|
+
* - **digits** fill a numeric segment and auto-advance to the next when full;
|
|
617
|
+
* - **a / p** set the period on the AM/PM (`dayPeriod`) segment;
|
|
618
|
+
* - **ArrowUp / ArrowDown** step the value (day / month / hour / minute / second
|
|
619
|
+
* wrap, year clamps, dayPeriod toggles);
|
|
620
|
+
* - **ArrowLeft / ArrowRight** move between segments (mirrored under RTL, no wrap);
|
|
621
|
+
* - **Home / End** jump to the segment minimum / maximum (dayPeriod → AM / PM);
|
|
622
|
+
* - **Backspace / Delete** clear a numeric segment.
|
|
623
|
+
*
|
|
624
|
+
* All state lives on the root `ForDateField`; the segment only reads it and
|
|
625
|
+
* forwards intents. The rendered text comes from the root's `segments()` list
|
|
626
|
+
* (`{{ seg.text }}`), so this element carries behavior and ARIA, not content.
|
|
627
|
+
*/
|
|
628
|
+
class ForDateFieldSegment extends ForDateTimeSegmentBase {
|
|
629
|
+
ctx = injectDateFieldContext('ForDateFieldSegment');
|
|
630
|
+
#defaults = inject(FOR_DATE_FIELD_DEFAULTS);
|
|
631
|
+
/** Which date or time part this segment edits. */
|
|
632
|
+
segment = input.required(/* @ts-ignore */
|
|
633
|
+
...(ngDevMode ? [{ debugName: "segment" }] : /* istanbul ignore next */ []));
|
|
634
|
+
/**
|
|
635
|
+
* Accessible name for this segment. Falls back to the scope's localized
|
|
636
|
+
* default label for the part (via `provideForDateFieldDefaults`), which in
|
|
637
|
+
* turn defaults to the part name — and `'AM/PM'` for the `dayPeriod` segment.
|
|
638
|
+
*/
|
|
639
|
+
ariaLabel = input(null, /* @ts-ignore */
|
|
640
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
641
|
+
resolvedAriaLabel = computed(() => {
|
|
642
|
+
const type = this.segment();
|
|
643
|
+
return (this.ariaLabel() ??
|
|
644
|
+
this.#defaults.segmentLabels[type] ??
|
|
645
|
+
DEFAULT_DATE_FIELD_SEGMENT_LABELS[type]);
|
|
646
|
+
}, /* @ts-ignore */
|
|
647
|
+
...(ngDevMode ? [{ debugName: "resolvedAriaLabel" }] : /* istanbul ignore next */ []));
|
|
648
|
+
constructor() {
|
|
649
|
+
super();
|
|
650
|
+
this.registerSegment();
|
|
651
|
+
}
|
|
652
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDateFieldSegment, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
653
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForDateFieldSegment, isStandalone: true, selector: "[forDateFieldSegment]", inputs: { segment: { classPropertyName: "segment", publicName: "segment", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["forDateFieldSegment"], usesInheritance: true, ngImport: i0 });
|
|
654
|
+
}
|
|
655
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDateFieldSegment, decorators: [{
|
|
656
|
+
type: Directive,
|
|
657
|
+
args: [{
|
|
658
|
+
selector: '[forDateFieldSegment]',
|
|
659
|
+
exportAs: 'forDateFieldSegment',
|
|
660
|
+
}]
|
|
661
|
+
}], ctorParameters: () => [], propDecorators: { segment: [{ type: i0.Input, args: [{ isSignal: true, alias: "segment", required: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* A decorative separator (`/`, `.`, `-`) between the editable segments of a
|
|
665
|
+
* `[forDateField]`. Apply on the element rendering the literal text; it is
|
|
666
|
+
* marked `aria-hidden` and stays out of the tab order, so assistive tech reads
|
|
667
|
+
* only the spinbutton segments. The separator characters come from the root's
|
|
668
|
+
* locale-ordered `segments()` list (`{{ seg.text }}`).
|
|
669
|
+
*/
|
|
670
|
+
class ForDateFieldLiteral extends ForDateTimeLiteralBase {
|
|
671
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDateFieldLiteral, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
672
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDateFieldLiteral, isStandalone: true, selector: "[forDateFieldLiteral]", exportAs: ["forDateFieldLiteral"], usesInheritance: true, ngImport: i0 });
|
|
673
|
+
}
|
|
674
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDateFieldLiteral, decorators: [{
|
|
675
|
+
type: Directive,
|
|
676
|
+
args: [{
|
|
677
|
+
selector: '[forDateFieldLiteral]',
|
|
678
|
+
exportAs: 'forDateFieldLiteral',
|
|
679
|
+
}]
|
|
680
|
+
}] });
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Exact public names of every `ForDateField` input, its models included. Spread it into the
|
|
684
|
+
* `inputs` array of a `hostDirectives` entry so a wrapper component re-exposes the
|
|
685
|
+
* primitive's full surface — the Signal Forms members `[formField]` binds among them —
|
|
686
|
+
* without hand-maintaining the list. Always spread into an inline object literal as shown
|
|
687
|
+
* below: the literal is what keeps the entry statically analyzable for consumers compiling
|
|
688
|
+
* against the published package. An anti-drift spec fails when this list no longer matches
|
|
689
|
+
* the directive's actual API. See `docs/wrapping-form-primitives.md` for both supported
|
|
690
|
+
* wrapping patterns.
|
|
691
|
+
*
|
|
692
|
+
* @example
|
|
693
|
+
* ```ts
|
|
694
|
+
* @Component({
|
|
695
|
+
* selector: 'div[myDateField]',
|
|
696
|
+
* template: '',
|
|
697
|
+
* hostDirectives: [
|
|
698
|
+
* {
|
|
699
|
+
* directive: ForDateField,
|
|
700
|
+
* inputs: [...FOR_DATE_FIELD_HOST_DIRECTIVE_INPUTS],
|
|
701
|
+
* outputs: [...FOR_DATE_FIELD_HOST_DIRECTIVE_OUTPUTS],
|
|
702
|
+
* },
|
|
703
|
+
* ],
|
|
704
|
+
* })
|
|
705
|
+
* export class MyDateField {}
|
|
706
|
+
* ```
|
|
707
|
+
*/
|
|
708
|
+
const FOR_DATE_FIELD_HOST_DIRECTIVE_INPUTS = [
|
|
709
|
+
'value',
|
|
710
|
+
'ariaLabel',
|
|
711
|
+
'dir',
|
|
712
|
+
'dirty',
|
|
713
|
+
'disabled',
|
|
714
|
+
'errors',
|
|
715
|
+
'granularity',
|
|
716
|
+
'hourCycle',
|
|
717
|
+
'invalid',
|
|
718
|
+
'locale',
|
|
719
|
+
'maxDate',
|
|
720
|
+
'minDate',
|
|
721
|
+
'name',
|
|
722
|
+
'pending',
|
|
723
|
+
'placeholder',
|
|
724
|
+
'readonly',
|
|
725
|
+
'required',
|
|
726
|
+
'touched',
|
|
727
|
+
];
|
|
728
|
+
/**
|
|
729
|
+
* Exact public names of every `ForDateField` output, the Signal Forms `touch` output
|
|
730
|
+
* included. Spread it into the `outputs` array of the same `hostDirectives` entry as
|
|
731
|
+
* {@link FOR_DATE_FIELD_HOST_DIRECTIVE_INPUTS}.
|
|
732
|
+
*/
|
|
733
|
+
const FOR_DATE_FIELD_HOST_DIRECTIVE_OUTPUTS = [
|
|
734
|
+
'valueChange',
|
|
735
|
+
'touchedChange',
|
|
736
|
+
'touch',
|
|
737
|
+
];
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Generated bundle index. Do not edit.
|
|
741
|
+
*/
|
|
742
|
+
|
|
743
|
+
export { DEFAULT_DATE_FIELD_SEGMENT_LABELS, FOR_DATE_FIELD_CONTEXT, FOR_DATE_FIELD_DEFAULTS, FOR_DATE_FIELD_HOST_DIRECTIVE_INPUTS, FOR_DATE_FIELD_HOST_DIRECTIVE_OUTPUTS, ForDateField, ForDateFieldLiteral, ForDateFieldSegment, provideForDateFieldDefaults };
|
|
744
|
+
//# sourceMappingURL=forty-cdk-date-field.mjs.map
|