ngx-com 0.0.21 → 0.1.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/README.md +137 -33
- package/fesm2022/ngx-com-components-alert.mjs +21 -11
- package/fesm2022/ngx-com-components-alert.mjs.map +1 -1
- package/fesm2022/ngx-com-components-avatar.mjs +9 -7
- package/fesm2022/ngx-com-components-avatar.mjs.map +1 -1
- package/fesm2022/ngx-com-components-button.mjs +1 -1
- package/fesm2022/ngx-com-components-button.mjs.map +1 -1
- package/fesm2022/ngx-com-components-calendar.mjs +27 -3112
- package/fesm2022/ngx-com-components-calendar.mjs.map +1 -1
- package/fesm2022/ngx-com-components-card.mjs +8 -8
- package/fesm2022/ngx-com-components-card.mjs.map +1 -1
- package/fesm2022/ngx-com-components-carousel.mjs +16 -4
- package/fesm2022/ngx-com-components-carousel.mjs.map +1 -1
- package/fesm2022/ngx-com-components-checkbox.mjs +1 -1
- package/fesm2022/ngx-com-components-checkbox.mjs.map +1 -1
- package/fesm2022/ngx-com-components-code-block.mjs +9 -9
- package/fesm2022/ngx-com-components-code-block.mjs.map +1 -1
- package/fesm2022/ngx-com-components-collapsible.mjs +15 -13
- package/fesm2022/ngx-com-components-collapsible.mjs.map +1 -1
- package/fesm2022/ngx-com-components-confirm.mjs +4 -4
- package/fesm2022/ngx-com-components-confirm.mjs.map +1 -1
- package/fesm2022/ngx-com-components-datepicker.mjs +2334 -0
- package/fesm2022/ngx-com-components-datepicker.mjs.map +1 -0
- package/fesm2022/ngx-com-components-dialog.mjs +47 -45
- package/fesm2022/ngx-com-components-dialog.mjs.map +1 -1
- package/fesm2022/ngx-com-components-dropdown.mjs +446 -340
- package/fesm2022/ngx-com-components-dropdown.mjs.map +1 -1
- package/fesm2022/ngx-com-components-empty-state.mjs +5 -3
- package/fesm2022/ngx-com-components-empty-state.mjs.map +1 -1
- package/fesm2022/ngx-com-components-form-field.mjs +11 -6
- package/fesm2022/ngx-com-components-form-field.mjs.map +1 -1
- package/fesm2022/ngx-com-components-icon-lucide.mjs +41 -0
- package/fesm2022/ngx-com-components-icon-lucide.mjs.map +1 -0
- package/fesm2022/ngx-com-components-icon.mjs +89 -61
- package/fesm2022/ngx-com-components-icon.mjs.map +1 -1
- package/fesm2022/ngx-com-components-item.mjs +14 -4
- package/fesm2022/ngx-com-components-item.mjs.map +1 -1
- package/fesm2022/ngx-com-components-menu.mjs +61 -69
- package/fesm2022/ngx-com-components-menu.mjs.map +1 -1
- package/fesm2022/ngx-com-components-native-control.mjs +170 -0
- package/fesm2022/ngx-com-components-native-control.mjs.map +1 -0
- package/fesm2022/ngx-com-components-paginator.mjs +11 -3
- package/fesm2022/ngx-com-components-paginator.mjs.map +1 -1
- package/fesm2022/ngx-com-components-popover.mjs +58 -33
- package/fesm2022/ngx-com-components-popover.mjs.map +1 -1
- package/fesm2022/ngx-com-components-radio.mjs +4 -4
- package/fesm2022/ngx-com-components-radio.mjs.map +1 -1
- package/fesm2022/ngx-com-components-segmented-control.mjs +6 -4
- package/fesm2022/ngx-com-components-segmented-control.mjs.map +1 -1
- package/fesm2022/ngx-com-components-sort.mjs +63 -57
- package/fesm2022/ngx-com-components-sort.mjs.map +1 -1
- package/fesm2022/ngx-com-components-spinner.mjs +6 -6
- package/fesm2022/ngx-com-components-spinner.mjs.map +1 -1
- package/fesm2022/ngx-com-components-switch.mjs +18 -9
- package/fesm2022/ngx-com-components-switch.mjs.map +1 -1
- package/fesm2022/ngx-com-components-table.mjs +23 -9
- package/fesm2022/ngx-com-components-table.mjs.map +1 -1
- package/fesm2022/ngx-com-components-tabs.mjs +81 -58
- package/fesm2022/ngx-com-components-tabs.mjs.map +1 -1
- package/fesm2022/ngx-com-components-timepicker.mjs +1048 -0
- package/fesm2022/ngx-com-components-timepicker.mjs.map +1 -0
- package/fesm2022/ngx-com-components-toast.mjs +18 -14
- package/fesm2022/ngx-com-components-toast.mjs.map +1 -1
- package/fesm2022/ngx-com-components-tooltip.mjs +5 -5
- package/fesm2022/ngx-com-components-tooltip.mjs.map +1 -1
- package/fesm2022/ngx-com-components.mjs +0 -13
- package/fesm2022/ngx-com-components.mjs.map +1 -1
- package/fesm2022/ngx-com-tokens.mjs +0 -8
- package/fesm2022/ngx-com-tokens.mjs.map +1 -1
- package/fesm2022/ngx-com-utils.mjs +13 -1
- package/fesm2022/ngx-com-utils.mjs.map +1 -1
- package/fesm2022/ngx-com.mjs +1 -1
- package/fesm2022/ngx-com.mjs.map +1 -1
- package/package.json +51 -8
- package/styles/animations.css +38 -0
- package/styles/candy.css +121 -0
- package/styles/dark.css +159 -0
- package/styles/forest.css +117 -0
- package/styles/ocean.css +117 -0
- package/styles/themes.css +7 -0
- package/styles/tokens.css +277 -0
- package/styles/utilities.css +16 -0
- package/types/ngx-com-components-alert.d.ts +14 -4
- package/types/ngx-com-components-avatar.d.ts +2 -0
- package/types/ngx-com-components-calendar.d.ts +3 -747
- package/types/ngx-com-components-card.d.ts +2 -2
- package/types/ngx-com-components-carousel.d.ts +11 -1
- package/types/ngx-com-components-code-block.d.ts +4 -4
- package/types/ngx-com-components-collapsible.d.ts +10 -2
- package/types/ngx-com-components-confirm.d.ts +2 -2
- package/types/ngx-com-components-datepicker.d.ts +623 -0
- package/types/ngx-com-components-dialog.d.ts +5 -2
- package/types/ngx-com-components-dropdown.d.ts +22 -4
- package/types/ngx-com-components-empty-state.d.ts +2 -0
- package/types/ngx-com-components-form-field.d.ts +4 -1
- package/types/ngx-com-components-icon-lucide.d.ts +32 -0
- package/types/ngx-com-components-icon.d.ts +49 -35
- package/types/ngx-com-components-item.d.ts +12 -2
- package/types/ngx-com-components-menu.d.ts +38 -38
- package/types/ngx-com-components-native-control.d.ts +99 -0
- package/types/ngx-com-components-paginator.d.ts +2 -0
- package/types/ngx-com-components-popover.d.ts +19 -12
- package/types/ngx-com-components-segmented-control.d.ts +3 -1
- package/types/ngx-com-components-sort.d.ts +13 -10
- package/types/ngx-com-components-switch.d.ts +7 -2
- package/types/ngx-com-components-table.d.ts +16 -2
- package/types/ngx-com-components-tabs.d.ts +46 -34
- package/types/ngx-com-components-timepicker.d.ts +273 -0
- package/types/ngx-com-components-toast.d.ts +4 -2
- package/types/ngx-com-components-tooltip.d.ts +1 -1
- package/types/ngx-com-components.d.ts +6 -7
- package/types/ngx-com-tokens.d.ts +5 -3
- package/types/ngx-com-utils.d.ts +11 -1
- package/types/ngx-com.d.ts +1 -1
|
@@ -0,0 +1,2334 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { inject, ElementRef, DestroyRef, ViewContainerRef, DOCUMENT, viewChild, model, input, output, signal, linkedSignal, computed, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
4
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
|
+
import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
|
|
6
|
+
import { Overlay, OverlayModule } from '@angular/cdk/overlay';
|
|
7
|
+
import { TemplatePortal } from '@angular/cdk/portal';
|
|
8
|
+
import * as i1 from '@angular/cdk/a11y';
|
|
9
|
+
import { A11yModule } from '@angular/cdk/a11y';
|
|
10
|
+
import { DATE_ADAPTER, ComCalendar, SingleSelectionStrategy, CALENDAR_SELECTION_STRATEGY, RangeSelectionStrategy } from 'ngx-com/components/calendar';
|
|
11
|
+
import { ComIcon } from 'ngx-com/components/icon';
|
|
12
|
+
import { mergeClasses, joinClasses } from 'ngx-com/utils';
|
|
13
|
+
import { ErrorStateMatcher, FormFieldControl } from 'ngx-com/components/form-field';
|
|
14
|
+
import { timepickerSectionVariants, createTimeValue, ComTimePicker, timepickerLabelVariants } from 'ngx-com/components/timepicker';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Types and interfaces for DatePicker and DateRangePicker components.
|
|
18
|
+
*/
|
|
19
|
+
/** Creates a DateRangeValue */
|
|
20
|
+
function createDateRangeValue(start = null, end = null) {
|
|
21
|
+
return { start, end };
|
|
22
|
+
}
|
|
23
|
+
/** Generates a unique ID for datepicker instances */
|
|
24
|
+
let datepickerIdCounter = 0;
|
|
25
|
+
function generateDatepickerId() {
|
|
26
|
+
return `com-datepicker-${datepickerIdCounter++}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* CVA variants for the datepicker trigger input.
|
|
31
|
+
* Uses semantic theme tokens for consistent cross-theme styling.
|
|
32
|
+
*
|
|
33
|
+
* @tokens `--color-input-background`, `--color-input-foreground`, `--color-input-border`,
|
|
34
|
+
* `--color-input-placeholder`, `--color-ring`, `--color-muted`, `--color-muted-hover`,
|
|
35
|
+
* `--color-warn`, `--color-success`, `--color-primary`, `--color-border`,
|
|
36
|
+
* `--color-disabled`, `--color-disabled-foreground`, `--radius-input`
|
|
37
|
+
*/
|
|
38
|
+
const datepickerTriggerVariants = cva([
|
|
39
|
+
'inline-flex',
|
|
40
|
+
'items-center',
|
|
41
|
+
'justify-between',
|
|
42
|
+
'w-full',
|
|
43
|
+
'rounded-input',
|
|
44
|
+
'border',
|
|
45
|
+
'bg-input-background',
|
|
46
|
+
'text-input-foreground',
|
|
47
|
+
'transition-colors',
|
|
48
|
+
'duration-normal',
|
|
49
|
+
'placeholder:text-input-placeholder',
|
|
50
|
+
'focus-within:outline-[1px]',
|
|
51
|
+
'focus-within:outline-offset-2',
|
|
52
|
+
'focus-within:outline-ring',
|
|
53
|
+
'cursor-pointer',
|
|
54
|
+
], {
|
|
55
|
+
variants: {
|
|
56
|
+
variant: {
|
|
57
|
+
default: [
|
|
58
|
+
'border-input-border',
|
|
59
|
+
'hover:border-border',
|
|
60
|
+
],
|
|
61
|
+
outline: [
|
|
62
|
+
'border-2',
|
|
63
|
+
'border-input-border',
|
|
64
|
+
'hover:border-foreground',
|
|
65
|
+
],
|
|
66
|
+
ghost: [
|
|
67
|
+
'border-transparent',
|
|
68
|
+
'bg-transparent',
|
|
69
|
+
'hover:bg-muted',
|
|
70
|
+
],
|
|
71
|
+
filled: [
|
|
72
|
+
'border-transparent',
|
|
73
|
+
'bg-muted',
|
|
74
|
+
'hover:bg-muted-hover',
|
|
75
|
+
],
|
|
76
|
+
naked: [
|
|
77
|
+
'border-transparent',
|
|
78
|
+
'bg-transparent',
|
|
79
|
+
'shadow-none',
|
|
80
|
+
'focus-within:outline-none',
|
|
81
|
+
'rounded-none',
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
size: {
|
|
85
|
+
sm: ['h-8', 'px-2', 'text-xs', 'gap-1'],
|
|
86
|
+
default: ['h-10', 'px-3', 'text-sm', 'gap-2'],
|
|
87
|
+
lg: ['h-12', 'px-4', 'text-base', 'gap-3'],
|
|
88
|
+
},
|
|
89
|
+
state: {
|
|
90
|
+
default: [],
|
|
91
|
+
error: [
|
|
92
|
+
'border-warn',
|
|
93
|
+
'focus-within:outline-warn',
|
|
94
|
+
],
|
|
95
|
+
success: [
|
|
96
|
+
'border-success',
|
|
97
|
+
'focus-within:outline-success',
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
open: {
|
|
101
|
+
true: ['outline-[1px]', 'outline-ring', 'border-primary'],
|
|
102
|
+
false: [],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
compoundVariants: [
|
|
106
|
+
{
|
|
107
|
+
open: true,
|
|
108
|
+
variant: 'default',
|
|
109
|
+
class: ['border-primary'],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
open: true,
|
|
113
|
+
variant: 'outline',
|
|
114
|
+
class: ['border-primary'],
|
|
115
|
+
},
|
|
116
|
+
// Naked variant should not show ring when open (form-field provides focus styling)
|
|
117
|
+
{
|
|
118
|
+
open: true,
|
|
119
|
+
variant: 'naked',
|
|
120
|
+
class: ['outline-none', 'border-transparent'],
|
|
121
|
+
},
|
|
122
|
+
// Naked variant should not show error border (form-field provides error styling)
|
|
123
|
+
{
|
|
124
|
+
state: 'error',
|
|
125
|
+
variant: 'naked',
|
|
126
|
+
class: ['border-transparent', 'focus-within:outline-none'],
|
|
127
|
+
},
|
|
128
|
+
// Naked variant should not show success border (form-field provides styling)
|
|
129
|
+
{
|
|
130
|
+
state: 'success',
|
|
131
|
+
variant: 'naked',
|
|
132
|
+
class: ['border-transparent', 'focus-within:outline-none'],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
defaultVariants: {
|
|
136
|
+
variant: 'default',
|
|
137
|
+
size: 'default',
|
|
138
|
+
state: 'default',
|
|
139
|
+
open: false,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
/**
|
|
143
|
+
* CVA variants for the disabled state of datepicker trigger.
|
|
144
|
+
*
|
|
145
|
+
* @tokens `--color-disabled`, `--color-disabled-foreground`
|
|
146
|
+
*/
|
|
147
|
+
const datepickerDisabledVariants = cva([
|
|
148
|
+
'cursor-not-allowed',
|
|
149
|
+
'bg-disabled',
|
|
150
|
+
'text-disabled-foreground',
|
|
151
|
+
'hover:border-input-border',
|
|
152
|
+
'pointer-events-none',
|
|
153
|
+
]);
|
|
154
|
+
/**
|
|
155
|
+
* CVA variants for the datepicker input field.
|
|
156
|
+
*
|
|
157
|
+
* @tokens `--color-input-foreground`, `--color-input-placeholder`
|
|
158
|
+
*/
|
|
159
|
+
const datepickerInputVariants = cva([
|
|
160
|
+
'flex-1',
|
|
161
|
+
'bg-transparent',
|
|
162
|
+
'outline-none',
|
|
163
|
+
'placeholder:text-input-placeholder',
|
|
164
|
+
'disabled:cursor-not-allowed',
|
|
165
|
+
'min-w-0',
|
|
166
|
+
'truncate',
|
|
167
|
+
], {
|
|
168
|
+
variants: {
|
|
169
|
+
size: {
|
|
170
|
+
sm: ['text-xs'],
|
|
171
|
+
default: ['text-sm'],
|
|
172
|
+
lg: ['text-base'],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
defaultVariants: {
|
|
176
|
+
size: 'default',
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
/**
|
|
180
|
+
* CVA variants for the calendar icon button.
|
|
181
|
+
*
|
|
182
|
+
* @tokens `--color-ring`, `--color-muted-foreground`, `--radius-interactive-sm`
|
|
183
|
+
*/
|
|
184
|
+
const datepickerIconVariants = cva([
|
|
185
|
+
'inline-flex',
|
|
186
|
+
'items-center',
|
|
187
|
+
'justify-center',
|
|
188
|
+
'shrink-0',
|
|
189
|
+
'text-muted-foreground',
|
|
190
|
+
'transition-colors',
|
|
191
|
+
'hover:text-foreground',
|
|
192
|
+
'focus-visible:outline-[1px]',
|
|
193
|
+
'focus-visible:outline-offset-2',
|
|
194
|
+
'focus-visible:outline-ring',
|
|
195
|
+
'rounded-interactive-sm',
|
|
196
|
+
], {
|
|
197
|
+
variants: {
|
|
198
|
+
size: {
|
|
199
|
+
sm: ['h-4', 'w-4'],
|
|
200
|
+
default: ['h-5', 'w-5'],
|
|
201
|
+
lg: ['h-6', 'w-6'],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
defaultVariants: {
|
|
205
|
+
size: 'default',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
/**
|
|
209
|
+
* CVA variants for the clear button.
|
|
210
|
+
*
|
|
211
|
+
* @tokens `--color-ring`, `--color-muted-foreground`, `--color-foreground`, `--radius-interactive-sm`
|
|
212
|
+
*/
|
|
213
|
+
const datepickerClearVariants = cva([
|
|
214
|
+
'inline-flex',
|
|
215
|
+
'items-center',
|
|
216
|
+
'justify-center',
|
|
217
|
+
'rounded-interactive-sm',
|
|
218
|
+
'text-muted-foreground',
|
|
219
|
+
'transition-colors',
|
|
220
|
+
'hover:text-foreground',
|
|
221
|
+
'focus-visible:outline-[1px]',
|
|
222
|
+
'focus-visible:outline-offset-2',
|
|
223
|
+
'focus-visible:outline-ring',
|
|
224
|
+
'shrink-0',
|
|
225
|
+
], {
|
|
226
|
+
variants: {
|
|
227
|
+
size: {
|
|
228
|
+
sm: ['h-4', 'w-4'],
|
|
229
|
+
default: ['h-5', 'w-5'],
|
|
230
|
+
lg: ['h-6', 'w-6'],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
defaultVariants: {
|
|
234
|
+
size: 'default',
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
/**
|
|
238
|
+
* CVA variants for the datepicker panel (overlay).
|
|
239
|
+
*
|
|
240
|
+
* @tokens `--color-popover`, `--color-popover-foreground`, `--color-border-subtle`, `--radius-overlay`
|
|
241
|
+
*/
|
|
242
|
+
const datepickerPanelVariants = cva([
|
|
243
|
+
'z-50',
|
|
244
|
+
'overflow-hidden',
|
|
245
|
+
'rounded-overlay',
|
|
246
|
+
'border',
|
|
247
|
+
'border-border-subtle',
|
|
248
|
+
'bg-popover',
|
|
249
|
+
'text-popover-foreground',
|
|
250
|
+
'shadow-overlay',
|
|
251
|
+
'outline-none',
|
|
252
|
+
], {
|
|
253
|
+
variants: {
|
|
254
|
+
size: {
|
|
255
|
+
sm: ['p-2', 'text-xs'],
|
|
256
|
+
default: ['p-3', 'text-sm'],
|
|
257
|
+
lg: ['p-4', 'text-base'],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
defaultVariants: {
|
|
261
|
+
size: 'default',
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
/**
|
|
265
|
+
* CVA variants for the footer section of the datepicker panel.
|
|
266
|
+
*
|
|
267
|
+
* @tokens `--color-border-subtle`
|
|
268
|
+
*/
|
|
269
|
+
const datepickerFooterVariants = cva([
|
|
270
|
+
'flex',
|
|
271
|
+
'items-center',
|
|
272
|
+
'justify-between',
|
|
273
|
+
'border-t',
|
|
274
|
+
'border-border-subtle',
|
|
275
|
+
'mt-3',
|
|
276
|
+
'pt-3',
|
|
277
|
+
], {
|
|
278
|
+
variants: {
|
|
279
|
+
size: {
|
|
280
|
+
sm: ['mt-2', 'pt-2', 'gap-1'],
|
|
281
|
+
default: ['mt-3', 'pt-3', 'gap-2'],
|
|
282
|
+
lg: ['mt-4', 'pt-4', 'gap-3'],
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
defaultVariants: {
|
|
286
|
+
size: 'default',
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
/**
|
|
290
|
+
* CVA variants for the footer buttons.
|
|
291
|
+
*
|
|
292
|
+
* @tokens `--color-primary`, `--color-primary-foreground`, `--color-primary-hover`,
|
|
293
|
+
* `--color-muted`, `--color-muted-foreground`, `--color-muted-hover`, `--radius-control-sm`
|
|
294
|
+
*/
|
|
295
|
+
const datepickerFooterButtonVariants = cva([
|
|
296
|
+
'inline-flex',
|
|
297
|
+
'items-center',
|
|
298
|
+
'justify-center',
|
|
299
|
+
'rounded-control-sm',
|
|
300
|
+
'font-medium',
|
|
301
|
+
'transition-colors',
|
|
302
|
+
'focus-visible:outline-[1px]',
|
|
303
|
+
'focus-visible:outline-offset-2',
|
|
304
|
+
'focus-visible:outline-ring',
|
|
305
|
+
], {
|
|
306
|
+
variants: {
|
|
307
|
+
size: {
|
|
308
|
+
sm: ['h-7', 'px-2', 'text-xs'],
|
|
309
|
+
default: ['h-8', 'px-3', 'text-sm'],
|
|
310
|
+
lg: ['h-9', 'px-4', 'text-base'],
|
|
311
|
+
},
|
|
312
|
+
variant: {
|
|
313
|
+
primary: [
|
|
314
|
+
'bg-primary',
|
|
315
|
+
'text-primary-foreground',
|
|
316
|
+
'hover:bg-primary-hover',
|
|
317
|
+
],
|
|
318
|
+
secondary: [
|
|
319
|
+
'bg-muted',
|
|
320
|
+
'text-muted-foreground',
|
|
321
|
+
'hover:bg-muted-hover',
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
defaultVariants: {
|
|
326
|
+
size: 'default',
|
|
327
|
+
variant: 'secondary',
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
/**
|
|
331
|
+
* CVA variants for the range separator.
|
|
332
|
+
*
|
|
333
|
+
* @tokens `--color-muted-foreground`
|
|
334
|
+
*/
|
|
335
|
+
const datepickerRangeSeparatorVariants = cva([
|
|
336
|
+
'inline-flex',
|
|
337
|
+
'items-center',
|
|
338
|
+
'justify-center',
|
|
339
|
+
'text-muted-foreground',
|
|
340
|
+
'shrink-0',
|
|
341
|
+
], {
|
|
342
|
+
variants: {
|
|
343
|
+
size: {
|
|
344
|
+
sm: ['px-1', 'text-xs'],
|
|
345
|
+
default: ['px-2', 'text-sm'],
|
|
346
|
+
lg: ['px-3', 'text-base'],
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
defaultVariants: {
|
|
350
|
+
size: 'default',
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
/** Default position for the datepicker panel. */
|
|
355
|
+
const DEFAULT_POSITIONS$1 = [
|
|
356
|
+
// Below trigger, aligned start
|
|
357
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
|
|
358
|
+
// Above trigger, aligned start
|
|
359
|
+
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
|
|
360
|
+
// Below trigger, aligned end
|
|
361
|
+
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 4 },
|
|
362
|
+
// Above trigger, aligned end
|
|
363
|
+
{ originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -4 },
|
|
364
|
+
];
|
|
365
|
+
/**
|
|
366
|
+
* Single date picker component with calendar popup.
|
|
367
|
+
* Implements ControlValueAccessor for Reactive Forms and Template-driven Forms.
|
|
368
|
+
*
|
|
369
|
+
* @tokens `--color-input-background`, `--color-input-foreground`, `--color-input-border`,
|
|
370
|
+
* `--color-input-placeholder`, `--color-ring`, `--color-muted`, `--color-muted-foreground`,
|
|
371
|
+
* `--color-popover`, `--color-popover-foreground`, `--color-border-subtle`,
|
|
372
|
+
* `--color-primary`, `--color-primary-foreground`, `--color-primary-hover`,
|
|
373
|
+
* `--color-warn`, `--color-success`, `--color-disabled`, `--color-disabled-foreground`
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* ```html
|
|
377
|
+
* <com-datepicker
|
|
378
|
+
* formControlName="birthDate"
|
|
379
|
+
* placeholder="Select date..."
|
|
380
|
+
* [min]="minDate"
|
|
381
|
+
* [max]="maxDate"
|
|
382
|
+
* [showTodayButton]="true"
|
|
383
|
+
* [showClearButton]="true"
|
|
384
|
+
* />
|
|
385
|
+
* ```
|
|
386
|
+
*/
|
|
387
|
+
class ComDatepicker {
|
|
388
|
+
elementRef = inject(ElementRef);
|
|
389
|
+
destroyRef = inject(DestroyRef);
|
|
390
|
+
overlay = inject(Overlay);
|
|
391
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
392
|
+
document = inject(DOCUMENT);
|
|
393
|
+
dateAdapter = inject(DATE_ADAPTER);
|
|
394
|
+
/** NgControl bound to this datepicker (if using reactive forms). */
|
|
395
|
+
ngControl = inject(NgControl, { optional: true, self: true });
|
|
396
|
+
defaultErrorStateMatcher = inject(ErrorStateMatcher);
|
|
397
|
+
parentForm = inject(NgForm, { optional: true });
|
|
398
|
+
parentFormGroup = inject(FormGroupDirective, { optional: true });
|
|
399
|
+
/** Reference to the trigger element. */
|
|
400
|
+
triggerRef = viewChild.required('triggerElement');
|
|
401
|
+
/** Reference to the input element. */
|
|
402
|
+
inputRef = viewChild.required('inputElement');
|
|
403
|
+
/** Reference to the panel template. */
|
|
404
|
+
panelTemplateRef = viewChild.required('panelTemplate');
|
|
405
|
+
/** Overlay reference. */
|
|
406
|
+
overlayRef = null;
|
|
407
|
+
/** Unique ID for the datepicker. */
|
|
408
|
+
datepickerId = generateDatepickerId();
|
|
409
|
+
// ============ INPUTS ============
|
|
410
|
+
/** Current value. */
|
|
411
|
+
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
412
|
+
/** Minimum selectable date. */
|
|
413
|
+
min = input(null, ...(ngDevMode ? [{ debugName: "min" }] : []));
|
|
414
|
+
/** Maximum selectable date. */
|
|
415
|
+
max = input(null, ...(ngDevMode ? [{ debugName: "max" }] : []));
|
|
416
|
+
/** Custom filter function to disable specific dates. */
|
|
417
|
+
dateFilter = input(null, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
|
|
418
|
+
/** Date the calendar opens to (defaults to selected or today). */
|
|
419
|
+
startAt = input(null, ...(ngDevMode ? [{ debugName: "startAt" }] : []));
|
|
420
|
+
/** Initial calendar view. */
|
|
421
|
+
startView = input('month', ...(ngDevMode ? [{ debugName: "startView" }] : []));
|
|
422
|
+
/** First day of week override (0=Sun, 1=Mon, ..., 6=Sat). */
|
|
423
|
+
firstDayOfWeek = input(null, ...(ngDevMode ? [{ debugName: "firstDayOfWeek" }] : []));
|
|
424
|
+
/** Placeholder text. */
|
|
425
|
+
placeholder = input('Select date...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
426
|
+
/** Whether the datepicker is disabled. */
|
|
427
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
428
|
+
/** Whether the datepicker is required. */
|
|
429
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
430
|
+
/** Display format for the date. */
|
|
431
|
+
dateFormat = input('medium', ...(ngDevMode ? [{ debugName: "dateFormat" }] : []));
|
|
432
|
+
/** Show a clear button in the trigger. */
|
|
433
|
+
showClearButton = input(false, ...(ngDevMode ? [{ debugName: "showClearButton" }] : []));
|
|
434
|
+
/** Show a today button in the footer. */
|
|
435
|
+
showTodayButton = input(false, ...(ngDevMode ? [{ debugName: "showTodayButton" }] : []));
|
|
436
|
+
/** Show a clear button in the footer. */
|
|
437
|
+
showFooterClearButton = input(false, ...(ngDevMode ? [{ debugName: "showFooterClearButton" }] : []));
|
|
438
|
+
/** Don't auto-close on selection. */
|
|
439
|
+
keepOpen = input(false, ...(ngDevMode ? [{ debugName: "keepOpen" }] : []));
|
|
440
|
+
/** Allow manual text input. */
|
|
441
|
+
allowManualInput = input(true, ...(ngDevMode ? [{ debugName: "allowManualInput" }] : []));
|
|
442
|
+
/** Additional CSS classes for the panel. */
|
|
443
|
+
panelClass = input('', ...(ngDevMode ? [{ debugName: "panelClass" }] : []));
|
|
444
|
+
/** Panel width strategy. */
|
|
445
|
+
panelWidth = input('auto', ...(ngDevMode ? [{ debugName: "panelWidth" }] : []));
|
|
446
|
+
/** CVA variant for trigger styling. */
|
|
447
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
448
|
+
/** Size variant. */
|
|
449
|
+
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
450
|
+
/** Validation state. */
|
|
451
|
+
state = input('default', ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
452
|
+
/** Additional CSS classes for the trigger. */
|
|
453
|
+
userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
|
|
454
|
+
/** Accessible label for the input. */
|
|
455
|
+
ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
456
|
+
/** ID of element describing the input. */
|
|
457
|
+
ariaDescribedBy = input(null, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
|
|
458
|
+
/** Custom error state matcher for determining when to show errors. */
|
|
459
|
+
errorStateMatcher = input(...(ngDevMode ? [undefined, { debugName: "errorStateMatcher" }] : []));
|
|
460
|
+
// Signal Forms inputs — set automatically by [formField] via setInputOnDirectives
|
|
461
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : []));
|
|
462
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : []));
|
|
463
|
+
sfErrors = input([], { ...(ngDevMode ? { debugName: "sfErrors" } : {}), alias: 'errors' });
|
|
464
|
+
/** Whether to show the time picker below the calendar. */
|
|
465
|
+
showTimePicker = input(false, ...(ngDevMode ? [{ debugName: "showTimePicker" }] : []));
|
|
466
|
+
/** 12h vs 24h format for the time picker. `null` = auto-detect. */
|
|
467
|
+
use12HourFormat = input(null, ...(ngDevMode ? [{ debugName: "use12HourFormat" }] : []));
|
|
468
|
+
/** Whether the time picker shows seconds. */
|
|
469
|
+
showSeconds = input(false, ...(ngDevMode ? [{ debugName: "showSeconds" }] : []));
|
|
470
|
+
/** Step interval for minutes in the time picker. */
|
|
471
|
+
minuteStep = input(1, ...(ngDevMode ? [{ debugName: "minuteStep" }] : []));
|
|
472
|
+
// ============ OUTPUTS ============
|
|
473
|
+
/** Emitted when a date is selected. */
|
|
474
|
+
dateChange = output();
|
|
475
|
+
/** Emitted when the panel opens. */
|
|
476
|
+
opened = output();
|
|
477
|
+
/** Emitted when the panel closes. */
|
|
478
|
+
closed = output();
|
|
479
|
+
// ============ INTERNAL STATE ============
|
|
480
|
+
/** Whether the panel is open. */
|
|
481
|
+
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
482
|
+
/** Internal value state (managed by CVA or input). */
|
|
483
|
+
internalValue = linkedSignal(() => this.value() ?? null, ...(ngDevMode ? [{ debugName: "internalValue" }] : []));
|
|
484
|
+
/** Calendar active date for navigation. Recomputes when value or startAt changes; user navigation overrides via .set(). */
|
|
485
|
+
calendarActiveDate = linkedSignal({ ...(ngDevMode ? { debugName: "calendarActiveDate" } : {}), source: () => ({ value: this.internalValue(), startAt: this.startAt() }),
|
|
486
|
+
computation: ({ value, startAt }) => value ?? startAt ?? this.dateAdapter.today() });
|
|
487
|
+
/** Live announcements for screen readers. */
|
|
488
|
+
liveAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "liveAnnouncement" }] : []));
|
|
489
|
+
/** Whether the input is focused (not panel). */
|
|
490
|
+
_inputFocused = signal(false, ...(ngDevMode ? [{ debugName: "_inputFocused" }] : []));
|
|
491
|
+
/** IDs for aria-describedby (set by form-field). */
|
|
492
|
+
_describedByIds = signal('', ...(ngDevMode ? [{ debugName: "_describedByIds" }] : []));
|
|
493
|
+
/** Form field appearance (set by form-field). */
|
|
494
|
+
_appearance = signal('outline', ...(ngDevMode ? [{ debugName: "_appearance" }] : []));
|
|
495
|
+
// ============ FormFieldControl SIGNALS ============
|
|
496
|
+
/** Whether the datepicker is focused (input focused or panel open). Implements FormFieldControl. */
|
|
497
|
+
focused = computed(() => this._inputFocused() || this.isOpen(), ...(ngDevMode ? [{ debugName: "focused" }] : []));
|
|
498
|
+
/** Whether the label should float. Label floats when focused or has a value. */
|
|
499
|
+
shouldLabelFloat = computed(() => this.focused() || this.hasValue(), ...(ngDevMode ? [{ debugName: "shouldLabelFloat" }] : []));
|
|
500
|
+
/** Whether the control is in an error state. Implements FormFieldControl. */
|
|
501
|
+
errorState = computed(() => {
|
|
502
|
+
if (!this.ngControl) {
|
|
503
|
+
// Signal Forms: gate on invalid AND touched
|
|
504
|
+
return this.invalid() && this.touched();
|
|
505
|
+
}
|
|
506
|
+
// Reactive Forms: use ErrorStateMatcher
|
|
507
|
+
this.isOpen();
|
|
508
|
+
this.hasValue();
|
|
509
|
+
const matcher = this.errorStateMatcher() ?? this.defaultErrorStateMatcher;
|
|
510
|
+
const form = this.parentFormGroup ?? this.parentForm;
|
|
511
|
+
return matcher.isErrorState(this.ngControl.control ?? null, form);
|
|
512
|
+
}, ...(ngDevMode ? [{ debugName: "errorState" }] : []));
|
|
513
|
+
/** Structured validation errors from Signal Forms, exposed for the parent form field. */
|
|
514
|
+
errors = computed(() => !this.ngControl ? this.sfErrors() : null, ...(ngDevMode ? [{ debugName: "errors" }] : []));
|
|
515
|
+
/** Unique ID for the control. Implements FormFieldControl. */
|
|
516
|
+
id = computed(() => `${this.datepickerId}-input`, ...(ngDevMode ? [{ debugName: "id" }] : []));
|
|
517
|
+
/**
|
|
518
|
+
* Effective state combining manual state with automatic error detection.
|
|
519
|
+
* Manual state takes precedence over auto-detected error state.
|
|
520
|
+
*/
|
|
521
|
+
effectiveState = computed(() => {
|
|
522
|
+
const manualState = this.state();
|
|
523
|
+
if (manualState !== 'default')
|
|
524
|
+
return manualState;
|
|
525
|
+
return this.errorState() ? 'error' : 'default';
|
|
526
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveState" }] : []));
|
|
527
|
+
/** Combined aria-describedby from form-field and manual input. */
|
|
528
|
+
effectiveAriaDescribedBy = computed(() => this._describedByIds() || this.ariaDescribedBy() || null, ...(ngDevMode ? [{ debugName: "effectiveAriaDescribedBy" }] : []));
|
|
529
|
+
// ============ COMPUTED STATE ============
|
|
530
|
+
/** Input element ID (alias for FormFieldControl id). */
|
|
531
|
+
inputId = this.id;
|
|
532
|
+
/** Panel element ID. */
|
|
533
|
+
panelId = computed(() => `${this.datepickerId}-panel`, ...(ngDevMode ? [{ debugName: "panelId" }] : []));
|
|
534
|
+
/** Whether the datepicker has a value. */
|
|
535
|
+
hasValue = computed(() => this.internalValue() !== null, ...(ngDevMode ? [{ debugName: "hasValue" }] : []));
|
|
536
|
+
/** Icon size based on datepicker size. */
|
|
537
|
+
iconSize = computed(() => {
|
|
538
|
+
const sizeMap = {
|
|
539
|
+
sm: 'sm',
|
|
540
|
+
default: 'md',
|
|
541
|
+
lg: 'lg',
|
|
542
|
+
};
|
|
543
|
+
return sizeMap[this.size()];
|
|
544
|
+
}, ...(ngDevMode ? [{ debugName: "iconSize" }] : []));
|
|
545
|
+
/** Formatted display value. */
|
|
546
|
+
displayValue = computed(() => {
|
|
547
|
+
const value = this.internalValue();
|
|
548
|
+
if (!value)
|
|
549
|
+
return '';
|
|
550
|
+
return this.dateAdapter.format(value, this.effectiveDateFormat());
|
|
551
|
+
}, ...(ngDevMode ? [{ debugName: "displayValue" }] : []));
|
|
552
|
+
/** Computed trigger classes. */
|
|
553
|
+
triggerClasses = computed(() => {
|
|
554
|
+
const baseClasses = datepickerTriggerVariants({
|
|
555
|
+
variant: this.variant(),
|
|
556
|
+
size: this.size(),
|
|
557
|
+
state: this.effectiveState(),
|
|
558
|
+
open: this.isOpen(),
|
|
559
|
+
});
|
|
560
|
+
const disabledClasses = this.disabled() ? datepickerDisabledVariants() : '';
|
|
561
|
+
// For naked variant, add padding based on form-field appearance
|
|
562
|
+
let paddingClasses = '';
|
|
563
|
+
if (this.variant() === 'naked') {
|
|
564
|
+
paddingClasses = this._appearance() === 'fill' ? 'pt-5 pb-1.5 px-3' : 'py-2.5 px-3';
|
|
565
|
+
}
|
|
566
|
+
return mergeClasses(baseClasses, disabledClasses, paddingClasses, this.userClass());
|
|
567
|
+
}, ...(ngDevMode ? [{ debugName: "triggerClasses" }] : []));
|
|
568
|
+
/** Computed input classes. */
|
|
569
|
+
inputClasses = computed(() => {
|
|
570
|
+
return datepickerInputVariants({ size: this.size() });
|
|
571
|
+
}, ...(ngDevMode ? [{ debugName: "inputClasses" }] : []));
|
|
572
|
+
/** Computed icon classes. */
|
|
573
|
+
iconClasses = computed(() => {
|
|
574
|
+
return datepickerIconVariants({ size: this.size() });
|
|
575
|
+
}, ...(ngDevMode ? [{ debugName: "iconClasses" }] : []));
|
|
576
|
+
/** Computed clear button classes. */
|
|
577
|
+
clearClasses = computed(() => {
|
|
578
|
+
return datepickerClearVariants({ size: this.size() });
|
|
579
|
+
}, ...(ngDevMode ? [{ debugName: "clearClasses" }] : []));
|
|
580
|
+
/** Computed panel classes. */
|
|
581
|
+
panelClasses = computed(() => {
|
|
582
|
+
const baseClasses = datepickerPanelVariants({ size: this.size() });
|
|
583
|
+
return joinClasses(baseClasses, this.panelClass());
|
|
584
|
+
}, ...(ngDevMode ? [{ debugName: "panelClasses" }] : []));
|
|
585
|
+
/** Computed footer classes. */
|
|
586
|
+
footerClasses = computed(() => {
|
|
587
|
+
return datepickerFooterVariants({ size: this.size() });
|
|
588
|
+
}, ...(ngDevMode ? [{ debugName: "footerClasses" }] : []));
|
|
589
|
+
/** Computed today button classes. */
|
|
590
|
+
todayButtonClasses = computed(() => {
|
|
591
|
+
return datepickerFooterButtonVariants({ size: this.size(), variant: 'primary' });
|
|
592
|
+
}, ...(ngDevMode ? [{ debugName: "todayButtonClasses" }] : []));
|
|
593
|
+
/** Computed clear button classes (footer). */
|
|
594
|
+
clearButtonClasses = computed(() => {
|
|
595
|
+
return datepickerFooterButtonVariants({ size: this.size(), variant: 'secondary' });
|
|
596
|
+
}, ...(ngDevMode ? [{ debugName: "clearButtonClasses" }] : []));
|
|
597
|
+
/** Time section divider classes. */
|
|
598
|
+
timeSectionClasses = computed(() => {
|
|
599
|
+
return timepickerSectionVariants({ size: this.size() });
|
|
600
|
+
}, ...(ngDevMode ? [{ debugName: "timeSectionClasses" }] : []));
|
|
601
|
+
/** Time value derived from the current date value. */
|
|
602
|
+
timeValue = computed(() => {
|
|
603
|
+
const date = this.internalValue();
|
|
604
|
+
if (!date)
|
|
605
|
+
return null;
|
|
606
|
+
return createTimeValue(this.dateAdapter.getHours(date), this.dateAdapter.getMinutes(date), this.dateAdapter.getSeconds(date));
|
|
607
|
+
}, ...(ngDevMode ? [{ debugName: "timeValue" }] : []));
|
|
608
|
+
/** Effective display format — switches to dateTime when time picker is shown. */
|
|
609
|
+
effectiveDateFormat = computed(() => {
|
|
610
|
+
if (this.showTimePicker()) {
|
|
611
|
+
return this.showSeconds() ? 'dateTimeLong' : 'dateTimeMedium';
|
|
612
|
+
}
|
|
613
|
+
return this.dateFormat();
|
|
614
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveDateFormat" }] : []));
|
|
615
|
+
/** Whether the panel should stay open (keepOpen or time picker shown). */
|
|
616
|
+
effectiveKeepOpen = computed(() => {
|
|
617
|
+
return this.keepOpen() || this.showTimePicker();
|
|
618
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveKeepOpen" }] : []));
|
|
619
|
+
// ============ CVA CALLBACKS ============
|
|
620
|
+
onChange = () => { };
|
|
621
|
+
onTouched = () => { };
|
|
622
|
+
onValidatorChange = () => { };
|
|
623
|
+
constructor() {
|
|
624
|
+
// Wire up NgControl if present
|
|
625
|
+
if (this.ngControl) {
|
|
626
|
+
this.ngControl.valueAccessor = this;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
ngOnDestroy() {
|
|
630
|
+
this.destroyOverlay();
|
|
631
|
+
}
|
|
632
|
+
// ============ CVA IMPLEMENTATION ============
|
|
633
|
+
writeValue(value) {
|
|
634
|
+
this.internalValue.set(value);
|
|
635
|
+
}
|
|
636
|
+
registerOnChange(fn) {
|
|
637
|
+
this.onChange = fn;
|
|
638
|
+
}
|
|
639
|
+
registerOnTouched(fn) {
|
|
640
|
+
this.onTouched = fn;
|
|
641
|
+
}
|
|
642
|
+
setDisabledState(_isDisabled) {
|
|
643
|
+
// Disabled state is handled via the disabled input
|
|
644
|
+
}
|
|
645
|
+
// ============ VALIDATOR IMPLEMENTATION ============
|
|
646
|
+
validate() {
|
|
647
|
+
const value = this.internalValue();
|
|
648
|
+
// Required validation
|
|
649
|
+
if (this.required() && !value) {
|
|
650
|
+
return { required: true };
|
|
651
|
+
}
|
|
652
|
+
if (value) {
|
|
653
|
+
// Min validation
|
|
654
|
+
const min = this.min();
|
|
655
|
+
if (min && this.dateAdapter.compareDate(value, min) < 0) {
|
|
656
|
+
return { minDate: { min, actual: value } };
|
|
657
|
+
}
|
|
658
|
+
// Max validation
|
|
659
|
+
const max = this.max();
|
|
660
|
+
if (max && this.dateAdapter.compareDate(value, max) > 0) {
|
|
661
|
+
return { maxDate: { max, actual: value } };
|
|
662
|
+
}
|
|
663
|
+
// Date filter validation
|
|
664
|
+
const dateFilter = this.dateFilter();
|
|
665
|
+
if (dateFilter && !dateFilter(value)) {
|
|
666
|
+
return { dateFilter: true };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
registerOnValidatorChange(fn) {
|
|
672
|
+
this.onValidatorChange = fn;
|
|
673
|
+
}
|
|
674
|
+
// ============ PUBLIC METHODS ============
|
|
675
|
+
/** Opens the datepicker panel. */
|
|
676
|
+
open() {
|
|
677
|
+
if (this.disabled() || this.isOpen()) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
this.createOverlay();
|
|
681
|
+
this.isOpen.set(true);
|
|
682
|
+
this.opened.emit();
|
|
683
|
+
this.announce('Calendar opened');
|
|
684
|
+
}
|
|
685
|
+
/** Closes the datepicker panel. */
|
|
686
|
+
close() {
|
|
687
|
+
if (!this.isOpen()) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
this.destroyOverlay();
|
|
691
|
+
this.isOpen.set(false);
|
|
692
|
+
this.closed.emit();
|
|
693
|
+
this.onTouched();
|
|
694
|
+
this.touched.set(true);
|
|
695
|
+
// Return focus to trigger
|
|
696
|
+
this.inputRef().nativeElement.focus();
|
|
697
|
+
}
|
|
698
|
+
/** Toggles the datepicker panel. */
|
|
699
|
+
toggle() {
|
|
700
|
+
if (this.isOpen()) {
|
|
701
|
+
this.close();
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
this.open();
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/** Clears the selected date. */
|
|
708
|
+
clear(event) {
|
|
709
|
+
event?.preventDefault();
|
|
710
|
+
event?.stopPropagation();
|
|
711
|
+
this.updateValue(null);
|
|
712
|
+
this.announce('Date cleared');
|
|
713
|
+
}
|
|
714
|
+
/** Selects today's date. */
|
|
715
|
+
selectToday() {
|
|
716
|
+
const today = this.dateAdapter.today();
|
|
717
|
+
this.updateValue(today);
|
|
718
|
+
if (!this.effectiveKeepOpen()) {
|
|
719
|
+
this.close();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// ============ EVENT HANDLERS ============
|
|
723
|
+
onInputFocus() {
|
|
724
|
+
this._inputFocused.set(true);
|
|
725
|
+
}
|
|
726
|
+
onTriggerClick() {
|
|
727
|
+
if (!this.disabled()) {
|
|
728
|
+
this.toggle();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
onTriggerKeydown(event) {
|
|
732
|
+
switch (event.key) {
|
|
733
|
+
case 'ArrowDown':
|
|
734
|
+
case 'ArrowUp':
|
|
735
|
+
event.preventDefault();
|
|
736
|
+
this.open();
|
|
737
|
+
break;
|
|
738
|
+
case 'Escape':
|
|
739
|
+
if (this.isOpen()) {
|
|
740
|
+
event.preventDefault();
|
|
741
|
+
this.close();
|
|
742
|
+
}
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
onInputKeydown(event) {
|
|
747
|
+
switch (event.key) {
|
|
748
|
+
case 'Enter':
|
|
749
|
+
event.preventDefault();
|
|
750
|
+
if (this.isOpen()) {
|
|
751
|
+
// Commit manual input
|
|
752
|
+
this.parseAndSetValue(this.inputRef().nativeElement.value);
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
this.open();
|
|
756
|
+
}
|
|
757
|
+
break;
|
|
758
|
+
case 'Escape':
|
|
759
|
+
if (this.isOpen()) {
|
|
760
|
+
event.preventDefault();
|
|
761
|
+
this.close();
|
|
762
|
+
}
|
|
763
|
+
break;
|
|
764
|
+
case 'ArrowDown':
|
|
765
|
+
if (!this.isOpen()) {
|
|
766
|
+
event.preventDefault();
|
|
767
|
+
this.open();
|
|
768
|
+
}
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
onInputChange(event) {
|
|
773
|
+
if (!this.allowManualInput()) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const _input = event.target;
|
|
777
|
+
// Debounce or wait for blur/enter to actually parse
|
|
778
|
+
// For now, we just allow typing without immediate parsing
|
|
779
|
+
}
|
|
780
|
+
onInputBlur() {
|
|
781
|
+
this._inputFocused.set(false);
|
|
782
|
+
if (this.allowManualInput()) {
|
|
783
|
+
this.parseAndSetValue(this.inputRef().nativeElement.value);
|
|
784
|
+
}
|
|
785
|
+
this.onTouched();
|
|
786
|
+
this.touched.set(true);
|
|
787
|
+
}
|
|
788
|
+
onPanelKeydown(event) {
|
|
789
|
+
switch (event.key) {
|
|
790
|
+
case 'Escape':
|
|
791
|
+
event.preventDefault();
|
|
792
|
+
this.close();
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
onDateSelected(date) {
|
|
797
|
+
// Preserve time when selecting a new date if time picker is shown
|
|
798
|
+
if (this.showTimePicker()) {
|
|
799
|
+
const currentValue = this.internalValue();
|
|
800
|
+
if (currentValue) {
|
|
801
|
+
const withTime = this.dateAdapter.setTime(date, this.dateAdapter.getHours(currentValue), this.dateAdapter.getMinutes(currentValue), this.dateAdapter.getSeconds(currentValue));
|
|
802
|
+
this.updateValue(withTime);
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
this.updateValue(date);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
this.updateValue(date);
|
|
810
|
+
}
|
|
811
|
+
if (!this.effectiveKeepOpen()) {
|
|
812
|
+
this.close();
|
|
813
|
+
}
|
|
814
|
+
this.announce(`Selected ${this.dateAdapter.format(date, 'long')}`);
|
|
815
|
+
}
|
|
816
|
+
onTimeChange(time) {
|
|
817
|
+
if (!time)
|
|
818
|
+
return;
|
|
819
|
+
const current = this.internalValue() ?? this.dateAdapter.today();
|
|
820
|
+
const updated = this.dateAdapter.setTime(current, time.hours, time.minutes, time.seconds);
|
|
821
|
+
this.updateValue(updated);
|
|
822
|
+
}
|
|
823
|
+
onActiveDateChange(date) {
|
|
824
|
+
this.calendarActiveDate.set(date);
|
|
825
|
+
}
|
|
826
|
+
// ============ FormFieldControl IMPLEMENTATION ============
|
|
827
|
+
/**
|
|
828
|
+
* Called when the form field container is clicked.
|
|
829
|
+
* Implements FormFieldControl.
|
|
830
|
+
*/
|
|
831
|
+
onContainerClick(event) {
|
|
832
|
+
const target = event.target;
|
|
833
|
+
if (!this.disabled() && !this.triggerRef().nativeElement.contains(target)) {
|
|
834
|
+
this.toggle();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Sets the describedBy IDs from the form field.
|
|
839
|
+
* Called by the parent form field component.
|
|
840
|
+
*/
|
|
841
|
+
setDescribedByIds(ids) {
|
|
842
|
+
this._describedByIds.set(ids);
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Sets the appearance for styling.
|
|
846
|
+
* Called by the parent form field component.
|
|
847
|
+
*/
|
|
848
|
+
setAppearance(appearance) {
|
|
849
|
+
this._appearance.set(appearance);
|
|
850
|
+
}
|
|
851
|
+
// ============ PRIVATE METHODS ============
|
|
852
|
+
createOverlay() {
|
|
853
|
+
if (this.overlayRef) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const hostEl = this.elementRef.nativeElement;
|
|
857
|
+
const positionStrategy = this.overlay
|
|
858
|
+
.position()
|
|
859
|
+
.flexibleConnectedTo(hostEl)
|
|
860
|
+
.withPositions(DEFAULT_POSITIONS$1)
|
|
861
|
+
.withFlexibleDimensions(false)
|
|
862
|
+
.withPush(true);
|
|
863
|
+
this.overlayRef = this.overlay.create({
|
|
864
|
+
positionStrategy,
|
|
865
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
866
|
+
hasBackdrop: true,
|
|
867
|
+
backdropClass: 'cdk-overlay-transparent-backdrop',
|
|
868
|
+
});
|
|
869
|
+
// Attach panel template
|
|
870
|
+
const portal = new TemplatePortal(this.panelTemplateRef(), this.viewContainerRef);
|
|
871
|
+
this.overlayRef.attach(portal);
|
|
872
|
+
// Close on backdrop click
|
|
873
|
+
this.overlayRef
|
|
874
|
+
.backdropClick()
|
|
875
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
876
|
+
.subscribe(() => this.close());
|
|
877
|
+
// Close on outside click
|
|
878
|
+
this.overlayRef
|
|
879
|
+
.outsidePointerEvents()
|
|
880
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
881
|
+
.subscribe(() => this.close());
|
|
882
|
+
}
|
|
883
|
+
destroyOverlay() {
|
|
884
|
+
if (this.overlayRef) {
|
|
885
|
+
this.overlayRef.dispose();
|
|
886
|
+
this.overlayRef = null;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
updateValue(value) {
|
|
890
|
+
this.value.set(value);
|
|
891
|
+
this.onChange(value);
|
|
892
|
+
this.dateChange.emit(value);
|
|
893
|
+
this.onValidatorChange();
|
|
894
|
+
}
|
|
895
|
+
parseAndSetValue(inputValue) {
|
|
896
|
+
if (!inputValue.trim()) {
|
|
897
|
+
// Empty input - clear if allowed
|
|
898
|
+
if (this.hasValue()) {
|
|
899
|
+
this.updateValue(null);
|
|
900
|
+
}
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const parsed = this.dateAdapter.parse(inputValue, this.effectiveDateFormat());
|
|
904
|
+
if (parsed && this.dateAdapter.isValid(parsed)) {
|
|
905
|
+
// Validate against min/max/filter
|
|
906
|
+
if (this.isDateValid(parsed)) {
|
|
907
|
+
this.updateValue(parsed);
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
// Invalid date - revert to current value
|
|
911
|
+
this.inputRef().nativeElement.value = this.displayValue();
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
// Parse failed - revert to current value
|
|
916
|
+
this.inputRef().nativeElement.value = this.displayValue();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
isDateValid(date) {
|
|
920
|
+
const min = this.min();
|
|
921
|
+
const max = this.max();
|
|
922
|
+
const filter = this.dateFilter();
|
|
923
|
+
if (min && this.dateAdapter.compareDate(date, min) < 0) {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
if (max && this.dateAdapter.compareDate(date, max) > 0) {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
if (filter && !filter(date)) {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
announce(message) {
|
|
935
|
+
this.liveAnnouncement.set(message);
|
|
936
|
+
}
|
|
937
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDatepicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
938
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDatepicker, isStandalone: true, selector: "com-datepicker", inputs: { value: { classPropertyName: "value", publicName: "value", 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 }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, startAt: { classPropertyName: "startAt", publicName: "startAt", isSignal: true, isRequired: false, transformFunction: null }, startView: { classPropertyName: "startView", publicName: "startView", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", 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 }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, dateFormat: { classPropertyName: "dateFormat", publicName: "dateFormat", isSignal: true, isRequired: false, transformFunction: null }, showClearButton: { classPropertyName: "showClearButton", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, showTodayButton: { classPropertyName: "showTodayButton", publicName: "showTodayButton", isSignal: true, isRequired: false, transformFunction: null }, showFooterClearButton: { classPropertyName: "showFooterClearButton", publicName: "showFooterClearButton", isSignal: true, isRequired: false, transformFunction: null }, keepOpen: { classPropertyName: "keepOpen", publicName: "keepOpen", isSignal: true, isRequired: false, transformFunction: null }, allowManualInput: { classPropertyName: "allowManualInput", publicName: "allowManualInput", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, sfErrors: { classPropertyName: "sfErrors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, showTimePicker: { classPropertyName: "showTimePicker", publicName: "showTimePicker", isSignal: true, isRequired: false, transformFunction: null }, use12HourFormat: { classPropertyName: "use12HourFormat", publicName: "use12HourFormat", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", dateChange: "dateChange", opened: "opened", closed: "closed" }, host: { properties: { "class.com-datepicker-disabled": "disabled()", "class.com-datepicker-open": "isOpen()" }, classAttribute: "com-datepicker-host inline-block" }, providers: [
|
|
939
|
+
SingleSelectionStrategy,
|
|
940
|
+
{ provide: CALENDAR_SELECTION_STRATEGY, useExisting: SingleSelectionStrategy },
|
|
941
|
+
{ provide: FormFieldControl, useExisting: forwardRef(() => ComDatepicker) },
|
|
942
|
+
], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["triggerElement"], descendants: true, isSignal: true }, { propertyName: "inputRef", first: true, predicate: ["inputElement"], descendants: true, isSignal: true }, { propertyName: "panelTemplateRef", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }], exportAs: ["comDatepicker"], ngImport: i0, template: `
|
|
943
|
+
<!-- Trigger container -->
|
|
944
|
+
<div
|
|
945
|
+
#triggerElement
|
|
946
|
+
[class]="triggerClasses()"
|
|
947
|
+
role="group"
|
|
948
|
+
tabindex="-1"
|
|
949
|
+
[attr.aria-expanded]="isOpen()"
|
|
950
|
+
[attr.aria-haspopup]="'dialog'"
|
|
951
|
+
[attr.aria-owns]="panelId()"
|
|
952
|
+
[attr.aria-disabled]="disabled() || null"
|
|
953
|
+
(click)="onTriggerClick()"
|
|
954
|
+
(keydown)="onTriggerKeydown($event)"
|
|
955
|
+
>
|
|
956
|
+
<!-- Date input display -->
|
|
957
|
+
<input
|
|
958
|
+
#inputElement
|
|
959
|
+
type="text"
|
|
960
|
+
[class]="inputClasses()"
|
|
961
|
+
[value]="displayValue()"
|
|
962
|
+
[placeholder]="placeholder()"
|
|
963
|
+
[disabled]="disabled()"
|
|
964
|
+
[readonly]="!allowManualInput()"
|
|
965
|
+
[attr.id]="inputId()"
|
|
966
|
+
[attr.aria-label]="ariaLabel() || placeholder()"
|
|
967
|
+
[attr.aria-describedby]="effectiveAriaDescribedBy() || null"
|
|
968
|
+
[attr.aria-invalid]="effectiveState() === 'error' || null"
|
|
969
|
+
[attr.aria-required]="required() || null"
|
|
970
|
+
(focus)="onInputFocus()"
|
|
971
|
+
(input)="onInputChange($event)"
|
|
972
|
+
(blur)="onInputBlur()"
|
|
973
|
+
(keydown)="onInputKeydown($event)"
|
|
974
|
+
/>
|
|
975
|
+
|
|
976
|
+
<!-- Clear button -->
|
|
977
|
+
@if (showClearButton() && hasValue() && !disabled()) {
|
|
978
|
+
<button
|
|
979
|
+
type="button"
|
|
980
|
+
[class]="clearClasses()"
|
|
981
|
+
[attr.aria-label]="'Clear date'"
|
|
982
|
+
(click)="clear($event)"
|
|
983
|
+
>
|
|
984
|
+
<com-icon name="x" [size]="iconSize()" />
|
|
985
|
+
</button>
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
<!-- Calendar icon -->
|
|
989
|
+
<button
|
|
990
|
+
type="button"
|
|
991
|
+
[class]="iconClasses()"
|
|
992
|
+
[attr.aria-label]="isOpen() ? 'Close calendar' : 'Open calendar'"
|
|
993
|
+
[disabled]="disabled()"
|
|
994
|
+
tabindex="-1"
|
|
995
|
+
>
|
|
996
|
+
<com-icon name="calendar" [size]="iconSize()" />
|
|
997
|
+
</button>
|
|
998
|
+
</div>
|
|
999
|
+
|
|
1000
|
+
<!-- Panel template (rendered in overlay) -->
|
|
1001
|
+
<ng-template #panelTemplate>
|
|
1002
|
+
<div
|
|
1003
|
+
[class]="panelClasses()"
|
|
1004
|
+
[attr.id]="panelId()"
|
|
1005
|
+
role="dialog"
|
|
1006
|
+
aria-modal="true"
|
|
1007
|
+
[attr.aria-label]="'Choose date'"
|
|
1008
|
+
(keydown)="onPanelKeydown($event)"
|
|
1009
|
+
cdkTrapFocus
|
|
1010
|
+
[cdkTrapFocusAutoCapture]="true"
|
|
1011
|
+
>
|
|
1012
|
+
<com-calendar
|
|
1013
|
+
[activeDate]="calendarActiveDate()"
|
|
1014
|
+
[selected]="internalValue()"
|
|
1015
|
+
[minDate]="min()"
|
|
1016
|
+
[maxDate]="max()"
|
|
1017
|
+
[dateFilter]="dateFilter()"
|
|
1018
|
+
[startView]="startView()"
|
|
1019
|
+
[firstDayOfWeek]="firstDayOfWeek()"
|
|
1020
|
+
[bordered]="false"
|
|
1021
|
+
(selectedChange)="onDateSelected($event)"
|
|
1022
|
+
(activeDateChange)="onActiveDateChange($event)"
|
|
1023
|
+
/>
|
|
1024
|
+
|
|
1025
|
+
@if (showTimePicker()) {
|
|
1026
|
+
<div [class]="timeSectionClasses()">
|
|
1027
|
+
<com-time-picker
|
|
1028
|
+
variant="embedded"
|
|
1029
|
+
[size]="size()"
|
|
1030
|
+
[value]="timeValue()"
|
|
1031
|
+
[use12HourFormat]="use12HourFormat()"
|
|
1032
|
+
[showSeconds]="showSeconds()"
|
|
1033
|
+
[minuteStep]="minuteStep()"
|
|
1034
|
+
[disabled]="disabled()"
|
|
1035
|
+
(timeChange)="onTimeChange($event)"
|
|
1036
|
+
/>
|
|
1037
|
+
</div>
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
@if (showTodayButton() || showFooterClearButton() || showTimePicker()) {
|
|
1041
|
+
<div [class]="footerClasses()">
|
|
1042
|
+
@if (showTodayButton()) {
|
|
1043
|
+
<button
|
|
1044
|
+
type="button"
|
|
1045
|
+
[class]="todayButtonClasses()"
|
|
1046
|
+
(click)="selectToday()"
|
|
1047
|
+
>
|
|
1048
|
+
Today
|
|
1049
|
+
</button>
|
|
1050
|
+
}
|
|
1051
|
+
@if (showFooterClearButton()) {
|
|
1052
|
+
<button
|
|
1053
|
+
type="button"
|
|
1054
|
+
[class]="clearButtonClasses()"
|
|
1055
|
+
(click)="clear($event)"
|
|
1056
|
+
>
|
|
1057
|
+
Clear
|
|
1058
|
+
</button>
|
|
1059
|
+
}
|
|
1060
|
+
@if (showTimePicker()) {
|
|
1061
|
+
<button
|
|
1062
|
+
type="button"
|
|
1063
|
+
[class]="todayButtonClasses()"
|
|
1064
|
+
(click)="close()"
|
|
1065
|
+
>
|
|
1066
|
+
Done
|
|
1067
|
+
</button>
|
|
1068
|
+
}
|
|
1069
|
+
</div>
|
|
1070
|
+
}
|
|
1071
|
+
</div>
|
|
1072
|
+
</ng-template>
|
|
1073
|
+
|
|
1074
|
+
<!-- Live announcer region -->
|
|
1075
|
+
<div class="sr-only" aria-live="polite" aria-atomic="true">
|
|
1076
|
+
{{ liveAnnouncement() }}
|
|
1077
|
+
</div>
|
|
1078
|
+
`, isInline: true, styles: [".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: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: ComCalendar, selector: "com-calendar", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter", "dateClass", "bordered", "startView", "firstDayOfWeek", "monthColumns", "cellTemplate"], outputs: ["selectedChange", "viewChanged", "activeDateChange"] }, { kind: "component", type: ComIcon, selector: "com-icon", inputs: ["name", "img", "color", "size", "strokeWidth", "absoluteStrokeWidth", "ariaLabel"] }, { kind: "component", type: ComTimePicker, selector: "com-time-picker", inputs: ["value", "disabled", "required", "showSeconds", "use12HourFormat", "minuteStep", "secondStep", "minTime", "maxTime", "variant", "size", "state", "ariaLabel", "class", "placeholder", "errorStateMatcher", "touched", "invalid", "errors"], outputs: ["valueChange", "disabledChange", "touchedChange", "timeChange"], exportAs: ["comTimePicker"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1079
|
+
}
|
|
1080
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDatepicker, decorators: [{
|
|
1081
|
+
type: Component,
|
|
1082
|
+
args: [{ selector: 'com-datepicker', exportAs: 'comDatepicker', template: `
|
|
1083
|
+
<!-- Trigger container -->
|
|
1084
|
+
<div
|
|
1085
|
+
#triggerElement
|
|
1086
|
+
[class]="triggerClasses()"
|
|
1087
|
+
role="group"
|
|
1088
|
+
tabindex="-1"
|
|
1089
|
+
[attr.aria-expanded]="isOpen()"
|
|
1090
|
+
[attr.aria-haspopup]="'dialog'"
|
|
1091
|
+
[attr.aria-owns]="panelId()"
|
|
1092
|
+
[attr.aria-disabled]="disabled() || null"
|
|
1093
|
+
(click)="onTriggerClick()"
|
|
1094
|
+
(keydown)="onTriggerKeydown($event)"
|
|
1095
|
+
>
|
|
1096
|
+
<!-- Date input display -->
|
|
1097
|
+
<input
|
|
1098
|
+
#inputElement
|
|
1099
|
+
type="text"
|
|
1100
|
+
[class]="inputClasses()"
|
|
1101
|
+
[value]="displayValue()"
|
|
1102
|
+
[placeholder]="placeholder()"
|
|
1103
|
+
[disabled]="disabled()"
|
|
1104
|
+
[readonly]="!allowManualInput()"
|
|
1105
|
+
[attr.id]="inputId()"
|
|
1106
|
+
[attr.aria-label]="ariaLabel() || placeholder()"
|
|
1107
|
+
[attr.aria-describedby]="effectiveAriaDescribedBy() || null"
|
|
1108
|
+
[attr.aria-invalid]="effectiveState() === 'error' || null"
|
|
1109
|
+
[attr.aria-required]="required() || null"
|
|
1110
|
+
(focus)="onInputFocus()"
|
|
1111
|
+
(input)="onInputChange($event)"
|
|
1112
|
+
(blur)="onInputBlur()"
|
|
1113
|
+
(keydown)="onInputKeydown($event)"
|
|
1114
|
+
/>
|
|
1115
|
+
|
|
1116
|
+
<!-- Clear button -->
|
|
1117
|
+
@if (showClearButton() && hasValue() && !disabled()) {
|
|
1118
|
+
<button
|
|
1119
|
+
type="button"
|
|
1120
|
+
[class]="clearClasses()"
|
|
1121
|
+
[attr.aria-label]="'Clear date'"
|
|
1122
|
+
(click)="clear($event)"
|
|
1123
|
+
>
|
|
1124
|
+
<com-icon name="x" [size]="iconSize()" />
|
|
1125
|
+
</button>
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
<!-- Calendar icon -->
|
|
1129
|
+
<button
|
|
1130
|
+
type="button"
|
|
1131
|
+
[class]="iconClasses()"
|
|
1132
|
+
[attr.aria-label]="isOpen() ? 'Close calendar' : 'Open calendar'"
|
|
1133
|
+
[disabled]="disabled()"
|
|
1134
|
+
tabindex="-1"
|
|
1135
|
+
>
|
|
1136
|
+
<com-icon name="calendar" [size]="iconSize()" />
|
|
1137
|
+
</button>
|
|
1138
|
+
</div>
|
|
1139
|
+
|
|
1140
|
+
<!-- Panel template (rendered in overlay) -->
|
|
1141
|
+
<ng-template #panelTemplate>
|
|
1142
|
+
<div
|
|
1143
|
+
[class]="panelClasses()"
|
|
1144
|
+
[attr.id]="panelId()"
|
|
1145
|
+
role="dialog"
|
|
1146
|
+
aria-modal="true"
|
|
1147
|
+
[attr.aria-label]="'Choose date'"
|
|
1148
|
+
(keydown)="onPanelKeydown($event)"
|
|
1149
|
+
cdkTrapFocus
|
|
1150
|
+
[cdkTrapFocusAutoCapture]="true"
|
|
1151
|
+
>
|
|
1152
|
+
<com-calendar
|
|
1153
|
+
[activeDate]="calendarActiveDate()"
|
|
1154
|
+
[selected]="internalValue()"
|
|
1155
|
+
[minDate]="min()"
|
|
1156
|
+
[maxDate]="max()"
|
|
1157
|
+
[dateFilter]="dateFilter()"
|
|
1158
|
+
[startView]="startView()"
|
|
1159
|
+
[firstDayOfWeek]="firstDayOfWeek()"
|
|
1160
|
+
[bordered]="false"
|
|
1161
|
+
(selectedChange)="onDateSelected($event)"
|
|
1162
|
+
(activeDateChange)="onActiveDateChange($event)"
|
|
1163
|
+
/>
|
|
1164
|
+
|
|
1165
|
+
@if (showTimePicker()) {
|
|
1166
|
+
<div [class]="timeSectionClasses()">
|
|
1167
|
+
<com-time-picker
|
|
1168
|
+
variant="embedded"
|
|
1169
|
+
[size]="size()"
|
|
1170
|
+
[value]="timeValue()"
|
|
1171
|
+
[use12HourFormat]="use12HourFormat()"
|
|
1172
|
+
[showSeconds]="showSeconds()"
|
|
1173
|
+
[minuteStep]="minuteStep()"
|
|
1174
|
+
[disabled]="disabled()"
|
|
1175
|
+
(timeChange)="onTimeChange($event)"
|
|
1176
|
+
/>
|
|
1177
|
+
</div>
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
@if (showTodayButton() || showFooterClearButton() || showTimePicker()) {
|
|
1181
|
+
<div [class]="footerClasses()">
|
|
1182
|
+
@if (showTodayButton()) {
|
|
1183
|
+
<button
|
|
1184
|
+
type="button"
|
|
1185
|
+
[class]="todayButtonClasses()"
|
|
1186
|
+
(click)="selectToday()"
|
|
1187
|
+
>
|
|
1188
|
+
Today
|
|
1189
|
+
</button>
|
|
1190
|
+
}
|
|
1191
|
+
@if (showFooterClearButton()) {
|
|
1192
|
+
<button
|
|
1193
|
+
type="button"
|
|
1194
|
+
[class]="clearButtonClasses()"
|
|
1195
|
+
(click)="clear($event)"
|
|
1196
|
+
>
|
|
1197
|
+
Clear
|
|
1198
|
+
</button>
|
|
1199
|
+
}
|
|
1200
|
+
@if (showTimePicker()) {
|
|
1201
|
+
<button
|
|
1202
|
+
type="button"
|
|
1203
|
+
[class]="todayButtonClasses()"
|
|
1204
|
+
(click)="close()"
|
|
1205
|
+
>
|
|
1206
|
+
Done
|
|
1207
|
+
</button>
|
|
1208
|
+
}
|
|
1209
|
+
</div>
|
|
1210
|
+
}
|
|
1211
|
+
</div>
|
|
1212
|
+
</ng-template>
|
|
1213
|
+
|
|
1214
|
+
<!-- Live announcer region -->
|
|
1215
|
+
<div class="sr-only" aria-live="polite" aria-atomic="true">
|
|
1216
|
+
{{ liveAnnouncement() }}
|
|
1217
|
+
</div>
|
|
1218
|
+
`, imports: [
|
|
1219
|
+
OverlayModule,
|
|
1220
|
+
A11yModule,
|
|
1221
|
+
ComCalendar,
|
|
1222
|
+
ComIcon,
|
|
1223
|
+
ComTimePicker,
|
|
1224
|
+
], providers: [
|
|
1225
|
+
SingleSelectionStrategy,
|
|
1226
|
+
{ provide: CALENDAR_SELECTION_STRATEGY, useExisting: SingleSelectionStrategy },
|
|
1227
|
+
{ provide: FormFieldControl, useExisting: forwardRef(() => ComDatepicker) },
|
|
1228
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1229
|
+
class: 'com-datepicker-host inline-block',
|
|
1230
|
+
'[class.com-datepicker-disabled]': 'disabled()',
|
|
1231
|
+
'[class.com-datepicker-open]': 'isOpen()',
|
|
1232
|
+
}, styles: [".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"] }]
|
|
1233
|
+
}], ctorParameters: () => [], propDecorators: { triggerRef: [{ type: i0.ViewChild, args: ['triggerElement', { isSignal: true }] }], inputRef: [{ type: i0.ViewChild, args: ['inputElement', { isSignal: true }] }], panelTemplateRef: [{ type: i0.ViewChild, args: ['panelTemplate', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], startAt: [{ type: i0.Input, args: [{ isSignal: true, alias: "startAt", required: false }] }], startView: [{ type: i0.Input, args: [{ isSignal: true, alias: "startView", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], dateFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFormat", required: false }] }], showClearButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], showTodayButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTodayButton", required: false }] }], showFooterClearButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooterClearButton", required: false }] }], keepOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepOpen", required: false }] }], allowManualInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowManualInput", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], sfErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], showTimePicker: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTimePicker", required: false }] }], use12HourFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "use12HourFormat", required: false }] }], showSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeconds", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], dateChange: [{ type: i0.Output, args: ["dateChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
1234
|
+
|
|
1235
|
+
/** Default position for the datepicker panel. */
|
|
1236
|
+
const DEFAULT_POSITIONS = [
|
|
1237
|
+
// Below trigger, aligned start
|
|
1238
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
|
|
1239
|
+
// Above trigger, aligned start
|
|
1240
|
+
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
|
|
1241
|
+
// Below trigger, aligned end
|
|
1242
|
+
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 4 },
|
|
1243
|
+
// Above trigger, aligned end
|
|
1244
|
+
{ originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -4 },
|
|
1245
|
+
];
|
|
1246
|
+
/**
|
|
1247
|
+
* Date range picker component with calendar popup.
|
|
1248
|
+
* Allows selecting a start and end date via a two-click interaction.
|
|
1249
|
+
* Implements ControlValueAccessor for Reactive Forms and Template-driven Forms.
|
|
1250
|
+
*
|
|
1251
|
+
* @tokens `--color-input-background`, `--color-input-foreground`, `--color-input-border`,
|
|
1252
|
+
* `--color-input-placeholder`, `--color-ring`, `--color-muted`, `--color-muted-foreground`,
|
|
1253
|
+
* `--color-popover`, `--color-popover-foreground`, `--color-border-subtle`,
|
|
1254
|
+
* `--color-primary`, `--color-primary-foreground`, `--color-primary-hover`,
|
|
1255
|
+
* `--color-warn`, `--color-success`, `--color-disabled`, `--color-disabled-foreground`
|
|
1256
|
+
*
|
|
1257
|
+
* @example
|
|
1258
|
+
* ```html
|
|
1259
|
+
* <com-date-range-picker
|
|
1260
|
+
* formControlName="dateRange"
|
|
1261
|
+
* startPlaceholder="Start date"
|
|
1262
|
+
* endPlaceholder="End date"
|
|
1263
|
+
* [min]="minDate"
|
|
1264
|
+
* [max]="maxDate"
|
|
1265
|
+
* [showTodayButton]="true"
|
|
1266
|
+
* />
|
|
1267
|
+
* ```
|
|
1268
|
+
*/
|
|
1269
|
+
class ComDateRangePicker {
|
|
1270
|
+
elementRef = inject(ElementRef);
|
|
1271
|
+
destroyRef = inject(DestroyRef);
|
|
1272
|
+
overlay = inject(Overlay);
|
|
1273
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
1274
|
+
document = inject(DOCUMENT);
|
|
1275
|
+
dateAdapter = inject(DATE_ADAPTER);
|
|
1276
|
+
/** NgControl bound to this date range picker (if using reactive forms). */
|
|
1277
|
+
ngControl = inject(NgControl, { optional: true, self: true });
|
|
1278
|
+
defaultErrorStateMatcher = inject(ErrorStateMatcher);
|
|
1279
|
+
parentForm = inject(NgForm, { optional: true });
|
|
1280
|
+
parentFormGroup = inject(FormGroupDirective, { optional: true });
|
|
1281
|
+
/** Reference to the trigger element. */
|
|
1282
|
+
triggerRef = viewChild.required('triggerElement');
|
|
1283
|
+
/** Reference to the start input element. */
|
|
1284
|
+
startInputRef = viewChild.required('startInputElement');
|
|
1285
|
+
/** Reference to the end input element. */
|
|
1286
|
+
endInputRef = viewChild.required('endInputElement');
|
|
1287
|
+
/** Reference to the panel template. */
|
|
1288
|
+
panelTemplateRef = viewChild.required('panelTemplate');
|
|
1289
|
+
/** Overlay reference. */
|
|
1290
|
+
overlayRef = null;
|
|
1291
|
+
/** Unique ID for the datepicker. */
|
|
1292
|
+
datepickerId = generateDatepickerId();
|
|
1293
|
+
// ============ INPUTS ============
|
|
1294
|
+
/** Current value. */
|
|
1295
|
+
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1296
|
+
/** Minimum selectable date. */
|
|
1297
|
+
min = input(null, ...(ngDevMode ? [{ debugName: "min" }] : []));
|
|
1298
|
+
/** Maximum selectable date. */
|
|
1299
|
+
max = input(null, ...(ngDevMode ? [{ debugName: "max" }] : []));
|
|
1300
|
+
/** Custom filter function to disable specific dates. */
|
|
1301
|
+
dateFilter = input(null, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
|
|
1302
|
+
/** Date the calendar opens to (defaults to start date or today). */
|
|
1303
|
+
startAt = input(null, ...(ngDevMode ? [{ debugName: "startAt" }] : []));
|
|
1304
|
+
/** Initial calendar view. */
|
|
1305
|
+
startView = input('month', ...(ngDevMode ? [{ debugName: "startView" }] : []));
|
|
1306
|
+
/** First day of week override (0=Sun, 1=Mon, ..., 6=Sat). */
|
|
1307
|
+
firstDayOfWeek = input(null, ...(ngDevMode ? [{ debugName: "firstDayOfWeek" }] : []));
|
|
1308
|
+
/** Placeholder text for start date. */
|
|
1309
|
+
startPlaceholder = input('Start date', ...(ngDevMode ? [{ debugName: "startPlaceholder" }] : []));
|
|
1310
|
+
/** Placeholder text for end date. */
|
|
1311
|
+
endPlaceholder = input('End date', ...(ngDevMode ? [{ debugName: "endPlaceholder" }] : []));
|
|
1312
|
+
/** Whether the datepicker is disabled. */
|
|
1313
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
1314
|
+
/** Whether the datepicker is required. */
|
|
1315
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
1316
|
+
/** Display format for the date. */
|
|
1317
|
+
dateFormat = input('medium', ...(ngDevMode ? [{ debugName: "dateFormat" }] : []));
|
|
1318
|
+
/** Show a clear button in the trigger. */
|
|
1319
|
+
showClearButton = input(false, ...(ngDevMode ? [{ debugName: "showClearButton" }] : []));
|
|
1320
|
+
/** Show a today button in the footer. */
|
|
1321
|
+
showTodayButton = input(false, ...(ngDevMode ? [{ debugName: "showTodayButton" }] : []));
|
|
1322
|
+
/** Show a clear button in the footer. */
|
|
1323
|
+
showFooterClearButton = input(false, ...(ngDevMode ? [{ debugName: "showFooterClearButton" }] : []));
|
|
1324
|
+
/** Don't auto-close on complete range selection. */
|
|
1325
|
+
keepOpen = input(false, ...(ngDevMode ? [{ debugName: "keepOpen" }] : []));
|
|
1326
|
+
/** Allow manual text input. */
|
|
1327
|
+
allowManualInput = input(true, ...(ngDevMode ? [{ debugName: "allowManualInput" }] : []));
|
|
1328
|
+
/** Additional CSS classes for the panel. */
|
|
1329
|
+
panelClass = input('', ...(ngDevMode ? [{ debugName: "panelClass" }] : []));
|
|
1330
|
+
/** Panel width strategy. */
|
|
1331
|
+
panelWidth = input('auto', ...(ngDevMode ? [{ debugName: "panelWidth" }] : []));
|
|
1332
|
+
/** CVA variant for trigger styling. */
|
|
1333
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
1334
|
+
/** Size variant. */
|
|
1335
|
+
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
1336
|
+
/** Validation state. */
|
|
1337
|
+
state = input('default', ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
1338
|
+
/** Additional CSS classes for the trigger. */
|
|
1339
|
+
userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
|
|
1340
|
+
/** Accessible label for the start input. */
|
|
1341
|
+
startAriaLabel = input(null, ...(ngDevMode ? [{ debugName: "startAriaLabel" }] : []));
|
|
1342
|
+
/** Accessible label for the end input. */
|
|
1343
|
+
endAriaLabel = input(null, ...(ngDevMode ? [{ debugName: "endAriaLabel" }] : []));
|
|
1344
|
+
/** Custom error state matcher for determining when to show errors. */
|
|
1345
|
+
errorStateMatcher = input(...(ngDevMode ? [undefined, { debugName: "errorStateMatcher" }] : []));
|
|
1346
|
+
// Signal Forms inputs — set automatically by [formField] via setInputOnDirectives
|
|
1347
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : []));
|
|
1348
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : []));
|
|
1349
|
+
sfErrors = input([], { ...(ngDevMode ? { debugName: "sfErrors" } : {}), alias: 'errors' });
|
|
1350
|
+
/** Whether to show time pickers below the calendar. */
|
|
1351
|
+
showTimePicker = input(false, ...(ngDevMode ? [{ debugName: "showTimePicker" }] : []));
|
|
1352
|
+
/** 12h vs 24h format for the time pickers. `null` = auto-detect. */
|
|
1353
|
+
use12HourFormat = input(null, ...(ngDevMode ? [{ debugName: "use12HourFormat" }] : []));
|
|
1354
|
+
/** Whether the time pickers show seconds. */
|
|
1355
|
+
showSeconds = input(false, ...(ngDevMode ? [{ debugName: "showSeconds" }] : []));
|
|
1356
|
+
/** Step interval for minutes in the time pickers. */
|
|
1357
|
+
minuteStep = input(1, ...(ngDevMode ? [{ debugName: "minuteStep" }] : []));
|
|
1358
|
+
// ============ OUTPUTS ============
|
|
1359
|
+
/** Emitted when a complete range is selected. */
|
|
1360
|
+
rangeChange = output();
|
|
1361
|
+
/** Emitted when the panel opens. */
|
|
1362
|
+
opened = output();
|
|
1363
|
+
/** Emitted when the panel closes. */
|
|
1364
|
+
closed = output();
|
|
1365
|
+
// ============ INTERNAL STATE ============
|
|
1366
|
+
/** Whether the panel is open. */
|
|
1367
|
+
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
1368
|
+
/** Which input is currently focused. */
|
|
1369
|
+
activeInput = signal(null, ...(ngDevMode ? [{ debugName: "activeInput" }] : []));
|
|
1370
|
+
/** Internal value state (managed by CVA or input). */
|
|
1371
|
+
internalValue = linkedSignal(() => this.value() ?? null, ...(ngDevMode ? [{ debugName: "internalValue" }] : []));
|
|
1372
|
+
/** Calendar active date for navigation. Recomputes when value or startAt changes; user navigation overrides via .set(). */
|
|
1373
|
+
calendarActiveDate = linkedSignal({ ...(ngDevMode ? { debugName: "calendarActiveDate" } : {}), source: () => ({ value: this.internalValue(), startAt: this.startAt() }),
|
|
1374
|
+
computation: ({ value, startAt }) => value?.start ?? startAt ?? this.dateAdapter.today() });
|
|
1375
|
+
/** Live announcements for screen readers. */
|
|
1376
|
+
liveAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "liveAnnouncement" }] : []));
|
|
1377
|
+
/** Whether any input is focused. */
|
|
1378
|
+
_inputFocused = signal(false, ...(ngDevMode ? [{ debugName: "_inputFocused" }] : []));
|
|
1379
|
+
/** IDs for aria-describedby (set by form-field). */
|
|
1380
|
+
_describedByIds = signal('', ...(ngDevMode ? [{ debugName: "_describedByIds" }] : []));
|
|
1381
|
+
/** Form field appearance (set by form-field). */
|
|
1382
|
+
_appearance = signal('outline', ...(ngDevMode ? [{ debugName: "_appearance" }] : []));
|
|
1383
|
+
// ============ FormFieldControl SIGNALS ============
|
|
1384
|
+
/** Whether the date range picker is focused. Implements FormFieldControl. */
|
|
1385
|
+
focused = computed(() => this._inputFocused() || this.isOpen(), ...(ngDevMode ? [{ debugName: "focused" }] : []));
|
|
1386
|
+
/** Whether the label should float. */
|
|
1387
|
+
shouldLabelFloat = computed(() => this.focused() || this.hasValue(), ...(ngDevMode ? [{ debugName: "shouldLabelFloat" }] : []));
|
|
1388
|
+
/** Whether the control is in an error state. Implements FormFieldControl. */
|
|
1389
|
+
errorState = computed(() => {
|
|
1390
|
+
if (!this.ngControl) {
|
|
1391
|
+
return this.invalid() && this.touched();
|
|
1392
|
+
}
|
|
1393
|
+
this.isOpen();
|
|
1394
|
+
this.hasValue();
|
|
1395
|
+
const matcher = this.errorStateMatcher() ?? this.defaultErrorStateMatcher;
|
|
1396
|
+
const form = this.parentFormGroup ?? this.parentForm;
|
|
1397
|
+
return matcher.isErrorState(this.ngControl.control ?? null, form);
|
|
1398
|
+
}, ...(ngDevMode ? [{ debugName: "errorState" }] : []));
|
|
1399
|
+
/** Structured validation errors from Signal Forms. */
|
|
1400
|
+
errors = computed(() => !this.ngControl ? this.sfErrors() : null, ...(ngDevMode ? [{ debugName: "errors" }] : []));
|
|
1401
|
+
/** Unique ID for the control (maps to start input). Implements FormFieldControl. */
|
|
1402
|
+
id = computed(() => `${this.datepickerId}-start`, ...(ngDevMode ? [{ debugName: "id" }] : []));
|
|
1403
|
+
/**
|
|
1404
|
+
* Effective state combining manual state with automatic error detection.
|
|
1405
|
+
*/
|
|
1406
|
+
effectiveState = computed(() => {
|
|
1407
|
+
const manualState = this.state();
|
|
1408
|
+
if (manualState !== 'default')
|
|
1409
|
+
return manualState;
|
|
1410
|
+
return this.errorState() ? 'error' : 'default';
|
|
1411
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveState" }] : []));
|
|
1412
|
+
/** Combined aria-describedby from form-field. */
|
|
1413
|
+
effectiveAriaDescribedBy = computed(() => this._describedByIds() || null, ...(ngDevMode ? [{ debugName: "effectiveAriaDescribedBy" }] : []));
|
|
1414
|
+
// ============ COMPUTED STATE ============
|
|
1415
|
+
/** Start input element ID (alias for FormFieldControl id). */
|
|
1416
|
+
startInputId = this.id;
|
|
1417
|
+
/** End input element ID. */
|
|
1418
|
+
endInputId = computed(() => `${this.datepickerId}-end`, ...(ngDevMode ? [{ debugName: "endInputId" }] : []));
|
|
1419
|
+
/** Panel element ID. */
|
|
1420
|
+
panelId = computed(() => `${this.datepickerId}-panel`, ...(ngDevMode ? [{ debugName: "panelId" }] : []));
|
|
1421
|
+
/** Whether the datepicker has a value. */
|
|
1422
|
+
hasValue = computed(() => {
|
|
1423
|
+
const value = this.internalValue();
|
|
1424
|
+
return value !== null && (value.start !== null || value.end !== null);
|
|
1425
|
+
}, ...(ngDevMode ? [{ debugName: "hasValue" }] : []));
|
|
1426
|
+
/** Icon size based on datepicker size. */
|
|
1427
|
+
iconSize = computed(() => {
|
|
1428
|
+
const sizeMap = {
|
|
1429
|
+
sm: 'sm',
|
|
1430
|
+
default: 'md',
|
|
1431
|
+
lg: 'lg',
|
|
1432
|
+
};
|
|
1433
|
+
return sizeMap[this.size()];
|
|
1434
|
+
}, ...(ngDevMode ? [{ debugName: "iconSize" }] : []));
|
|
1435
|
+
/** Calendar selection (converts DateRangeValue to DateRange for calendar). */
|
|
1436
|
+
calendarSelection = computed(() => {
|
|
1437
|
+
const value = this.internalValue();
|
|
1438
|
+
if (!value)
|
|
1439
|
+
return null;
|
|
1440
|
+
return { start: value.start, end: value.end };
|
|
1441
|
+
}, ...(ngDevMode ? [{ debugName: "calendarSelection" }] : []));
|
|
1442
|
+
/** Formatted start display value. */
|
|
1443
|
+
startDisplayValue = computed(() => {
|
|
1444
|
+
const value = this.internalValue();
|
|
1445
|
+
if (!value?.start)
|
|
1446
|
+
return '';
|
|
1447
|
+
return this.dateAdapter.format(value.start, this.effectiveDateFormat());
|
|
1448
|
+
}, ...(ngDevMode ? [{ debugName: "startDisplayValue" }] : []));
|
|
1449
|
+
/** Formatted end display value. */
|
|
1450
|
+
endDisplayValue = computed(() => {
|
|
1451
|
+
const value = this.internalValue();
|
|
1452
|
+
if (!value?.end)
|
|
1453
|
+
return '';
|
|
1454
|
+
return this.dateAdapter.format(value.end, this.effectiveDateFormat());
|
|
1455
|
+
}, ...(ngDevMode ? [{ debugName: "endDisplayValue" }] : []));
|
|
1456
|
+
/** Computed trigger classes. */
|
|
1457
|
+
triggerClasses = computed(() => {
|
|
1458
|
+
const baseClasses = datepickerTriggerVariants({
|
|
1459
|
+
variant: this.variant(),
|
|
1460
|
+
size: this.size(),
|
|
1461
|
+
state: this.effectiveState(),
|
|
1462
|
+
open: this.isOpen(),
|
|
1463
|
+
});
|
|
1464
|
+
const disabledClasses = this.disabled() ? datepickerDisabledVariants() : '';
|
|
1465
|
+
// For naked variant, add padding based on form-field appearance
|
|
1466
|
+
let paddingClasses = '';
|
|
1467
|
+
if (this.variant() === 'naked') {
|
|
1468
|
+
paddingClasses = this._appearance() === 'fill' ? 'pt-5 pb-1.5 px-3' : 'py-2.5 px-3';
|
|
1469
|
+
}
|
|
1470
|
+
return mergeClasses(baseClasses, disabledClasses, paddingClasses, this.userClass());
|
|
1471
|
+
}, ...(ngDevMode ? [{ debugName: "triggerClasses" }] : []));
|
|
1472
|
+
/** Computed input classes. */
|
|
1473
|
+
inputClasses = computed(() => {
|
|
1474
|
+
return datepickerInputVariants({ size: this.size() });
|
|
1475
|
+
}, ...(ngDevMode ? [{ debugName: "inputClasses" }] : []));
|
|
1476
|
+
/** Computed separator classes. */
|
|
1477
|
+
separatorClasses = computed(() => {
|
|
1478
|
+
return datepickerRangeSeparatorVariants({ size: this.size() });
|
|
1479
|
+
}, ...(ngDevMode ? [{ debugName: "separatorClasses" }] : []));
|
|
1480
|
+
/** Computed icon classes. */
|
|
1481
|
+
iconClasses = computed(() => {
|
|
1482
|
+
return datepickerIconVariants({ size: this.size() });
|
|
1483
|
+
}, ...(ngDevMode ? [{ debugName: "iconClasses" }] : []));
|
|
1484
|
+
/** Computed clear button classes. */
|
|
1485
|
+
clearClasses = computed(() => {
|
|
1486
|
+
return datepickerClearVariants({ size: this.size() });
|
|
1487
|
+
}, ...(ngDevMode ? [{ debugName: "clearClasses" }] : []));
|
|
1488
|
+
/** Computed panel classes. */
|
|
1489
|
+
panelClasses = computed(() => {
|
|
1490
|
+
const baseClasses = datepickerPanelVariants({ size: this.size() });
|
|
1491
|
+
return joinClasses(baseClasses, this.panelClass());
|
|
1492
|
+
}, ...(ngDevMode ? [{ debugName: "panelClasses" }] : []));
|
|
1493
|
+
/** Computed footer classes. */
|
|
1494
|
+
footerClasses = computed(() => {
|
|
1495
|
+
return datepickerFooterVariants({ size: this.size() });
|
|
1496
|
+
}, ...(ngDevMode ? [{ debugName: "footerClasses" }] : []));
|
|
1497
|
+
/** Computed today button classes. */
|
|
1498
|
+
todayButtonClasses = computed(() => {
|
|
1499
|
+
return datepickerFooterButtonVariants({ size: this.size(), variant: 'primary' });
|
|
1500
|
+
}, ...(ngDevMode ? [{ debugName: "todayButtonClasses" }] : []));
|
|
1501
|
+
/** Computed clear button classes (footer). */
|
|
1502
|
+
clearButtonClasses = computed(() => {
|
|
1503
|
+
return datepickerFooterButtonVariants({ size: this.size(), variant: 'secondary' });
|
|
1504
|
+
}, ...(ngDevMode ? [{ debugName: "clearButtonClasses" }] : []));
|
|
1505
|
+
/** Time section divider classes. */
|
|
1506
|
+
timeSectionClasses = computed(() => {
|
|
1507
|
+
return timepickerSectionVariants({ size: this.size() });
|
|
1508
|
+
}, ...(ngDevMode ? [{ debugName: "timeSectionClasses" }] : []));
|
|
1509
|
+
/** Time label classes. */
|
|
1510
|
+
timeLabelClasses = computed(() => {
|
|
1511
|
+
return timepickerLabelVariants({ size: this.size() });
|
|
1512
|
+
}, ...(ngDevMode ? [{ debugName: "timeLabelClasses" }] : []));
|
|
1513
|
+
/** Start time value derived from the start date. */
|
|
1514
|
+
startTimeValue = computed(() => {
|
|
1515
|
+
const value = this.internalValue();
|
|
1516
|
+
if (!value?.start)
|
|
1517
|
+
return null;
|
|
1518
|
+
return createTimeValue(this.dateAdapter.getHours(value.start), this.dateAdapter.getMinutes(value.start), this.dateAdapter.getSeconds(value.start));
|
|
1519
|
+
}, ...(ngDevMode ? [{ debugName: "startTimeValue" }] : []));
|
|
1520
|
+
/** End time value derived from the end date. */
|
|
1521
|
+
endTimeValue = computed(() => {
|
|
1522
|
+
const value = this.internalValue();
|
|
1523
|
+
if (!value?.end)
|
|
1524
|
+
return null;
|
|
1525
|
+
return createTimeValue(this.dateAdapter.getHours(value.end), this.dateAdapter.getMinutes(value.end), this.dateAdapter.getSeconds(value.end));
|
|
1526
|
+
}, ...(ngDevMode ? [{ debugName: "endTimeValue" }] : []));
|
|
1527
|
+
/** Effective display format — switches to dateTime when time picker is shown. */
|
|
1528
|
+
effectiveDateFormat = computed(() => {
|
|
1529
|
+
if (this.showTimePicker()) {
|
|
1530
|
+
return this.showSeconds() ? 'dateTimeLong' : 'dateTimeMedium';
|
|
1531
|
+
}
|
|
1532
|
+
return this.dateFormat();
|
|
1533
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveDateFormat" }] : []));
|
|
1534
|
+
/** Whether the panel should stay open (keepOpen or time picker shown). */
|
|
1535
|
+
effectiveKeepOpen = computed(() => {
|
|
1536
|
+
return this.keepOpen() || this.showTimePicker();
|
|
1537
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveKeepOpen" }] : []));
|
|
1538
|
+
// ============ CVA CALLBACKS ============
|
|
1539
|
+
onChange = () => { };
|
|
1540
|
+
onTouched = () => { };
|
|
1541
|
+
onValidatorChange = () => { };
|
|
1542
|
+
constructor() {
|
|
1543
|
+
// Wire up NgControl if present
|
|
1544
|
+
if (this.ngControl) {
|
|
1545
|
+
this.ngControl.valueAccessor = this;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
ngOnDestroy() {
|
|
1549
|
+
this.destroyOverlay();
|
|
1550
|
+
}
|
|
1551
|
+
// ============ CVA IMPLEMENTATION ============
|
|
1552
|
+
writeValue(value) {
|
|
1553
|
+
this.internalValue.set(value);
|
|
1554
|
+
}
|
|
1555
|
+
registerOnChange(fn) {
|
|
1556
|
+
this.onChange = fn;
|
|
1557
|
+
}
|
|
1558
|
+
registerOnTouched(fn) {
|
|
1559
|
+
this.onTouched = fn;
|
|
1560
|
+
}
|
|
1561
|
+
setDisabledState(_isDisabled) {
|
|
1562
|
+
// Disabled state is handled via the disabled input
|
|
1563
|
+
}
|
|
1564
|
+
// ============ VALIDATOR IMPLEMENTATION ============
|
|
1565
|
+
validate() {
|
|
1566
|
+
const value = this.internalValue();
|
|
1567
|
+
// Required validation
|
|
1568
|
+
if (this.required() && (!value || (!value.start && !value.end))) {
|
|
1569
|
+
return { required: true };
|
|
1570
|
+
}
|
|
1571
|
+
if (value) {
|
|
1572
|
+
const { start, end } = value;
|
|
1573
|
+
const min = this.min();
|
|
1574
|
+
const max = this.max();
|
|
1575
|
+
const dateFilter = this.dateFilter();
|
|
1576
|
+
// Min validation for start
|
|
1577
|
+
if (start && min && this.dateAdapter.compareDate(start, min) < 0) {
|
|
1578
|
+
return { minDate: { min, actual: start } };
|
|
1579
|
+
}
|
|
1580
|
+
// Max validation for end
|
|
1581
|
+
if (end && max && this.dateAdapter.compareDate(end, max) > 0) {
|
|
1582
|
+
return { maxDate: { max, actual: end } };
|
|
1583
|
+
}
|
|
1584
|
+
// Date filter validation
|
|
1585
|
+
if (start && dateFilter && !dateFilter(start)) {
|
|
1586
|
+
return { dateFilter: { date: start } };
|
|
1587
|
+
}
|
|
1588
|
+
if (end && dateFilter && !dateFilter(end)) {
|
|
1589
|
+
return { dateFilter: { date: end } };
|
|
1590
|
+
}
|
|
1591
|
+
// Range validation (start must be before or equal to end)
|
|
1592
|
+
if (start && end && this.dateAdapter.compareDate(start, end) > 0) {
|
|
1593
|
+
return { rangeInvalid: { start, end } };
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
registerOnValidatorChange(fn) {
|
|
1599
|
+
this.onValidatorChange = fn;
|
|
1600
|
+
}
|
|
1601
|
+
// ============ PUBLIC METHODS ============
|
|
1602
|
+
/** Opens the datepicker panel. */
|
|
1603
|
+
open() {
|
|
1604
|
+
if (this.disabled() || this.isOpen()) {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
this.createOverlay();
|
|
1608
|
+
this.isOpen.set(true);
|
|
1609
|
+
this.opened.emit();
|
|
1610
|
+
this.announce('Calendar opened. Select a start date.');
|
|
1611
|
+
}
|
|
1612
|
+
/** Closes the datepicker panel. */
|
|
1613
|
+
close() {
|
|
1614
|
+
if (!this.isOpen()) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
this.destroyOverlay();
|
|
1618
|
+
this.isOpen.set(false);
|
|
1619
|
+
this.closed.emit();
|
|
1620
|
+
this.onTouched();
|
|
1621
|
+
this.touched.set(true);
|
|
1622
|
+
// Return focus to appropriate input
|
|
1623
|
+
const activeInput = this.activeInput();
|
|
1624
|
+
if (activeInput === 'end') {
|
|
1625
|
+
this.endInputRef().nativeElement.focus();
|
|
1626
|
+
}
|
|
1627
|
+
else {
|
|
1628
|
+
this.startInputRef().nativeElement.focus();
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
/** Toggles the datepicker panel. */
|
|
1632
|
+
toggle() {
|
|
1633
|
+
if (this.isOpen()) {
|
|
1634
|
+
this.close();
|
|
1635
|
+
}
|
|
1636
|
+
else {
|
|
1637
|
+
this.open();
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
/** Clears the selected date range. */
|
|
1641
|
+
clear(event) {
|
|
1642
|
+
event?.preventDefault();
|
|
1643
|
+
event?.stopPropagation();
|
|
1644
|
+
this.updateValue(null);
|
|
1645
|
+
this.announce('Date range cleared');
|
|
1646
|
+
}
|
|
1647
|
+
/** Selects today's date as the start date. */
|
|
1648
|
+
selectToday() {
|
|
1649
|
+
const today = this.dateAdapter.today();
|
|
1650
|
+
const currentValue = this.internalValue();
|
|
1651
|
+
// Set today as start if no start, or as end if we have a start
|
|
1652
|
+
if (!currentValue?.start) {
|
|
1653
|
+
this.updateValue(createDateRangeValue(today, null));
|
|
1654
|
+
this.announce('Start date set to today');
|
|
1655
|
+
}
|
|
1656
|
+
else if (!currentValue.end) {
|
|
1657
|
+
// Ensure proper ordering
|
|
1658
|
+
const newRange = this.dateAdapter.compareDate(today, currentValue.start) < 0
|
|
1659
|
+
? createDateRangeValue(today, currentValue.start)
|
|
1660
|
+
: createDateRangeValue(currentValue.start, today);
|
|
1661
|
+
this.updateValue(newRange);
|
|
1662
|
+
if (!this.effectiveKeepOpen()) {
|
|
1663
|
+
this.close();
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
// ============ EVENT HANDLERS ============
|
|
1668
|
+
onTriggerClick() {
|
|
1669
|
+
if (!this.disabled() && !this.isOpen()) {
|
|
1670
|
+
this.open();
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
onTriggerKeydown(event) {
|
|
1674
|
+
switch (event.key) {
|
|
1675
|
+
case 'Escape':
|
|
1676
|
+
if (this.isOpen()) {
|
|
1677
|
+
event.preventDefault();
|
|
1678
|
+
this.close();
|
|
1679
|
+
}
|
|
1680
|
+
break;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
onStartInputFocus() {
|
|
1684
|
+
this.activeInput.set('start');
|
|
1685
|
+
this._inputFocused.set(true);
|
|
1686
|
+
}
|
|
1687
|
+
onEndInputFocus() {
|
|
1688
|
+
this.activeInput.set('end');
|
|
1689
|
+
this._inputFocused.set(true);
|
|
1690
|
+
}
|
|
1691
|
+
onInputKeydown(event, inputType) {
|
|
1692
|
+
switch (event.key) {
|
|
1693
|
+
case 'Enter':
|
|
1694
|
+
event.preventDefault();
|
|
1695
|
+
if (this.isOpen()) {
|
|
1696
|
+
// Commit manual input
|
|
1697
|
+
if (inputType === 'start') {
|
|
1698
|
+
this.parseAndSetStart(this.startInputRef().nativeElement.value);
|
|
1699
|
+
}
|
|
1700
|
+
else {
|
|
1701
|
+
this.parseAndSetEnd(this.endInputRef().nativeElement.value);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
else {
|
|
1705
|
+
this.open();
|
|
1706
|
+
}
|
|
1707
|
+
break;
|
|
1708
|
+
case 'Escape':
|
|
1709
|
+
if (this.isOpen()) {
|
|
1710
|
+
event.preventDefault();
|
|
1711
|
+
this.close();
|
|
1712
|
+
}
|
|
1713
|
+
break;
|
|
1714
|
+
case 'ArrowDown':
|
|
1715
|
+
if (!this.isOpen()) {
|
|
1716
|
+
event.preventDefault();
|
|
1717
|
+
this.open();
|
|
1718
|
+
}
|
|
1719
|
+
break;
|
|
1720
|
+
case 'Tab':
|
|
1721
|
+
// Allow natural tab navigation between inputs
|
|
1722
|
+
break;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
onStartInputChange(_event) {
|
|
1726
|
+
// Debounced in blur handler
|
|
1727
|
+
}
|
|
1728
|
+
onEndInputChange(_event) {
|
|
1729
|
+
// Debounced in blur handler
|
|
1730
|
+
}
|
|
1731
|
+
onStartInputBlur() {
|
|
1732
|
+
this._inputFocused.set(false);
|
|
1733
|
+
if (this.allowManualInput()) {
|
|
1734
|
+
this.parseAndSetStart(this.startInputRef().nativeElement.value);
|
|
1735
|
+
}
|
|
1736
|
+
this.onTouched();
|
|
1737
|
+
this.touched.set(true);
|
|
1738
|
+
}
|
|
1739
|
+
onEndInputBlur() {
|
|
1740
|
+
this._inputFocused.set(false);
|
|
1741
|
+
if (this.allowManualInput()) {
|
|
1742
|
+
this.parseAndSetEnd(this.endInputRef().nativeElement.value);
|
|
1743
|
+
}
|
|
1744
|
+
this.onTouched();
|
|
1745
|
+
this.touched.set(true);
|
|
1746
|
+
}
|
|
1747
|
+
onPanelKeydown(event) {
|
|
1748
|
+
switch (event.key) {
|
|
1749
|
+
case 'Escape':
|
|
1750
|
+
event.preventDefault();
|
|
1751
|
+
this.close();
|
|
1752
|
+
break;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
onCalendarSelectionChange(selection) {
|
|
1756
|
+
// The calendar emits the selection from the strategy
|
|
1757
|
+
const range = selection;
|
|
1758
|
+
if (range) {
|
|
1759
|
+
const newValue = createDateRangeValue(range.start, range.end);
|
|
1760
|
+
this.updateValue(newValue);
|
|
1761
|
+
if (range.start && range.end) {
|
|
1762
|
+
// Complete range selected
|
|
1763
|
+
this.announce(`Range selected: ${this.formatDate(range.start)} to ${this.formatDate(range.end)}`);
|
|
1764
|
+
if (!this.effectiveKeepOpen()) {
|
|
1765
|
+
this.close();
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
else if (range.start) {
|
|
1769
|
+
this.announce(`Start date selected: ${this.formatDate(range.start)}. Now select end date.`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
onActiveDateChange(date) {
|
|
1774
|
+
this.calendarActiveDate.set(date);
|
|
1775
|
+
}
|
|
1776
|
+
onStartTimeChange(time) {
|
|
1777
|
+
if (!time)
|
|
1778
|
+
return;
|
|
1779
|
+
const value = this.internalValue();
|
|
1780
|
+
const startDate = value?.start ?? this.dateAdapter.today();
|
|
1781
|
+
const updated = this.dateAdapter.setTime(startDate, time.hours, time.minutes, time.seconds);
|
|
1782
|
+
this.updateValue(createDateRangeValue(updated, value?.end ?? null));
|
|
1783
|
+
}
|
|
1784
|
+
onEndTimeChange(time) {
|
|
1785
|
+
if (!time)
|
|
1786
|
+
return;
|
|
1787
|
+
const value = this.internalValue();
|
|
1788
|
+
const endDate = value?.end ?? this.dateAdapter.today();
|
|
1789
|
+
const updated = this.dateAdapter.setTime(endDate, time.hours, time.minutes, time.seconds);
|
|
1790
|
+
this.updateValue(createDateRangeValue(value?.start ?? null, updated));
|
|
1791
|
+
}
|
|
1792
|
+
// ============ FormFieldControl IMPLEMENTATION ============
|
|
1793
|
+
/**
|
|
1794
|
+
* Called when the form field container is clicked.
|
|
1795
|
+
* Implements FormFieldControl.
|
|
1796
|
+
*/
|
|
1797
|
+
onContainerClick(event) {
|
|
1798
|
+
const target = event.target;
|
|
1799
|
+
if (!this.disabled() && !this.triggerRef().nativeElement.contains(target)) {
|
|
1800
|
+
this.open();
|
|
1801
|
+
this.startInputRef().nativeElement.focus();
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Sets the describedBy IDs from the form field.
|
|
1806
|
+
* Called by the parent form field component.
|
|
1807
|
+
*/
|
|
1808
|
+
setDescribedByIds(ids) {
|
|
1809
|
+
this._describedByIds.set(ids);
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Sets the appearance for styling.
|
|
1813
|
+
* Called by the parent form field component.
|
|
1814
|
+
*/
|
|
1815
|
+
setAppearance(appearance) {
|
|
1816
|
+
this._appearance.set(appearance);
|
|
1817
|
+
}
|
|
1818
|
+
// ============ PRIVATE METHODS ============
|
|
1819
|
+
createOverlay() {
|
|
1820
|
+
if (this.overlayRef) {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
const hostEl = this.elementRef.nativeElement;
|
|
1824
|
+
const positionStrategy = this.overlay
|
|
1825
|
+
.position()
|
|
1826
|
+
.flexibleConnectedTo(hostEl)
|
|
1827
|
+
.withPositions(DEFAULT_POSITIONS)
|
|
1828
|
+
.withFlexibleDimensions(false)
|
|
1829
|
+
.withPush(true);
|
|
1830
|
+
this.overlayRef = this.overlay.create({
|
|
1831
|
+
positionStrategy,
|
|
1832
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
1833
|
+
hasBackdrop: true,
|
|
1834
|
+
backdropClass: 'cdk-overlay-transparent-backdrop',
|
|
1835
|
+
});
|
|
1836
|
+
// Attach panel template
|
|
1837
|
+
const portal = new TemplatePortal(this.panelTemplateRef(), this.viewContainerRef);
|
|
1838
|
+
this.overlayRef.attach(portal);
|
|
1839
|
+
// Close on backdrop click
|
|
1840
|
+
this.overlayRef
|
|
1841
|
+
.backdropClick()
|
|
1842
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1843
|
+
.subscribe(() => this.close());
|
|
1844
|
+
// Close on outside click
|
|
1845
|
+
this.overlayRef
|
|
1846
|
+
.outsidePointerEvents()
|
|
1847
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1848
|
+
.subscribe(() => this.close());
|
|
1849
|
+
}
|
|
1850
|
+
destroyOverlay() {
|
|
1851
|
+
if (this.overlayRef) {
|
|
1852
|
+
this.overlayRef.dispose();
|
|
1853
|
+
this.overlayRef = null;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
updateValue(value) {
|
|
1857
|
+
this.value.set(value);
|
|
1858
|
+
this.onChange(value);
|
|
1859
|
+
this.rangeChange.emit(value);
|
|
1860
|
+
this.onValidatorChange();
|
|
1861
|
+
}
|
|
1862
|
+
parseAndSetStart(inputValue) {
|
|
1863
|
+
const currentValue = this.internalValue();
|
|
1864
|
+
if (!inputValue.trim()) {
|
|
1865
|
+
// Clear start date
|
|
1866
|
+
if (currentValue?.start) {
|
|
1867
|
+
this.updateValue(createDateRangeValue(null, currentValue.end));
|
|
1868
|
+
}
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
const parsed = this.dateAdapter.parse(inputValue, this.effectiveDateFormat());
|
|
1872
|
+
if (parsed && this.dateAdapter.isValid(parsed) && this.isDateValid(parsed)) {
|
|
1873
|
+
// Ensure start <= end
|
|
1874
|
+
if (currentValue?.end && this.dateAdapter.compareDate(parsed, currentValue.end) > 0) {
|
|
1875
|
+
// Swap dates
|
|
1876
|
+
this.updateValue(createDateRangeValue(currentValue.end, parsed));
|
|
1877
|
+
}
|
|
1878
|
+
else {
|
|
1879
|
+
this.updateValue(createDateRangeValue(parsed, currentValue?.end ?? null));
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
else {
|
|
1883
|
+
// Revert to current value
|
|
1884
|
+
this.startInputRef().nativeElement.value = this.startDisplayValue();
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
parseAndSetEnd(inputValue) {
|
|
1888
|
+
const currentValue = this.internalValue();
|
|
1889
|
+
if (!inputValue.trim()) {
|
|
1890
|
+
// Clear end date
|
|
1891
|
+
if (currentValue?.end) {
|
|
1892
|
+
this.updateValue(createDateRangeValue(currentValue.start, null));
|
|
1893
|
+
}
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
const parsed = this.dateAdapter.parse(inputValue, this.effectiveDateFormat());
|
|
1897
|
+
if (parsed && this.dateAdapter.isValid(parsed) && this.isDateValid(parsed)) {
|
|
1898
|
+
// Ensure start <= end
|
|
1899
|
+
if (currentValue?.start && this.dateAdapter.compareDate(parsed, currentValue.start) < 0) {
|
|
1900
|
+
// Swap dates
|
|
1901
|
+
this.updateValue(createDateRangeValue(parsed, currentValue.start));
|
|
1902
|
+
}
|
|
1903
|
+
else {
|
|
1904
|
+
this.updateValue(createDateRangeValue(currentValue?.start ?? null, parsed));
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
else {
|
|
1908
|
+
// Revert to current value
|
|
1909
|
+
this.endInputRef().nativeElement.value = this.endDisplayValue();
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
isDateValid(date) {
|
|
1913
|
+
const min = this.min();
|
|
1914
|
+
const max = this.max();
|
|
1915
|
+
const filter = this.dateFilter();
|
|
1916
|
+
if (min && this.dateAdapter.compareDate(date, min) < 0) {
|
|
1917
|
+
return false;
|
|
1918
|
+
}
|
|
1919
|
+
if (max && this.dateAdapter.compareDate(date, max) > 0) {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
if (filter && !filter(date)) {
|
|
1923
|
+
return false;
|
|
1924
|
+
}
|
|
1925
|
+
return true;
|
|
1926
|
+
}
|
|
1927
|
+
formatDate(date) {
|
|
1928
|
+
return this.dateAdapter.format(date, 'long');
|
|
1929
|
+
}
|
|
1930
|
+
announce(message) {
|
|
1931
|
+
this.liveAnnouncement.set(message);
|
|
1932
|
+
}
|
|
1933
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDateRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1934
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDateRangePicker, isStandalone: true, selector: "com-date-range-picker", inputs: { value: { classPropertyName: "value", publicName: "value", 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 }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, startAt: { classPropertyName: "startAt", publicName: "startAt", isSignal: true, isRequired: false, transformFunction: null }, startView: { classPropertyName: "startView", publicName: "startView", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, dateFormat: { classPropertyName: "dateFormat", publicName: "dateFormat", isSignal: true, isRequired: false, transformFunction: null }, showClearButton: { classPropertyName: "showClearButton", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, showTodayButton: { classPropertyName: "showTodayButton", publicName: "showTodayButton", isSignal: true, isRequired: false, transformFunction: null }, showFooterClearButton: { classPropertyName: "showFooterClearButton", publicName: "showFooterClearButton", isSignal: true, isRequired: false, transformFunction: null }, keepOpen: { classPropertyName: "keepOpen", publicName: "keepOpen", isSignal: true, isRequired: false, transformFunction: null }, allowManualInput: { classPropertyName: "allowManualInput", publicName: "allowManualInput", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, startAriaLabel: { classPropertyName: "startAriaLabel", publicName: "startAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, endAriaLabel: { classPropertyName: "endAriaLabel", publicName: "endAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, sfErrors: { classPropertyName: "sfErrors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, showTimePicker: { classPropertyName: "showTimePicker", publicName: "showTimePicker", isSignal: true, isRequired: false, transformFunction: null }, use12HourFormat: { classPropertyName: "use12HourFormat", publicName: "use12HourFormat", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", rangeChange: "rangeChange", opened: "opened", closed: "closed" }, host: { properties: { "class.com-date-range-picker-disabled": "disabled()", "class.com-date-range-picker-open": "isOpen()" }, classAttribute: "com-date-range-picker-host inline-block" }, providers: [
|
|
1935
|
+
RangeSelectionStrategy,
|
|
1936
|
+
{ provide: CALENDAR_SELECTION_STRATEGY, useExisting: RangeSelectionStrategy },
|
|
1937
|
+
{ provide: FormFieldControl, useExisting: forwardRef(() => ComDateRangePicker) },
|
|
1938
|
+
], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["triggerElement"], descendants: true, isSignal: true }, { propertyName: "startInputRef", first: true, predicate: ["startInputElement"], descendants: true, isSignal: true }, { propertyName: "endInputRef", first: true, predicate: ["endInputElement"], descendants: true, isSignal: true }, { propertyName: "panelTemplateRef", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }], exportAs: ["comDateRangePicker"], ngImport: i0, template: `
|
|
1939
|
+
<!-- Trigger container -->
|
|
1940
|
+
<div
|
|
1941
|
+
#triggerElement
|
|
1942
|
+
[class]="triggerClasses()"
|
|
1943
|
+
role="group"
|
|
1944
|
+
tabindex="-1"
|
|
1945
|
+
[attr.aria-expanded]="isOpen()"
|
|
1946
|
+
[attr.aria-haspopup]="'dialog'"
|
|
1947
|
+
[attr.aria-owns]="panelId()"
|
|
1948
|
+
[attr.aria-disabled]="disabled() || null"
|
|
1949
|
+
(click)="onTriggerClick()"
|
|
1950
|
+
(keydown)="onTriggerKeydown($event)"
|
|
1951
|
+
>
|
|
1952
|
+
<!-- Start date input -->
|
|
1953
|
+
<input
|
|
1954
|
+
#startInputElement
|
|
1955
|
+
type="text"
|
|
1956
|
+
[class]="inputClasses()"
|
|
1957
|
+
[value]="startDisplayValue()"
|
|
1958
|
+
[placeholder]="startPlaceholder()"
|
|
1959
|
+
[disabled]="disabled()"
|
|
1960
|
+
[readonly]="!allowManualInput()"
|
|
1961
|
+
[attr.id]="startInputId()"
|
|
1962
|
+
[attr.aria-label]="startAriaLabel() || startPlaceholder()"
|
|
1963
|
+
[attr.aria-describedby]="effectiveAriaDescribedBy() || null"
|
|
1964
|
+
[attr.aria-invalid]="effectiveState() === 'error' || null"
|
|
1965
|
+
[attr.aria-required]="required() || null"
|
|
1966
|
+
(focus)="onStartInputFocus()"
|
|
1967
|
+
(input)="onStartInputChange($event)"
|
|
1968
|
+
(blur)="onStartInputBlur()"
|
|
1969
|
+
(keydown)="onInputKeydown($event, 'start')"
|
|
1970
|
+
/>
|
|
1971
|
+
|
|
1972
|
+
<!-- Range separator -->
|
|
1973
|
+
<span [class]="separatorClasses()">
|
|
1974
|
+
<com-icon name="arrow-right" [size]="iconSize()" />
|
|
1975
|
+
</span>
|
|
1976
|
+
|
|
1977
|
+
<!-- End date input -->
|
|
1978
|
+
<input
|
|
1979
|
+
#endInputElement
|
|
1980
|
+
type="text"
|
|
1981
|
+
[class]="inputClasses()"
|
|
1982
|
+
[value]="endDisplayValue()"
|
|
1983
|
+
[placeholder]="endPlaceholder()"
|
|
1984
|
+
[disabled]="disabled()"
|
|
1985
|
+
[readonly]="!allowManualInput()"
|
|
1986
|
+
[attr.id]="endInputId()"
|
|
1987
|
+
[attr.aria-label]="endAriaLabel() || endPlaceholder()"
|
|
1988
|
+
[attr.aria-invalid]="effectiveState() === 'error' || null"
|
|
1989
|
+
(focus)="onEndInputFocus()"
|
|
1990
|
+
(input)="onEndInputChange($event)"
|
|
1991
|
+
(blur)="onEndInputBlur()"
|
|
1992
|
+
(keydown)="onInputKeydown($event, 'end')"
|
|
1993
|
+
/>
|
|
1994
|
+
|
|
1995
|
+
<!-- Clear button -->
|
|
1996
|
+
@if (showClearButton() && hasValue() && !disabled()) {
|
|
1997
|
+
<button
|
|
1998
|
+
type="button"
|
|
1999
|
+
[class]="clearClasses()"
|
|
2000
|
+
[attr.aria-label]="'Clear date range'"
|
|
2001
|
+
(click)="clear($event)"
|
|
2002
|
+
>
|
|
2003
|
+
<com-icon name="x" [size]="iconSize()" />
|
|
2004
|
+
</button>
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
<!-- Calendar icon -->
|
|
2008
|
+
<button
|
|
2009
|
+
type="button"
|
|
2010
|
+
[class]="iconClasses()"
|
|
2011
|
+
[attr.aria-label]="isOpen() ? 'Close calendar' : 'Open calendar'"
|
|
2012
|
+
[disabled]="disabled()"
|
|
2013
|
+
tabindex="-1"
|
|
2014
|
+
>
|
|
2015
|
+
<com-icon name="calendar" [size]="iconSize()" />
|
|
2016
|
+
</button>
|
|
2017
|
+
</div>
|
|
2018
|
+
|
|
2019
|
+
<!-- Panel template (rendered in overlay) -->
|
|
2020
|
+
<ng-template #panelTemplate>
|
|
2021
|
+
<div
|
|
2022
|
+
[class]="panelClasses()"
|
|
2023
|
+
[attr.id]="panelId()"
|
|
2024
|
+
role="dialog"
|
|
2025
|
+
aria-modal="true"
|
|
2026
|
+
[attr.aria-label]="'Choose date range'"
|
|
2027
|
+
(keydown)="onPanelKeydown($event)"
|
|
2028
|
+
cdkTrapFocus
|
|
2029
|
+
[cdkTrapFocusAutoCapture]="true"
|
|
2030
|
+
>
|
|
2031
|
+
<com-calendar
|
|
2032
|
+
[activeDate]="calendarActiveDate()"
|
|
2033
|
+
[selected]="calendarSelection()"
|
|
2034
|
+
[minDate]="min()"
|
|
2035
|
+
[maxDate]="max()"
|
|
2036
|
+
[dateFilter]="dateFilter()"
|
|
2037
|
+
[startView]="startView()"
|
|
2038
|
+
[firstDayOfWeek]="firstDayOfWeek()"
|
|
2039
|
+
[monthColumns]="2"
|
|
2040
|
+
[bordered]="false"
|
|
2041
|
+
(selectedChange)="onCalendarSelectionChange($event)"
|
|
2042
|
+
(activeDateChange)="onActiveDateChange($event)"
|
|
2043
|
+
/>
|
|
2044
|
+
|
|
2045
|
+
@if (showTimePicker()) {
|
|
2046
|
+
<div [class]="timeSectionClasses()">
|
|
2047
|
+
<div class="flex items-center gap-3">
|
|
2048
|
+
<div class="flex flex-col gap-1">
|
|
2049
|
+
<span [class]="timeLabelClasses()">Start time</span>
|
|
2050
|
+
<com-time-picker
|
|
2051
|
+
variant="embedded"
|
|
2052
|
+
[size]="size()"
|
|
2053
|
+
[value]="startTimeValue()"
|
|
2054
|
+
[use12HourFormat]="use12HourFormat()"
|
|
2055
|
+
[showSeconds]="showSeconds()"
|
|
2056
|
+
[minuteStep]="minuteStep()"
|
|
2057
|
+
[disabled]="disabled()"
|
|
2058
|
+
ariaLabel="Start time"
|
|
2059
|
+
(timeChange)="onStartTimeChange($event)"
|
|
2060
|
+
/>
|
|
2061
|
+
</div>
|
|
2062
|
+
<span class="text-muted-foreground mt-5">
|
|
2063
|
+
<com-icon name="arrow-right" [size]="iconSize()" />
|
|
2064
|
+
</span>
|
|
2065
|
+
<div class="flex flex-col gap-1">
|
|
2066
|
+
<span [class]="timeLabelClasses()">End time</span>
|
|
2067
|
+
<com-time-picker
|
|
2068
|
+
variant="embedded"
|
|
2069
|
+
[size]="size()"
|
|
2070
|
+
[value]="endTimeValue()"
|
|
2071
|
+
[use12HourFormat]="use12HourFormat()"
|
|
2072
|
+
[showSeconds]="showSeconds()"
|
|
2073
|
+
[minuteStep]="minuteStep()"
|
|
2074
|
+
[disabled]="disabled()"
|
|
2075
|
+
ariaLabel="End time"
|
|
2076
|
+
(timeChange)="onEndTimeChange($event)"
|
|
2077
|
+
/>
|
|
2078
|
+
</div>
|
|
2079
|
+
</div>
|
|
2080
|
+
</div>
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
@if (showTodayButton() || showFooterClearButton() || showTimePicker()) {
|
|
2084
|
+
<div [class]="footerClasses()">
|
|
2085
|
+
@if (showTodayButton()) {
|
|
2086
|
+
<button
|
|
2087
|
+
type="button"
|
|
2088
|
+
[class]="todayButtonClasses()"
|
|
2089
|
+
(click)="selectToday()"
|
|
2090
|
+
>
|
|
2091
|
+
Today
|
|
2092
|
+
</button>
|
|
2093
|
+
}
|
|
2094
|
+
@if (showFooterClearButton()) {
|
|
2095
|
+
<button
|
|
2096
|
+
type="button"
|
|
2097
|
+
[class]="clearButtonClasses()"
|
|
2098
|
+
(click)="clear($event)"
|
|
2099
|
+
>
|
|
2100
|
+
Clear
|
|
2101
|
+
</button>
|
|
2102
|
+
}
|
|
2103
|
+
@if (showTimePicker()) {
|
|
2104
|
+
<button
|
|
2105
|
+
type="button"
|
|
2106
|
+
[class]="todayButtonClasses()"
|
|
2107
|
+
(click)="close()"
|
|
2108
|
+
>
|
|
2109
|
+
Done
|
|
2110
|
+
</button>
|
|
2111
|
+
}
|
|
2112
|
+
</div>
|
|
2113
|
+
}
|
|
2114
|
+
</div>
|
|
2115
|
+
</ng-template>
|
|
2116
|
+
|
|
2117
|
+
<!-- Live announcer region -->
|
|
2118
|
+
<div class="sr-only" aria-live="polite" aria-atomic="true">
|
|
2119
|
+
{{ liveAnnouncement() }}
|
|
2120
|
+
</div>
|
|
2121
|
+
`, isInline: true, styles: [".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: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: ComCalendar, selector: "com-calendar", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter", "dateClass", "bordered", "startView", "firstDayOfWeek", "monthColumns", "cellTemplate"], outputs: ["selectedChange", "viewChanged", "activeDateChange"] }, { kind: "component", type: ComIcon, selector: "com-icon", inputs: ["name", "img", "color", "size", "strokeWidth", "absoluteStrokeWidth", "ariaLabel"] }, { kind: "component", type: ComTimePicker, selector: "com-time-picker", inputs: ["value", "disabled", "required", "showSeconds", "use12HourFormat", "minuteStep", "secondStep", "minTime", "maxTime", "variant", "size", "state", "ariaLabel", "class", "placeholder", "errorStateMatcher", "touched", "invalid", "errors"], outputs: ["valueChange", "disabledChange", "touchedChange", "timeChange"], exportAs: ["comTimePicker"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2122
|
+
}
|
|
2123
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDateRangePicker, decorators: [{
|
|
2124
|
+
type: Component,
|
|
2125
|
+
args: [{ selector: 'com-date-range-picker', exportAs: 'comDateRangePicker', template: `
|
|
2126
|
+
<!-- Trigger container -->
|
|
2127
|
+
<div
|
|
2128
|
+
#triggerElement
|
|
2129
|
+
[class]="triggerClasses()"
|
|
2130
|
+
role="group"
|
|
2131
|
+
tabindex="-1"
|
|
2132
|
+
[attr.aria-expanded]="isOpen()"
|
|
2133
|
+
[attr.aria-haspopup]="'dialog'"
|
|
2134
|
+
[attr.aria-owns]="panelId()"
|
|
2135
|
+
[attr.aria-disabled]="disabled() || null"
|
|
2136
|
+
(click)="onTriggerClick()"
|
|
2137
|
+
(keydown)="onTriggerKeydown($event)"
|
|
2138
|
+
>
|
|
2139
|
+
<!-- Start date input -->
|
|
2140
|
+
<input
|
|
2141
|
+
#startInputElement
|
|
2142
|
+
type="text"
|
|
2143
|
+
[class]="inputClasses()"
|
|
2144
|
+
[value]="startDisplayValue()"
|
|
2145
|
+
[placeholder]="startPlaceholder()"
|
|
2146
|
+
[disabled]="disabled()"
|
|
2147
|
+
[readonly]="!allowManualInput()"
|
|
2148
|
+
[attr.id]="startInputId()"
|
|
2149
|
+
[attr.aria-label]="startAriaLabel() || startPlaceholder()"
|
|
2150
|
+
[attr.aria-describedby]="effectiveAriaDescribedBy() || null"
|
|
2151
|
+
[attr.aria-invalid]="effectiveState() === 'error' || null"
|
|
2152
|
+
[attr.aria-required]="required() || null"
|
|
2153
|
+
(focus)="onStartInputFocus()"
|
|
2154
|
+
(input)="onStartInputChange($event)"
|
|
2155
|
+
(blur)="onStartInputBlur()"
|
|
2156
|
+
(keydown)="onInputKeydown($event, 'start')"
|
|
2157
|
+
/>
|
|
2158
|
+
|
|
2159
|
+
<!-- Range separator -->
|
|
2160
|
+
<span [class]="separatorClasses()">
|
|
2161
|
+
<com-icon name="arrow-right" [size]="iconSize()" />
|
|
2162
|
+
</span>
|
|
2163
|
+
|
|
2164
|
+
<!-- End date input -->
|
|
2165
|
+
<input
|
|
2166
|
+
#endInputElement
|
|
2167
|
+
type="text"
|
|
2168
|
+
[class]="inputClasses()"
|
|
2169
|
+
[value]="endDisplayValue()"
|
|
2170
|
+
[placeholder]="endPlaceholder()"
|
|
2171
|
+
[disabled]="disabled()"
|
|
2172
|
+
[readonly]="!allowManualInput()"
|
|
2173
|
+
[attr.id]="endInputId()"
|
|
2174
|
+
[attr.aria-label]="endAriaLabel() || endPlaceholder()"
|
|
2175
|
+
[attr.aria-invalid]="effectiveState() === 'error' || null"
|
|
2176
|
+
(focus)="onEndInputFocus()"
|
|
2177
|
+
(input)="onEndInputChange($event)"
|
|
2178
|
+
(blur)="onEndInputBlur()"
|
|
2179
|
+
(keydown)="onInputKeydown($event, 'end')"
|
|
2180
|
+
/>
|
|
2181
|
+
|
|
2182
|
+
<!-- Clear button -->
|
|
2183
|
+
@if (showClearButton() && hasValue() && !disabled()) {
|
|
2184
|
+
<button
|
|
2185
|
+
type="button"
|
|
2186
|
+
[class]="clearClasses()"
|
|
2187
|
+
[attr.aria-label]="'Clear date range'"
|
|
2188
|
+
(click)="clear($event)"
|
|
2189
|
+
>
|
|
2190
|
+
<com-icon name="x" [size]="iconSize()" />
|
|
2191
|
+
</button>
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
<!-- Calendar icon -->
|
|
2195
|
+
<button
|
|
2196
|
+
type="button"
|
|
2197
|
+
[class]="iconClasses()"
|
|
2198
|
+
[attr.aria-label]="isOpen() ? 'Close calendar' : 'Open calendar'"
|
|
2199
|
+
[disabled]="disabled()"
|
|
2200
|
+
tabindex="-1"
|
|
2201
|
+
>
|
|
2202
|
+
<com-icon name="calendar" [size]="iconSize()" />
|
|
2203
|
+
</button>
|
|
2204
|
+
</div>
|
|
2205
|
+
|
|
2206
|
+
<!-- Panel template (rendered in overlay) -->
|
|
2207
|
+
<ng-template #panelTemplate>
|
|
2208
|
+
<div
|
|
2209
|
+
[class]="panelClasses()"
|
|
2210
|
+
[attr.id]="panelId()"
|
|
2211
|
+
role="dialog"
|
|
2212
|
+
aria-modal="true"
|
|
2213
|
+
[attr.aria-label]="'Choose date range'"
|
|
2214
|
+
(keydown)="onPanelKeydown($event)"
|
|
2215
|
+
cdkTrapFocus
|
|
2216
|
+
[cdkTrapFocusAutoCapture]="true"
|
|
2217
|
+
>
|
|
2218
|
+
<com-calendar
|
|
2219
|
+
[activeDate]="calendarActiveDate()"
|
|
2220
|
+
[selected]="calendarSelection()"
|
|
2221
|
+
[minDate]="min()"
|
|
2222
|
+
[maxDate]="max()"
|
|
2223
|
+
[dateFilter]="dateFilter()"
|
|
2224
|
+
[startView]="startView()"
|
|
2225
|
+
[firstDayOfWeek]="firstDayOfWeek()"
|
|
2226
|
+
[monthColumns]="2"
|
|
2227
|
+
[bordered]="false"
|
|
2228
|
+
(selectedChange)="onCalendarSelectionChange($event)"
|
|
2229
|
+
(activeDateChange)="onActiveDateChange($event)"
|
|
2230
|
+
/>
|
|
2231
|
+
|
|
2232
|
+
@if (showTimePicker()) {
|
|
2233
|
+
<div [class]="timeSectionClasses()">
|
|
2234
|
+
<div class="flex items-center gap-3">
|
|
2235
|
+
<div class="flex flex-col gap-1">
|
|
2236
|
+
<span [class]="timeLabelClasses()">Start time</span>
|
|
2237
|
+
<com-time-picker
|
|
2238
|
+
variant="embedded"
|
|
2239
|
+
[size]="size()"
|
|
2240
|
+
[value]="startTimeValue()"
|
|
2241
|
+
[use12HourFormat]="use12HourFormat()"
|
|
2242
|
+
[showSeconds]="showSeconds()"
|
|
2243
|
+
[minuteStep]="minuteStep()"
|
|
2244
|
+
[disabled]="disabled()"
|
|
2245
|
+
ariaLabel="Start time"
|
|
2246
|
+
(timeChange)="onStartTimeChange($event)"
|
|
2247
|
+
/>
|
|
2248
|
+
</div>
|
|
2249
|
+
<span class="text-muted-foreground mt-5">
|
|
2250
|
+
<com-icon name="arrow-right" [size]="iconSize()" />
|
|
2251
|
+
</span>
|
|
2252
|
+
<div class="flex flex-col gap-1">
|
|
2253
|
+
<span [class]="timeLabelClasses()">End time</span>
|
|
2254
|
+
<com-time-picker
|
|
2255
|
+
variant="embedded"
|
|
2256
|
+
[size]="size()"
|
|
2257
|
+
[value]="endTimeValue()"
|
|
2258
|
+
[use12HourFormat]="use12HourFormat()"
|
|
2259
|
+
[showSeconds]="showSeconds()"
|
|
2260
|
+
[minuteStep]="minuteStep()"
|
|
2261
|
+
[disabled]="disabled()"
|
|
2262
|
+
ariaLabel="End time"
|
|
2263
|
+
(timeChange)="onEndTimeChange($event)"
|
|
2264
|
+
/>
|
|
2265
|
+
</div>
|
|
2266
|
+
</div>
|
|
2267
|
+
</div>
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
@if (showTodayButton() || showFooterClearButton() || showTimePicker()) {
|
|
2271
|
+
<div [class]="footerClasses()">
|
|
2272
|
+
@if (showTodayButton()) {
|
|
2273
|
+
<button
|
|
2274
|
+
type="button"
|
|
2275
|
+
[class]="todayButtonClasses()"
|
|
2276
|
+
(click)="selectToday()"
|
|
2277
|
+
>
|
|
2278
|
+
Today
|
|
2279
|
+
</button>
|
|
2280
|
+
}
|
|
2281
|
+
@if (showFooterClearButton()) {
|
|
2282
|
+
<button
|
|
2283
|
+
type="button"
|
|
2284
|
+
[class]="clearButtonClasses()"
|
|
2285
|
+
(click)="clear($event)"
|
|
2286
|
+
>
|
|
2287
|
+
Clear
|
|
2288
|
+
</button>
|
|
2289
|
+
}
|
|
2290
|
+
@if (showTimePicker()) {
|
|
2291
|
+
<button
|
|
2292
|
+
type="button"
|
|
2293
|
+
[class]="todayButtonClasses()"
|
|
2294
|
+
(click)="close()"
|
|
2295
|
+
>
|
|
2296
|
+
Done
|
|
2297
|
+
</button>
|
|
2298
|
+
}
|
|
2299
|
+
</div>
|
|
2300
|
+
}
|
|
2301
|
+
</div>
|
|
2302
|
+
</ng-template>
|
|
2303
|
+
|
|
2304
|
+
<!-- Live announcer region -->
|
|
2305
|
+
<div class="sr-only" aria-live="polite" aria-atomic="true">
|
|
2306
|
+
{{ liveAnnouncement() }}
|
|
2307
|
+
</div>
|
|
2308
|
+
`, imports: [
|
|
2309
|
+
OverlayModule,
|
|
2310
|
+
A11yModule,
|
|
2311
|
+
ComCalendar,
|
|
2312
|
+
ComIcon,
|
|
2313
|
+
ComTimePicker,
|
|
2314
|
+
], providers: [
|
|
2315
|
+
RangeSelectionStrategy,
|
|
2316
|
+
{ provide: CALENDAR_SELECTION_STRATEGY, useExisting: RangeSelectionStrategy },
|
|
2317
|
+
{ provide: FormFieldControl, useExisting: forwardRef(() => ComDateRangePicker) },
|
|
2318
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
2319
|
+
class: 'com-date-range-picker-host inline-block',
|
|
2320
|
+
'[class.com-date-range-picker-disabled]': 'disabled()',
|
|
2321
|
+
'[class.com-date-range-picker-open]': 'isOpen()',
|
|
2322
|
+
}, styles: [".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"] }]
|
|
2323
|
+
}], ctorParameters: () => [], propDecorators: { triggerRef: [{ type: i0.ViewChild, args: ['triggerElement', { isSignal: true }] }], startInputRef: [{ type: i0.ViewChild, args: ['startInputElement', { isSignal: true }] }], endInputRef: [{ type: i0.ViewChild, args: ['endInputElement', { isSignal: true }] }], panelTemplateRef: [{ type: i0.ViewChild, args: ['panelTemplate', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], startAt: [{ type: i0.Input, args: [{ isSignal: true, alias: "startAt", required: false }] }], startView: [{ type: i0.Input, args: [{ isSignal: true, alias: "startView", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], startPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPlaceholder", required: false }] }], endPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "endPlaceholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], dateFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFormat", required: false }] }], showClearButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], showTodayButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTodayButton", required: false }] }], showFooterClearButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooterClearButton", required: false }] }], keepOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepOpen", required: false }] }], allowManualInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowManualInput", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], startAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startAriaLabel", required: false }] }], endAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "endAriaLabel", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], sfErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], showTimePicker: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTimePicker", required: false }] }], use12HourFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "use12HourFormat", required: false }] }], showSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeconds", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
2324
|
+
|
|
2325
|
+
/**
|
|
2326
|
+
* Datepicker components public API.
|
|
2327
|
+
*/
|
|
2328
|
+
|
|
2329
|
+
/**
|
|
2330
|
+
* Generated bundle index. Do not edit.
|
|
2331
|
+
*/
|
|
2332
|
+
|
|
2333
|
+
export { ComDateRangePicker, ComDatepicker, createDateRangeValue, datepickerClearVariants, datepickerDisabledVariants, datepickerFooterButtonVariants, datepickerFooterVariants, datepickerIconVariants, datepickerInputVariants, datepickerPanelVariants, datepickerRangeSeparatorVariants, datepickerTriggerVariants, generateDatepickerId };
|
|
2334
|
+
//# sourceMappingURL=ngx-com-components-datepicker.mjs.map
|