forty-cdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accordion/README.md +122 -0
- package/aspect-ratio/README.md +76 -0
- package/avatar/README.md +100 -0
- package/breadcrumbs/README.md +49 -0
- package/breakpoints/README.md +81 -0
- package/button/README.md +49 -0
- package/calendar/README.md +458 -0
- package/carousel/README.md +358 -0
- package/checkbox/README.md +146 -0
- package/combobox/README.md +535 -0
- package/context-menu/README.md +139 -0
- package/date-field/README.md +184 -0
- package/date-picker/README.md +338 -0
- package/dialog/README.md +388 -0
- package/disclosure/README.md +114 -0
- package/drag-drop/README.md +359 -0
- package/drawer/README.md +560 -0
- package/dropdown-menu/README.md +176 -0
- package/fesm2022/forty-cdk-accordion.mjs +348 -0
- package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
- package/fesm2022/forty-cdk-avatar.mjs +308 -0
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
- package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
- package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
- package/fesm2022/forty-cdk-button.mjs +134 -0
- package/fesm2022/forty-cdk-button.mjs.map +1 -0
- package/fesm2022/forty-cdk-calendar.mjs +2034 -0
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
- package/fesm2022/forty-cdk-carousel.mjs +968 -0
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
- package/fesm2022/forty-cdk-checkbox.mjs +226 -0
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-combobox.mjs +2596 -0
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
- package/fesm2022/forty-cdk-context-menu.mjs +413 -0
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-core.mjs +9022 -0
- package/fesm2022/forty-cdk-core.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-field.mjs +744 -0
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-dialog.mjs +707 -0
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
- package/fesm2022/forty-cdk-disclosure.mjs +190 -0
- package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
- package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
- package/fesm2022/forty-cdk-drawer.mjs +1641 -0
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-field.mjs +425 -0
- package/fesm2022/forty-cdk-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-fieldset.mjs +164 -0
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
- package/fesm2022/forty-cdk-file-upload.mjs +221 -0
- package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
- package/fesm2022/forty-cdk-hover-card.mjs +496 -0
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
- package/fesm2022/forty-cdk-input.mjs +274 -0
- package/fesm2022/forty-cdk-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
- package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +1279 -0
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-menu.mjs +1439 -0
- package/fesm2022/forty-cdk-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-menubar.mjs +787 -0
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
- package/fesm2022/forty-cdk-meter.mjs +211 -0
- package/fesm2022/forty-cdk-meter.mjs.map +1 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-number-input.mjs +559 -0
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-otp-input.mjs +527 -0
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-pagination.mjs +323 -0
- package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
- package/fesm2022/forty-cdk-popover.mjs +698 -0
- package/fesm2022/forty-cdk-popover.mjs.map +1 -0
- package/fesm2022/forty-cdk-progress.mjs +226 -0
- package/fesm2022/forty-cdk-progress.mjs.map +1 -0
- package/fesm2022/forty-cdk-radio-group.mjs +378 -0
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
- package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
- package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
- package/fesm2022/forty-cdk-search.mjs +205 -0
- package/fesm2022/forty-cdk-search.mjs.map +1 -0
- package/fesm2022/forty-cdk-select.mjs +1661 -0
- package/fesm2022/forty-cdk-select.mjs.map +1 -0
- package/fesm2022/forty-cdk-separator.mjs +82 -0
- package/fesm2022/forty-cdk-separator.mjs.map +1 -0
- package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
- package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
- package/fesm2022/forty-cdk-slider.mjs +803 -0
- package/fesm2022/forty-cdk-slider.mjs.map +1 -0
- package/fesm2022/forty-cdk-stepper.mjs +886 -0
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
- package/fesm2022/forty-cdk-switch.mjs +137 -0
- package/fesm2022/forty-cdk-switch.mjs.map +1 -0
- package/fesm2022/forty-cdk-table.mjs +1518 -0
- package/fesm2022/forty-cdk-table.mjs.map +1 -0
- package/fesm2022/forty-cdk-tabs.mjs +400 -0
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-field.mjs +593 -0
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-toast.mjs +1153 -0
- package/fesm2022/forty-cdk-toast.mjs.map +1 -0
- package/fesm2022/forty-cdk-toggle.mjs +516 -0
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
- package/fesm2022/forty-cdk-toolbar.mjs +374 -0
- package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
- package/fesm2022/forty-cdk-tooltip.mjs +672 -0
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
- package/fesm2022/forty-cdk-tree.mjs +2007 -0
- package/fesm2022/forty-cdk-tree.mjs.map +1 -0
- package/fesm2022/forty-cdk-virtualization.mjs +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/fesm2022/forty-cdk.mjs +0 -43310
- package/fesm2022/forty-cdk.mjs.map +1 -1
- package/field/README.md +97 -0
- package/fieldset/README.md +86 -0
- package/file-upload/README.md +73 -0
- package/hover-card/README.md +171 -0
- package/input/README.md +156 -0
- package/listbox/README.md +424 -0
- package/menu/README.md +181 -0
- package/menubar/README.md +140 -0
- package/meter/README.md +128 -0
- package/navigation-menu/README.md +253 -0
- package/number-input/README.md +171 -0
- package/otp-input/README.md +198 -0
- package/package.json +213 -1
- package/pagination/README.md +61 -0
- package/pane-resizer/README.md +136 -0
- package/popover/README.md +262 -0
- package/progress/README.md +115 -0
- package/radio-group/README.md +129 -0
- package/scroll-area/README.md +184 -0
- package/search/README.md +42 -0
- package/select/README.md +488 -0
- package/separator/README.md +84 -0
- package/signal-forms/README.md +72 -0
- package/slider/README.md +152 -0
- package/stepper/README.md +292 -0
- package/switch/README.md +116 -0
- package/table/README.md +769 -0
- package/tabs/README.md +130 -0
- package/time-field/README.md +157 -0
- package/time-picker/README.md +172 -0
- package/toast/README.md +398 -0
- package/toggle/README.md +224 -0
- package/toolbar/README.md +109 -0
- package/tooltip/README.md +274 -0
- package/tree/README.md +708 -0
- package/types/forty-cdk-accordion.d.ts +242 -0
- package/types/forty-cdk-aspect-ratio.d.ts +59 -0
- package/types/forty-cdk-avatar.d.ts +133 -0
- package/types/forty-cdk-breadcrumbs.d.ts +92 -0
- package/types/forty-cdk-breakpoints.d.ts +141 -0
- package/types/forty-cdk-button.d.ts +80 -0
- package/types/forty-cdk-calendar.d.ts +914 -0
- package/types/forty-cdk-carousel.d.ts +530 -0
- package/types/forty-cdk-checkbox.d.ts +141 -0
- package/types/forty-cdk-combobox.d.ts +1259 -0
- package/types/forty-cdk-context-menu.d.ts +313 -0
- package/types/forty-cdk-core.d.ts +5774 -0
- package/types/forty-cdk-date-field.d.ts +307 -0
- package/types/forty-cdk-date-picker.d.ts +622 -0
- package/types/forty-cdk-dialog.d.ts +546 -0
- package/types/forty-cdk-disclosure.d.ts +127 -0
- package/types/forty-cdk-drag-drop.d.ts +456 -0
- package/types/forty-cdk-drawer.d.ts +871 -0
- package/types/forty-cdk-dropdown-menu.d.ts +242 -0
- package/types/forty-cdk-field.d.ts +236 -0
- package/types/forty-cdk-fieldset.d.ts +119 -0
- package/types/forty-cdk-file-upload.d.ts +124 -0
- package/types/forty-cdk-hover-card.d.ts +320 -0
- package/types/forty-cdk-input.d.ts +169 -0
- package/types/forty-cdk-internationalized-date.d.ts +1 -1
- package/types/forty-cdk-listbox.d.ts +513 -0
- package/types/forty-cdk-menu.d.ts +629 -0
- package/types/forty-cdk-menubar.d.ts +451 -0
- package/types/forty-cdk-meter.d.ts +122 -0
- package/types/forty-cdk-navigation-menu.d.ts +514 -0
- package/types/forty-cdk-number-input.d.ts +319 -0
- package/types/forty-cdk-otp-input.d.ts +248 -0
- package/types/forty-cdk-pagination.d.ts +214 -0
- package/types/forty-cdk-pane-resizer.d.ts +145 -0
- package/types/forty-cdk-popover.d.ts +509 -0
- package/types/forty-cdk-progress.d.ts +143 -0
- package/types/forty-cdk-radio-group.d.ts +222 -0
- package/types/forty-cdk-scroll-area.d.ts +258 -0
- package/types/forty-cdk-search.d.ts +142 -0
- package/types/forty-cdk-select.d.ts +899 -0
- package/types/forty-cdk-separator.d.ts +59 -0
- package/types/forty-cdk-signal-forms.d.ts +58 -0
- package/types/forty-cdk-slider.d.ts +379 -0
- package/types/forty-cdk-stepper.d.ts +650 -0
- package/types/forty-cdk-switch.d.ts +87 -0
- package/types/forty-cdk-table.d.ts +723 -0
- package/types/forty-cdk-tabs.d.ts +235 -0
- package/types/forty-cdk-time-field.d.ts +307 -0
- package/types/forty-cdk-time-picker.d.ts +578 -0
- package/types/forty-cdk-toast.d.ts +598 -0
- package/types/forty-cdk-toggle.d.ts +310 -0
- package/types/forty-cdk-toolbar.d.ts +217 -0
- package/types/forty-cdk-tooltip.d.ts +436 -0
- package/types/forty-cdk-tree.d.ts +688 -0
- package/types/forty-cdk.d.ts +1 -19743
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, computed, model, input, booleanAttribute, numberAttribute, output, signal, contentChild, effect, isDevMode, Directive, ElementRef } from '@angular/core';
|
|
3
|
+
import { createDefaults, FormUiControlBase, IdGenerator, injectDateAdapter, injectTextDirection, FOR_TIME_VALUE_SOURCE, injectHiddenInput, serializeISODate, clampToBounds, composeWithTime, assertTimeCapable, adoptHostId, createVetoableNativeEvent, emitVetoableEvent, registerHandle, reflectDisabled, injectModalShell, injectOverlayShell } from 'forty-cdk/core';
|
|
4
|
+
import { ForCalendar } from 'forty-cdk/calendar';
|
|
5
|
+
|
|
6
|
+
/** Injection token for {@link ForDatePickerContext}, provided by `ForDatePicker`. */
|
|
7
|
+
const FOR_DATE_PICKER_CONTEXT = new InjectionToken('FOR_DATE_PICKER_CONTEXT');
|
|
8
|
+
/**
|
|
9
|
+
* Injects the nearest {@link ForDatePickerContext}, throwing a descriptive
|
|
10
|
+
* error when used outside a `[forDatePicker]` element.
|
|
11
|
+
*
|
|
12
|
+
* @param piece Name of the calling directive, used in the error message.
|
|
13
|
+
*/
|
|
14
|
+
function injectDatePickerContext(piece) {
|
|
15
|
+
const ctx = inject(FOR_DATE_PICKER_CONTEXT, { optional: true });
|
|
16
|
+
if (!ctx) {
|
|
17
|
+
throw new Error(`[forty-cdk/date-picker] ${piece} must be used inside a [forDatePicker] element. ` +
|
|
18
|
+
"If it is declared inside an ng-template, DI resolves at the template's declaration site — " +
|
|
19
|
+
'not where it is stamped (e.g. via ngTemplateOutlet) — so declare the template inside the ' +
|
|
20
|
+
'[forDatePicker] root.');
|
|
21
|
+
}
|
|
22
|
+
return ctx;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the trigger's root context: the explicit reference when the
|
|
26
|
+
* `[forDatePickerTrigger]` input carries one, the injected
|
|
27
|
+
* `FOR_DATE_PICKER_CONTEXT` otherwise. The orphan error only fires when neither
|
|
28
|
+
* resolves, on first read of the returned signal. Must be called in an
|
|
29
|
+
* injection context.
|
|
30
|
+
*/
|
|
31
|
+
function injectDatePickerTriggerContext(explicitRoot) {
|
|
32
|
+
const injected = inject(FOR_DATE_PICKER_CONTEXT, { optional: true });
|
|
33
|
+
return computed(() => {
|
|
34
|
+
const explicit = explicitRoot();
|
|
35
|
+
if (explicit !== '') {
|
|
36
|
+
return explicit;
|
|
37
|
+
}
|
|
38
|
+
if (injected) {
|
|
39
|
+
return injected;
|
|
40
|
+
}
|
|
41
|
+
throw new Error('[forty-cdk/date-picker] ForDatePickerTrigger could not resolve its [forDatePicker] root: ' +
|
|
42
|
+
'no FOR_DATE_PICKER_CONTEXT provider is visible and no explicit root reference was passed. ' +
|
|
43
|
+
"If this trigger is declared inside an ng-template, DI resolves at the template's declaration " +
|
|
44
|
+
'site — not where it is stamped — so either declare the template inside the root or pass the ' +
|
|
45
|
+
'root explicitly: [forDatePickerTrigger]="root" with #root="forDatePicker".');
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Library fallback for date-picker defaults, read at the root injector when no
|
|
51
|
+
* consumer has called `provideForDatePickerDefaults`. Exported for the shared
|
|
52
|
+
* defaults contract spec; not re-exported from the primitive's public entry.
|
|
53
|
+
*/
|
|
54
|
+
const FOR_DATE_PICKER_FALLBACK_DEFAULTS = {
|
|
55
|
+
sideOffset: 8,
|
|
56
|
+
collisionPadding: 8,
|
|
57
|
+
};
|
|
58
|
+
const { token, provideDefaults } = createDefaults('FOR_DATE_PICKER_DEFAULTS', FOR_DATE_PICKER_FALLBACK_DEFAULTS);
|
|
59
|
+
/** Token holding the resolved date-picker defaults for the current scope. */
|
|
60
|
+
const FOR_DATE_PICKER_DEFAULTS = token;
|
|
61
|
+
/**
|
|
62
|
+
* Configures forty-cdk date-picker defaults for this injector scope. Partial
|
|
63
|
+
* overrides inherit unspecified keys from the parent scope (or library
|
|
64
|
+
* defaults at the root).
|
|
65
|
+
*/
|
|
66
|
+
function provideForDatePickerDefaults(defaults = {}) {
|
|
67
|
+
return provideDefaults(defaults);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Headless date picker — the [WAI-ARIA Date Picker Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/)
|
|
72
|
+
* reinterpreted idiomatically for modern Angular: a focusable trigger that opens
|
|
73
|
+
* a floating surface wrapping a projected `ForCalendar`.
|
|
74
|
+
*
|
|
75
|
+
* `ForDatePicker` is the root and the form value: it implements
|
|
76
|
+
* `FormValueControl<D | null>` from `@angular/forms/signals`, so it auto-wires
|
|
77
|
+
* with `[formField]` and auto-associates inside a `[forField]`. The trigger is
|
|
78
|
+
* the focusable control that carries `name` / `disabled` / `invalid`; selection
|
|
79
|
+
* state flows root → projected calendar via `[(value)]`.
|
|
80
|
+
*
|
|
81
|
+
* The surface defaults to a **non-modal popover** (anchored to the trigger,
|
|
82
|
+
* dismiss on Escape / outside-pointer, return focus on close); set `modal`
|
|
83
|
+
* for the trapped / inert / scroll-locked dialog variant. Mount/unmount of the surface is the consumer's job — wrap
|
|
84
|
+
* `[forDatePickerContent]` with `@if (open())`.
|
|
85
|
+
*
|
|
86
|
+
* The projected `ForCalendar` is two-way bound by the consumer (`[(value)]`)
|
|
87
|
+
* and forwarded `[min]` / `[max]` / `[isDateUnavailable]` from the picker's
|
|
88
|
+
* accessors. A selection inside the grid is observed through a `contentChild`
|
|
89
|
+
* query on the calendar's `valueChange`, so the calendar primitive stays
|
|
90
|
+
* untouched; on selection the picker mirrors the value, flips `touched`, and —
|
|
91
|
+
* when `closeOnSelect` is on (default) — closes the surface.
|
|
92
|
+
*
|
|
93
|
+
* Set `granularity` coarser-than-a-day off (`'hour'` / `'minute'` / `'second'`)
|
|
94
|
+
* to make it a **date-time picker**: the consumer projects a `[forTimeField]`
|
|
95
|
+
* beside the calendar (binding both children **one-way** to `picker.value()` so
|
|
96
|
+
* their internal writes don't clobber each other), and the picker grafts the
|
|
97
|
+
* entered time onto each calendar selection. This needs a time-capable adapter
|
|
98
|
+
* (`provideNativeDateAdapter()` / `provideInternationalizedDateTimeAdapter()`).
|
|
99
|
+
*
|
|
100
|
+
* @typeParam D The adapter's immutable date (or, with `granularity > 'day'`,
|
|
101
|
+
* date-time) type.
|
|
102
|
+
*
|
|
103
|
+
* Note: the date bounds are named `minDate` / `maxDate`, not `min` / `max` —
|
|
104
|
+
* the latter are reserved `FormUiControl` members typed `number | undefined`
|
|
105
|
+
* for numeric validators, so a date-typed `min` / `max` would break the
|
|
106
|
+
* `FormValueControl` contract.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```html
|
|
110
|
+
* <div forDatePicker [(value)]="date" [(open)]="open" [minDate]="min" [maxDate]="max"
|
|
111
|
+
* name="dob" [ariaLabel]="'Choose date'" #picker="forDatePicker">
|
|
112
|
+
* <button forDatePickerTrigger>
|
|
113
|
+
* <span forDatePickerValue>Pick a date</span>
|
|
114
|
+
* </button>
|
|
115
|
+
*
|
|
116
|
+
* @if (open()) {
|
|
117
|
+
* <div forDatePickerContent>
|
|
118
|
+
* <div forCalendar [(value)]="date" [min]="picker.minDate()" [max]="picker.maxDate()">
|
|
119
|
+
* <!-- …calendar header + grid… -->
|
|
120
|
+
* </div>
|
|
121
|
+
* </div>
|
|
122
|
+
* }
|
|
123
|
+
* </div>
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @example Date-time picker (`granularity="minute"`), children bound one-way:
|
|
127
|
+
* ```html
|
|
128
|
+
* <div forDatePicker [(value)]="when" [(open)]="open" granularity="minute"
|
|
129
|
+
* [hourCycle]="24" #picker="forDatePicker">
|
|
130
|
+
* <button forDatePickerTrigger><span forDatePickerValue>Pick date & time</span></button>
|
|
131
|
+
* @if (open()) {
|
|
132
|
+
* <div forDatePickerContent>
|
|
133
|
+
* <div forCalendar [value]="picker.value()" [min]="picker.minDate()">…</div>
|
|
134
|
+
* <div forTimeField [value]="picker.value()" [hourCycle]="picker.hourCycle()">…</div>
|
|
135
|
+
* </div>
|
|
136
|
+
* }
|
|
137
|
+
* </div>
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
class ForDatePicker extends FormUiControlBase {
|
|
141
|
+
#idGen = inject(IdGenerator);
|
|
142
|
+
#defaults = inject(FOR_DATE_PICKER_DEFAULTS);
|
|
143
|
+
/** The active date adapter, resolved from `FOR_DATE_ADAPTER` (shared with `ForCalendar`). */
|
|
144
|
+
adapter = injectDateAdapter('ForDatePicker');
|
|
145
|
+
/**
|
|
146
|
+
* Two-way bindable selected date, or `null`. Required by
|
|
147
|
+
* `FormValueControl<D | null>`. The `model()` change emitter (`(valueChange)`)
|
|
148
|
+
* fires only when the picker itself commits a selection, never on consumer
|
|
149
|
+
* writes via `[(value)]`.
|
|
150
|
+
*/
|
|
151
|
+
value = model(null, /* @ts-ignore */
|
|
152
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
153
|
+
/**
|
|
154
|
+
* Two-way bindable. Whether the surface is open. The `model()` change emitter
|
|
155
|
+
* (`(openChange)`) fires only on internal transitions (trigger toggle,
|
|
156
|
+
* Escape, outside dismissal, selection), never on consumer writes via
|
|
157
|
+
* `[(open)]`.
|
|
158
|
+
*/
|
|
159
|
+
open = model(false, /* @ts-ignore */
|
|
160
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
161
|
+
/**
|
|
162
|
+
* Minimum selectable date (inclusive). Forward to the projected calendar's
|
|
163
|
+
* `[min]`. Named `minDate` (not `min`) because `FormUiControl.min` is reserved
|
|
164
|
+
* for a numeric validator bound by `[formField]`.
|
|
165
|
+
*/
|
|
166
|
+
minDate = input(null, /* @ts-ignore */
|
|
167
|
+
...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
|
|
168
|
+
/**
|
|
169
|
+
* Maximum selectable date (inclusive). Forward to the projected calendar's
|
|
170
|
+
* `[max]`. Named `maxDate` (not `max`) for the same reason as {@link minDate}.
|
|
171
|
+
*/
|
|
172
|
+
maxDate = input(null, /* @ts-ignore */
|
|
173
|
+
...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
|
|
174
|
+
/** Per-date predicate. Forward to the projected calendar's `[isDateUnavailable]`. */
|
|
175
|
+
isDateUnavailable = input(() => false, /* @ts-ignore */
|
|
176
|
+
...(ngDevMode ? [{ debugName: "isDateUnavailable" }] : /* istanbul ignore next */ []));
|
|
177
|
+
/**
|
|
178
|
+
* Close the surface after a date is selected in the projected calendar.
|
|
179
|
+
* Default `true`. Honoured only at `granularity="day"` — a date-time picker
|
|
180
|
+
* (`granularity > 'day'`) never closes on a calendar selection, so the user
|
|
181
|
+
* can go on to edit the time.
|
|
182
|
+
*/
|
|
183
|
+
closeOnSelect = input(true, { ...(ngDevMode ? { debugName: "closeOnSelect" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
184
|
+
/**
|
|
185
|
+
* Date-time precision. `'day'` (default, **non-breaking**) keeps a pure
|
|
186
|
+
* calendar picker. Anything coarser-than-a-day off — `'hour'` / `'minute'` /
|
|
187
|
+
* `'second'` — turns it into a date-time picker: the consumer projects a
|
|
188
|
+
* `[forTimeField]` beside the calendar, a calendar selection preserves the
|
|
189
|
+
* entered time, and the value carries a time component. Requires a
|
|
190
|
+
* time-capable adapter (`provideNativeDateAdapter()` or
|
|
191
|
+
* `provideInternationalizedDateTimeAdapter()`).
|
|
192
|
+
*/
|
|
193
|
+
granularity = input('day', /* @ts-ignore */
|
|
194
|
+
...(ngDevMode ? [{ debugName: "granularity" }] : /* istanbul ignore next */ []));
|
|
195
|
+
/**
|
|
196
|
+
* 12- or 24-hour cycle forwarded to `[forDatePickerValue]`'s formatting (and
|
|
197
|
+
* typically to the projected `[forTimeField][hourCycle]`). When `null`
|
|
198
|
+
* (default) it is derived from the runtime locale. Only meaningful when
|
|
199
|
+
* `granularity > 'day'`.
|
|
200
|
+
*/
|
|
201
|
+
hourCycle = input(null, /* @ts-ignore */
|
|
202
|
+
...(ngDevMode ? [{ debugName: "hourCycle" }] : /* istanbul ignore next */ []));
|
|
203
|
+
/**
|
|
204
|
+
* When `true`, the surface is a trapped / inert / scroll-locked modal dialog
|
|
205
|
+
* (routed through `_internal/modal-shell`) instead of the default non-modal
|
|
206
|
+
* anchored popover. Read once when the content mounts.
|
|
207
|
+
*/
|
|
208
|
+
modal = input(false, { ...(ngDevMode ? { debugName: "modal" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
209
|
+
/** When true (default), Escape, pointer-down outside, and focus outside close the surface. */
|
|
210
|
+
dismissible = input(true, { ...(ngDevMode ? { debugName: "dismissible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
211
|
+
/** When true (default), focus returns to the trigger on close. */
|
|
212
|
+
returnFocus = input(true, { ...(ngDevMode ? { debugName: "returnFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
213
|
+
/**
|
|
214
|
+
* `Intl.DateTimeFormat` options driving the text rendered by
|
|
215
|
+
* `[forDatePickerValue]`. Default `{ year: 'numeric', month: 'long', day: 'numeric' }`.
|
|
216
|
+
*/
|
|
217
|
+
formatOptions = input({
|
|
218
|
+
year: 'numeric',
|
|
219
|
+
month: 'long',
|
|
220
|
+
day: 'numeric',
|
|
221
|
+
}, /* @ts-ignore */
|
|
222
|
+
...(ngDevMode ? [{ debugName: "formatOptions" }] : /* istanbul ignore next */ []));
|
|
223
|
+
/** Text rendered by `[forDatePickerValue]` when no date is selected. */
|
|
224
|
+
placeholder = input('', /* @ts-ignore */
|
|
225
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
226
|
+
/**
|
|
227
|
+
* Selection mode. `'single'` (default) keeps the existing single-date
|
|
228
|
+
* `[(value)]` and `FormValueControl<D | null>` behaviour unchanged.
|
|
229
|
+
* `'range'` switches to the two-click anchor → commit flow and exposes the
|
|
230
|
+
* result through `[(range)]`. Range mode is day-granular and ignores
|
|
231
|
+
* `granularity` / time. Range is not a Signal Forms value in v1.
|
|
232
|
+
*/
|
|
233
|
+
selectionMode = input('single', /* @ts-ignore */
|
|
234
|
+
...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
|
|
235
|
+
/**
|
|
236
|
+
* Two-way bindable committed date range, or `null`. Only used when
|
|
237
|
+
* `selectionMode="range"`. The `model()` change emitter (`(rangeChange)`)
|
|
238
|
+
* fires only when the picker internally commits or clears a range, never on
|
|
239
|
+
* consumer writes via `[(range)]`.
|
|
240
|
+
*/
|
|
241
|
+
range = model(null, /* @ts-ignore */
|
|
242
|
+
...(ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []));
|
|
243
|
+
/**
|
|
244
|
+
* Separator rendered between start and end in `[forDatePickerValue]` while
|
|
245
|
+
* in `selectionMode="range"`. Default `' – '` (en-dash with spaces).
|
|
246
|
+
*/
|
|
247
|
+
rangeSeparator = input(' – ', /* @ts-ignore */
|
|
248
|
+
...(ngDevMode ? [{ debugName: "rangeSeparator" }] : /* istanbul ignore next */ []));
|
|
249
|
+
/** Side the surface is anchored to. Defaults to `'bottom'`. Ignored in `modal` mode. */
|
|
250
|
+
side = input('bottom', /* @ts-ignore */
|
|
251
|
+
...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
|
|
252
|
+
/** Alignment along the chosen `side`. Defaults to `'start'`. Ignored in `modal` mode. */
|
|
253
|
+
align = input('start', /* @ts-ignore */
|
|
254
|
+
...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
|
|
255
|
+
/** Gap (px) between trigger and surface along the main axis. Default from `provideForDatePickerDefaults`. */
|
|
256
|
+
sideOffset = input(this.#defaults.sideOffset, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
257
|
+
/** Gap (px) along the cross axis. Default `0`. */
|
|
258
|
+
alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
259
|
+
/** When `true` (default), `flip` and `shift` keep the surface inside the viewport. */
|
|
260
|
+
avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
261
|
+
/** Padding (px) applied uniformly to flip / shift / size. Default from `provideForDatePickerDefaults`. */
|
|
262
|
+
collisionPadding = input(this.#defaults.collisionPadding, { ...(ngDevMode ? { debugName: "collisionPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
263
|
+
/** Stickiness behaviour for `shift`. Default `'partial'`. */
|
|
264
|
+
sticky = input('partial', /* @ts-ignore */
|
|
265
|
+
...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
|
|
266
|
+
/** When `true`, sets `data-detached=""` while the trigger is scrolled off-screen. */
|
|
267
|
+
hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
268
|
+
/**
|
|
269
|
+
* When `true` (default), the content is clipped until floating-ui resolves
|
|
270
|
+
* its first position, preventing a flash at the viewport corner. Set to
|
|
271
|
+
* `false` so a dramatic `animate.enter` plays from its first frame (the
|
|
272
|
+
* surface may flash briefly at the unresolved position while positioning
|
|
273
|
+
* computes).
|
|
274
|
+
*/
|
|
275
|
+
clipUntilPositioned = input(true, { ...(ngDevMode ? { debugName: "clipUntilPositioned" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
276
|
+
/** Accessible name for the dialog surface. Emits no `aria-label` while `null`. */
|
|
277
|
+
ariaLabel = input(null, /* @ts-ignore */
|
|
278
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
279
|
+
/**
|
|
280
|
+
* Writing direction. When unset (default `null`), the inherited ambient
|
|
281
|
+
* direction is resolved from the nearest ancestor carrying a `dir` attribute
|
|
282
|
+
* (or `<html dir>`), defaulting to `'ltr'`. An explicit `[dir]` always wins.
|
|
283
|
+
* The resolved value is reflected to the host `dir` attribute.
|
|
284
|
+
*/
|
|
285
|
+
_dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
|
|
286
|
+
dir = injectTextDirection(this._dirInput);
|
|
287
|
+
/**
|
|
288
|
+
* Fires when the user presses Escape while this surface is the topmost
|
|
289
|
+
* dismissable layer. Call `preventDefault()` on the veto to suppress the
|
|
290
|
+
* automatic close.
|
|
291
|
+
*/
|
|
292
|
+
escapeKeyDown = output();
|
|
293
|
+
/** Fires when a pointer goes down outside the surface (and trigger). Vetoable. */
|
|
294
|
+
pointerDownOutside = output();
|
|
295
|
+
/** Fires when focus moves outside the surface (and trigger). Vetoable. */
|
|
296
|
+
focusOutside = output();
|
|
297
|
+
/** Composite event: shares veto state with `pointerDownOutside` / `focusOutside`. */
|
|
298
|
+
interactOutside = output();
|
|
299
|
+
/** Fires just before the surface sends focus into itself on open. Vetoable. */
|
|
300
|
+
autoFocusOnOpen = output();
|
|
301
|
+
/** Fires just before focus returns to the trigger on close. Vetoable. */
|
|
302
|
+
autoFocusOnClose = output();
|
|
303
|
+
triggerId = signal(this.#idGen.next('for-date-picker-trigger'), /* @ts-ignore */
|
|
304
|
+
...(ngDevMode ? [{ debugName: "triggerId" }] : /* istanbul ignore next */ []));
|
|
305
|
+
contentId = signal(this.#idGen.next('for-date-picker-content'), /* @ts-ignore */
|
|
306
|
+
...(ngDevMode ? [{ debugName: "contentId" }] : /* istanbul ignore next */ []));
|
|
307
|
+
#triggerEl = signal(null, /* @ts-ignore */
|
|
308
|
+
...(ngDevMode ? [{ debugName: "#triggerEl" }] : /* istanbul ignore next */ []));
|
|
309
|
+
trigger = this.#triggerEl.asReadonly();
|
|
310
|
+
#anchorEl = signal(null, /* @ts-ignore */
|
|
311
|
+
...(ngDevMode ? [{ debugName: "#anchorEl" }] : /* istanbul ignore next */ []));
|
|
312
|
+
/**
|
|
313
|
+
* Element floating-ui anchors the surface against. Prefers an optional
|
|
314
|
+
* `[forDatePickerAnchor]` when registered, otherwise falls back to the
|
|
315
|
+
* trigger so existing pickers without an anchor keep their behavior.
|
|
316
|
+
* Decoupled from `trigger` so the trigger keeps driving `aria-controls`, the
|
|
317
|
+
* click toggle, focus return, and its dismissal exemption regardless of where
|
|
318
|
+
* the surface paints.
|
|
319
|
+
*/
|
|
320
|
+
reference = computed(() => this.#anchorEl() ?? this.#triggerEl(), /* @ts-ignore */
|
|
321
|
+
...(ngDevMode ? [{ debugName: "reference" }] : /* istanbul ignore next */ []));
|
|
322
|
+
#contentEl = signal(null, /* @ts-ignore */
|
|
323
|
+
...(ngDevMode ? [{ debugName: "#contentEl" }] : /* istanbul ignore next */ []));
|
|
324
|
+
content = this.#contentEl.asReadonly();
|
|
325
|
+
/**
|
|
326
|
+
* `formatOptions` augmented with time fields when `granularity > 'day'` and
|
|
327
|
+
* the consumer hasn't already specified any — so a date-time picker's value
|
|
328
|
+
* display shows the time without extra wiring, while an explicit
|
|
329
|
+
* `formatOptions` is always honoured verbatim.
|
|
330
|
+
*/
|
|
331
|
+
#effectiveFormatOptions = computed(() => {
|
|
332
|
+
const options = this.formatOptions();
|
|
333
|
+
const granularity = this.granularity();
|
|
334
|
+
if (granularity === 'day' ||
|
|
335
|
+
options.hour !== undefined ||
|
|
336
|
+
options.minute !== undefined ||
|
|
337
|
+
options.second !== undefined) {
|
|
338
|
+
return options;
|
|
339
|
+
}
|
|
340
|
+
const cycle = this.hourCycle();
|
|
341
|
+
return {
|
|
342
|
+
...options,
|
|
343
|
+
hour: 'numeric',
|
|
344
|
+
minute: '2-digit',
|
|
345
|
+
...(granularity === 'second' ? { second: '2-digit' } : {}),
|
|
346
|
+
...(cycle !== null ? { hour12: cycle === 12 } : {}),
|
|
347
|
+
};
|
|
348
|
+
}, /* @ts-ignore */
|
|
349
|
+
...(ngDevMode ? [{ debugName: "#effectiveFormatOptions" }] : /* istanbul ignore next */ []));
|
|
350
|
+
/** Formatted current value via the adapter, or `null` when empty. */
|
|
351
|
+
formattedValue = computed(() => {
|
|
352
|
+
if (this.selectionMode() === 'range') {
|
|
353
|
+
const r = this.range();
|
|
354
|
+
if (r === null) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const fmtOpts = this.#effectiveFormatOptions();
|
|
358
|
+
return (this.adapter.format(r.start, fmtOpts) +
|
|
359
|
+
this.rangeSeparator() +
|
|
360
|
+
this.adapter.format(r.end, fmtOpts));
|
|
361
|
+
}
|
|
362
|
+
const value = this.value();
|
|
363
|
+
return value === null ? null : this.adapter.format(value, this.#effectiveFormatOptions());
|
|
364
|
+
}, /* @ts-ignore */
|
|
365
|
+
...(ngDevMode ? [{ debugName: "formattedValue" }] : /* istanbul ignore next */ []));
|
|
366
|
+
/**
|
|
367
|
+
* The projected `ForCalendar`. Mounts only while the surface is open, so the
|
|
368
|
+
* query resolves to the live instance on open and to `undefined` on close.
|
|
369
|
+
* Its `valueChange` is the single signal that a date was selected inside the
|
|
370
|
+
* grid — wired in the constructor.
|
|
371
|
+
*
|
|
372
|
+
* Invariant: the projected calendar MUST resolve the same `DateAdapter` as
|
|
373
|
+
* this picker (the same `FOR_DATE_ADAPTER` scope). Angular's `contentChild`
|
|
374
|
+
* erases the generic, so the bridge reads `calendar.value` as `D | null` via
|
|
375
|
+
* a cast; a mismatched adapter would leak a wrong-shaped date through that
|
|
376
|
+
* seam. A dev-mode assertion in the bridge effect catches it early.
|
|
377
|
+
*/
|
|
378
|
+
calendar = contentChild(ForCalendar, { ...(ngDevMode ? { debugName: "calendar" } : /* istanbul ignore next */ {}), descendants: true });
|
|
379
|
+
/**
|
|
380
|
+
* The projected `ForTimeField`, present only in a date-time picker
|
|
381
|
+
* (`granularity > 'day'`). Like the calendar, it mounts with the surface; its
|
|
382
|
+
* `valueChange` (a composed date-time, anchored on the picker's current
|
|
383
|
+
* value) is mirrored straight into the picker's value.
|
|
384
|
+
*
|
|
385
|
+
* Invariant: the projected time field MUST resolve the same `DateAdapter` as
|
|
386
|
+
* this picker (see {@link calendar}). The bridge casts its `value` to
|
|
387
|
+
* `D | null` because `contentChild` erases the generic; a dev-mode assertion
|
|
388
|
+
* guards the same-adapter contract.
|
|
389
|
+
*/
|
|
390
|
+
timeSource = contentChild(FOR_TIME_VALUE_SOURCE, { ...(ngDevMode ? { debugName: "timeSource" } : /* istanbul ignore next */ {}), descendants: true });
|
|
391
|
+
constructor() {
|
|
392
|
+
super();
|
|
393
|
+
injectHiddenInput({
|
|
394
|
+
name: this.name,
|
|
395
|
+
values: computed(() => {
|
|
396
|
+
const value = this.value();
|
|
397
|
+
if (value === null) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
return [serializeISODate(this.adapter, value, this.granularity(), 'ForDatePicker')];
|
|
401
|
+
}),
|
|
402
|
+
disabled: this.effectiveDisabled,
|
|
403
|
+
});
|
|
404
|
+
// Eager validation: a date-time picker needs a time-capable adapter. Fail
|
|
405
|
+
// loudly as soon as the granularity input settles, rather than on first
|
|
406
|
+
// selection deep in a subscription. The throw is raised during change
|
|
407
|
+
// detection (inside this `effect`) and propagates through Angular's error
|
|
408
|
+
// handling, so a day-only adapter misconfiguration is surfaced — never
|
|
409
|
+
// silently swallowed.
|
|
410
|
+
effect(() => {
|
|
411
|
+
if (this.granularity() !== 'day') {
|
|
412
|
+
this.#time();
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
// Calendar selection bridge. This `effect` does no state derivation — it
|
|
416
|
+
// only (re)subscribes to the projected calendar's `valueChange` or
|
|
417
|
+
// `rangeChange` as the surface mounts / unmounts. The writes happen
|
|
418
|
+
// asynchronously in the subscription callback (a discrete selection event),
|
|
419
|
+
// exactly like a click handler, never during the effect's reactive
|
|
420
|
+
// computation.
|
|
421
|
+
effect((onCleanup) => {
|
|
422
|
+
const calendar = this.calendar();
|
|
423
|
+
if (!calendar) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (isDevMode() && calendar.adapter !== this.adapter) {
|
|
427
|
+
throw new Error('[forty-cdk/date-picker] The projected ForCalendar must use the same DateAdapter as the ForDatePicker.');
|
|
428
|
+
}
|
|
429
|
+
if (this.selectionMode() === 'range') {
|
|
430
|
+
const sub = calendar.range.subscribe((next) => {
|
|
431
|
+
if (this.readonly() || this.effectiveDisabled()) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this.range.set(next);
|
|
435
|
+
this.markTouched();
|
|
436
|
+
if (next !== null && this.closeOnSelect()) {
|
|
437
|
+
this.close();
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
onCleanup(() => sub.unsubscribe());
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const sub = calendar.value.subscribe((date) => {
|
|
444
|
+
if (this.readonly() || this.effectiveDisabled()) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const selected = date;
|
|
448
|
+
// The projected calendar already preserves the time-of-day when it is
|
|
449
|
+
// one-way bound (`[value]`) to a timed value — `selectDate` re-grafts
|
|
450
|
+
// the current time via `#withPreservedTime`. This graft is a defensive
|
|
451
|
+
// fallback for a date-time picker whose calendar value was null or
|
|
452
|
+
// midnight (e.g. the very first selection): re-apply the previously
|
|
453
|
+
// entered time-of-day. Reading `value()` here is safe because the
|
|
454
|
+
// one-way binding means the calendar's own write didn't clobber it.
|
|
455
|
+
if (selected !== null && this.granularity() !== 'day') {
|
|
456
|
+
const base = this.value() ?? selected;
|
|
457
|
+
this.value.set(clampToBounds(this.adapter, composeWithTime(this.#time(), selected, base), this.minDate(), this.maxDate()));
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
this.value.set(selected === null
|
|
461
|
+
? null
|
|
462
|
+
: clampToBounds(this.adapter, selected, this.minDate(), this.maxDate()));
|
|
463
|
+
}
|
|
464
|
+
this.markTouched();
|
|
465
|
+
// A date-time picker stays open after a day is picked so the time can
|
|
466
|
+
// still be edited; only a pure day picker honours `closeOnSelect`.
|
|
467
|
+
if (this.closeOnSelect() && this.granularity() === 'day') {
|
|
468
|
+
this.close();
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
onCleanup(() => sub.unsubscribe());
|
|
472
|
+
});
|
|
473
|
+
// Time-source bridge (date-time pickers only). The projected time source
|
|
474
|
+
// (ForTimeField or ForTimePicker) is bound one-way to the picker's value,
|
|
475
|
+
// so its composed `valueChange` already carries the correct day plus the
|
|
476
|
+
// new time — mirror it straight in.
|
|
477
|
+
effect((onCleanup) => {
|
|
478
|
+
const timeSource = this.timeSource();
|
|
479
|
+
if (!timeSource) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (isDevMode() && timeSource.adapter !== this.adapter) {
|
|
483
|
+
throw new Error('[forty-cdk/date-picker] The projected time source (ForTimeField / ForTimePicker) must use the same DateAdapter as the ForDatePicker.');
|
|
484
|
+
}
|
|
485
|
+
const sub = timeSource.value.subscribe((value) => {
|
|
486
|
+
if (this.readonly() || this.effectiveDisabled()) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const next = value;
|
|
490
|
+
this.value.set(next === null ? null : clampToBounds(this.adapter, next, this.minDate(), this.maxDate()));
|
|
491
|
+
this.markTouched();
|
|
492
|
+
});
|
|
493
|
+
onCleanup(() => sub.unsubscribe());
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
/** The active adapter, narrowed to a time-capable one; throws when it is day-only. */
|
|
497
|
+
#time() {
|
|
498
|
+
return assertTimeCapable(this.adapter, 'ForDatePicker');
|
|
499
|
+
}
|
|
500
|
+
registerTrigger(el) {
|
|
501
|
+
adoptHostId(el, this.triggerId);
|
|
502
|
+
this.#triggerEl.set(el);
|
|
503
|
+
}
|
|
504
|
+
unregisterTrigger(el) {
|
|
505
|
+
if (this.#triggerEl() === el) {
|
|
506
|
+
this.#triggerEl.set(null);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
registerAnchor(el) {
|
|
510
|
+
const current = this.#anchorEl();
|
|
511
|
+
if (current !== null && current !== el) {
|
|
512
|
+
throw new Error('[forty-cdk/date-picker] Multiple [forDatePickerAnchor] inside the same [forDatePicker]; only one is allowed.');
|
|
513
|
+
}
|
|
514
|
+
this.#anchorEl.set(el);
|
|
515
|
+
}
|
|
516
|
+
unregisterAnchor(el) {
|
|
517
|
+
if (this.#anchorEl() === el) {
|
|
518
|
+
this.#anchorEl.set(null);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
registerContent(el) {
|
|
522
|
+
adoptHostId(el, this.contentId);
|
|
523
|
+
this.#contentEl.set(el);
|
|
524
|
+
}
|
|
525
|
+
unregisterContent(el) {
|
|
526
|
+
if (this.#contentEl() === el) {
|
|
527
|
+
this.#contentEl.set(null);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
toggle() {
|
|
531
|
+
if (this.effectiveDisabled()) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this.open.update((v) => !v);
|
|
535
|
+
}
|
|
536
|
+
close() {
|
|
537
|
+
this.markTouched();
|
|
538
|
+
this.open.set(false);
|
|
539
|
+
}
|
|
540
|
+
markTouched() {
|
|
541
|
+
super.markTouched();
|
|
542
|
+
}
|
|
543
|
+
focusCalendarCell() {
|
|
544
|
+
return this.calendar()?.focusActiveCell() ?? false;
|
|
545
|
+
}
|
|
546
|
+
// Both the anchored (`injectOverlayShell`) and modal (`injectModalShell`)
|
|
547
|
+
// surfaces now own the shared `#pendingOutsideVeto` reuse between the
|
|
548
|
+
// specific outside channels and the composite `interactOutside`, so this
|
|
549
|
+
// directive only forwards the shell-built veto to the matching `output()`
|
|
550
|
+
// and owns the close decision via `requestClose`. Escape is the one channel
|
|
551
|
+
// this directive still owns end-to-end on the anchored path (the modal path
|
|
552
|
+
// routes it through `emitEscapeKeyDown` on the shell, which builds the veto
|
|
553
|
+
// and calls `requestClose` itself).
|
|
554
|
+
/**
|
|
555
|
+
* Anchored-path Escape. Builds the veto, emits `(escapeKeyDown)`, and —
|
|
556
|
+
* unless vetoed and `dismissible` is off — stops propagation and closes.
|
|
557
|
+
*/
|
|
558
|
+
emitEscapeKeyDown(event) {
|
|
559
|
+
const veto = createVetoableNativeEvent(event);
|
|
560
|
+
this.escapeKeyDown.emit(veto);
|
|
561
|
+
if (!veto.defaultPrevented && this.dismissible()) {
|
|
562
|
+
event.stopPropagation();
|
|
563
|
+
this.close();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Outside-interaction emit forwarders shared by both shells. The shell
|
|
568
|
+
* builds and reuses the veto; these only fire the matching output.
|
|
569
|
+
*/
|
|
570
|
+
emitPointerDownOutside(veto) {
|
|
571
|
+
this.pointerDownOutside.emit(veto);
|
|
572
|
+
}
|
|
573
|
+
emitFocusOutside(veto) {
|
|
574
|
+
this.focusOutside.emit(veto);
|
|
575
|
+
}
|
|
576
|
+
emitInteractOutside(veto) {
|
|
577
|
+
this.interactOutside.emit(veto);
|
|
578
|
+
}
|
|
579
|
+
/** Modal-path Escape forwarder: emit only; the modal shell owns the close. */
|
|
580
|
+
forwardEscapeKeyDown(veto) {
|
|
581
|
+
this.escapeKeyDown.emit(veto);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Implicit close requested by either shell after an un-vetoed outside
|
|
585
|
+
* interaction (or, on the modal path, an un-vetoed Escape). Marks the
|
|
586
|
+
* control touched and closes.
|
|
587
|
+
*/
|
|
588
|
+
requestClose() {
|
|
589
|
+
this.close();
|
|
590
|
+
}
|
|
591
|
+
emitAutoFocusOnOpen() {
|
|
592
|
+
return emitVetoableEvent(this.autoFocusOnOpen);
|
|
593
|
+
}
|
|
594
|
+
emitAutoFocusOnClose() {
|
|
595
|
+
return emitVetoableEvent(this.autoFocusOnClose);
|
|
596
|
+
}
|
|
597
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDatePicker, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
598
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.0.2", type: ForDatePicker, isStandalone: true, selector: "[forDatePicker]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, isDateUnavailable: { classPropertyName: "isDateUnavailable", publicName: "isDateUnavailable", isSignal: true, isRequired: false, transformFunction: null }, closeOnSelect: { classPropertyName: "closeOnSelect", publicName: "closeOnSelect", isSignal: true, isRequired: false, transformFunction: null }, granularity: { classPropertyName: "granularity", publicName: "granularity", isSignal: true, isRequired: false, transformFunction: null }, hourCycle: { classPropertyName: "hourCycle", publicName: "hourCycle", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, returnFocus: { classPropertyName: "returnFocus", publicName: "returnFocus", isSignal: true, isRequired: false, transformFunction: null }, formatOptions: { classPropertyName: "formatOptions", publicName: "formatOptions", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, range: { classPropertyName: "range", publicName: "range", isSignal: true, isRequired: false, transformFunction: null }, rangeSeparator: { classPropertyName: "rangeSeparator", publicName: "rangeSeparator", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, sideOffset: { classPropertyName: "sideOffset", publicName: "sideOffset", isSignal: true, isRequired: false, transformFunction: null }, alignOffset: { classPropertyName: "alignOffset", publicName: "alignOffset", isSignal: true, isRequired: false, transformFunction: null }, avoidCollisions: { classPropertyName: "avoidCollisions", publicName: "avoidCollisions", isSignal: true, isRequired: false, transformFunction: null }, collisionPadding: { classPropertyName: "collisionPadding", publicName: "collisionPadding", isSignal: true, isRequired: false, transformFunction: null }, sticky: { classPropertyName: "sticky", publicName: "sticky", isSignal: true, isRequired: false, transformFunction: null }, hideWhenDetached: { classPropertyName: "hideWhenDetached", publicName: "hideWhenDetached", isSignal: true, isRequired: false, transformFunction: null }, clipUntilPositioned: { classPropertyName: "clipUntilPositioned", publicName: "clipUntilPositioned", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", open: "openChange", range: "rangeChange", escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside", focusOutside: "focusOutside", interactOutside: "interactOutside", autoFocusOnOpen: "autoFocusOnOpen", autoFocusOnClose: "autoFocusOnClose" }, host: { properties: { "attr.dir": "dir()", "attr.data-state": "open() ? \"open\" : \"closed\"", "attr.data-disabled": "effectiveDisabled() ? \"\" : null" } }, providers: [{ provide: FOR_DATE_PICKER_CONTEXT, useExisting: ForDatePicker }], queries: [{ propertyName: "calendar", first: true, predicate: ForCalendar, descendants: true, isSignal: true }, { propertyName: "timeSource", first: true, predicate: FOR_TIME_VALUE_SOURCE, descendants: true, isSignal: true }], exportAs: ["forDatePicker"], usesInheritance: true, ngImport: i0 });
|
|
599
|
+
}
|
|
600
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDatePicker, decorators: [{
|
|
601
|
+
type: Directive,
|
|
602
|
+
args: [{
|
|
603
|
+
selector: '[forDatePicker]',
|
|
604
|
+
exportAs: 'forDatePicker',
|
|
605
|
+
host: {
|
|
606
|
+
'[attr.dir]': 'dir()',
|
|
607
|
+
'[attr.data-state]': 'open() ? "open" : "closed"',
|
|
608
|
+
'[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
|
|
609
|
+
},
|
|
610
|
+
providers: [{ provide: FOR_DATE_PICKER_CONTEXT, useExisting: ForDatePicker }],
|
|
611
|
+
}]
|
|
612
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], isDateUnavailable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isDateUnavailable", required: false }] }], closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnSelect", required: false }] }], granularity: [{ type: i0.Input, args: [{ isSignal: true, alias: "granularity", required: false }] }], hourCycle: [{ type: i0.Input, args: [{ isSignal: true, alias: "hourCycle", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], returnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "returnFocus", required: false }] }], formatOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatOptions", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], range: [{ type: i0.Input, args: [{ isSignal: true, alias: "range", required: false }] }, { type: i0.Output, args: ["rangeChange"] }], rangeSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeSeparator", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], sideOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideOffset", required: false }] }], alignOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignOffset", required: false }] }], avoidCollisions: [{ type: i0.Input, args: [{ isSignal: true, alias: "avoidCollisions", required: false }] }], collisionPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "collisionPadding", required: false }] }], sticky: [{ type: i0.Input, args: [{ isSignal: true, alias: "sticky", required: false }] }], hideWhenDetached: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideWhenDetached", required: false }] }], clipUntilPositioned: [{ type: i0.Input, args: [{ isSignal: true, alias: "clipUntilPositioned", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }], focusOutside: [{ type: i0.Output, args: ["focusOutside"] }], interactOutside: [{ type: i0.Output, args: ["interactOutside"] }], autoFocusOnOpen: [{ type: i0.Output, args: ["autoFocusOnOpen"] }], autoFocusOnClose: [{ type: i0.Output, args: ["autoFocusOnClose"] }], calendar: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ForCalendar), { ...{ descendants: true }, isSignal: true }] }], timeSource: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FOR_TIME_VALUE_SOURCE), { ...{ descendants: true }, isSignal: true }] }] } });
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Optional positioning anchor. When present, `[forDatePickerContent]` is
|
|
616
|
+
* positioned against this element instead of `[forDatePickerTrigger]` — useful
|
|
617
|
+
* when the trigger lives inside a decorated field box (padding, prefix icon,
|
|
618
|
+
* clear / chevron buttons) and the surface should align to the visible field
|
|
619
|
+
* rather than the inner button.
|
|
620
|
+
*
|
|
621
|
+
* Only positioning changes: the trigger still owns `aria-controls`,
|
|
622
|
+
* `aria-expanded`, the click toggle, focus return on close, and its exemption
|
|
623
|
+
* from outside-pointer dismissal. If no anchor is registered the surface falls
|
|
624
|
+
* back to anchoring against the trigger, so existing usages are unaffected.
|
|
625
|
+
*
|
|
626
|
+
* A date picker's calendar has its own intrinsic width and ignores
|
|
627
|
+
* `--for-anchor-width`, so the practical effect is mostly start / side
|
|
628
|
+
* alignment to the box edge.
|
|
629
|
+
*
|
|
630
|
+
* At most one `[forDatePickerAnchor]` may be registered per `[forDatePicker]`;
|
|
631
|
+
* a second one throws.
|
|
632
|
+
*
|
|
633
|
+
* ```html
|
|
634
|
+
* <div forDatePicker [(value)]="date">
|
|
635
|
+
* <div forDatePickerAnchor class="field-box">
|
|
636
|
+
* <icon />
|
|
637
|
+
* <button forDatePickerTrigger><span forDatePickerValue>…</span></button>
|
|
638
|
+
* <button class="clear">×</button>
|
|
639
|
+
* </div>
|
|
640
|
+
* @if (open()) {
|
|
641
|
+
* <div forDatePickerContent>…</div>
|
|
642
|
+
* }
|
|
643
|
+
* </div>
|
|
644
|
+
* ```
|
|
645
|
+
*/
|
|
646
|
+
class ForDatePickerAnchor {
|
|
647
|
+
#ctx = injectDatePickerContext('ForDatePickerAnchor');
|
|
648
|
+
#host = inject(ElementRef);
|
|
649
|
+
constructor() {
|
|
650
|
+
registerHandle(this.#host.nativeElement, (el) => this.#ctx.registerAnchor(el), (el) => this.#ctx.unregisterAnchor(el));
|
|
651
|
+
}
|
|
652
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDatePickerAnchor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
653
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDatePickerAnchor, isStandalone: true, selector: "[forDatePickerAnchor]", exportAs: ["forDatePickerAnchor"], ngImport: i0 });
|
|
654
|
+
}
|
|
655
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDatePickerAnchor, decorators: [{
|
|
656
|
+
type: Directive,
|
|
657
|
+
args: [{
|
|
658
|
+
selector: '[forDatePickerAnchor]',
|
|
659
|
+
exportAs: 'forDatePickerAnchor',
|
|
660
|
+
}]
|
|
661
|
+
}], ctorParameters: () => [] });
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* The button that opens the date-picker surface — and the focusable form
|
|
665
|
+
* control the picker exposes for `[formField]`. Apply on a `<button>` so
|
|
666
|
+
* Space / Enter dispatch native clicks that toggle via `(click)`.
|
|
667
|
+
*
|
|
668
|
+
* Wires APG Date Picker Dialog attributes: `aria-haspopup="dialog"`,
|
|
669
|
+
* `aria-expanded` reflecting `open()`, and `aria-controls` pointing to the
|
|
670
|
+
* content while open. It also reflects the root's form-control state
|
|
671
|
+
* (`aria-disabled` / `aria-readonly` / `aria-required` / `aria-invalid` /
|
|
672
|
+
* `aria-busy`) so the focusable element advertises validity to assistive tech.
|
|
673
|
+
*
|
|
674
|
+
* The trigger is exempt from the surface's dismissable layer — its own click
|
|
675
|
+
* toggles open/close, so an outside-pointer dismissal never races with it.
|
|
676
|
+
*
|
|
677
|
+
* The root is normally resolved via DI from the enclosing `[forDatePicker]`.
|
|
678
|
+
* When the trigger is declared inside an `ng-template` stamped into the root
|
|
679
|
+
* (e.g. via `ngTemplateOutlet`), DI resolves at the template's declaration
|
|
680
|
+
* site and misses the root — pass it explicitly through the selector input,
|
|
681
|
+
* `routerLink`-style: `[forDatePickerTrigger]="root"` with
|
|
682
|
+
* `#root="forDatePicker"`.
|
|
683
|
+
*/
|
|
684
|
+
class ForDatePickerTrigger {
|
|
685
|
+
#host = inject(ElementRef);
|
|
686
|
+
/**
|
|
687
|
+
* Optional explicit reference to the `[forDatePicker]` root, named after the
|
|
688
|
+
* selector `routerLink`-style. The bare valueless attribute keeps resolving
|
|
689
|
+
* the enclosing root via DI; pass the root explicitly
|
|
690
|
+
* (`[forDatePickerTrigger]="root"`, with `#root="forDatePicker"`) when the
|
|
691
|
+
* trigger is declared in an `ng-template` stamped inside the root — DI
|
|
692
|
+
* resolves at the template's declaration site, so the enclosing root is
|
|
693
|
+
* invisible there. The empty string (what the valueless attribute yields) is
|
|
694
|
+
* treated as unset.
|
|
695
|
+
*/
|
|
696
|
+
forDatePickerTrigger = input('', /* @ts-ignore */
|
|
697
|
+
...(ngDevMode ? [{ debugName: "forDatePickerTrigger" }] : /* istanbul ignore next */ []));
|
|
698
|
+
ctx = injectDatePickerTriggerContext(this.forDatePickerTrigger);
|
|
699
|
+
constructor() {
|
|
700
|
+
const el = this.#host.nativeElement;
|
|
701
|
+
// Registration is an imperative call into the resolved root's registry,
|
|
702
|
+
// not state derivation — the effect only re-registers the element when the
|
|
703
|
+
// resolved root changes (explicit reference swapped at runtime).
|
|
704
|
+
effect((onCleanup) => {
|
|
705
|
+
const ctx = this.ctx();
|
|
706
|
+
ctx.registerTrigger(el);
|
|
707
|
+
onCleanup(() => ctx.unregisterTrigger(el));
|
|
708
|
+
});
|
|
709
|
+
reflectDisabled(computed(() => this.ctx().effectiveDisabled()));
|
|
710
|
+
}
|
|
711
|
+
onClick() {
|
|
712
|
+
this.ctx().toggle();
|
|
713
|
+
}
|
|
714
|
+
onFocusOut(event) {
|
|
715
|
+
const next = event.relatedTarget;
|
|
716
|
+
if (next) {
|
|
717
|
+
// Focus moving into the surface we just opened — not a leave.
|
|
718
|
+
const content = this.ctx().content();
|
|
719
|
+
if (content && content.contains(next)) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
// Focus staying within the trigger's own subtree — not a leave.
|
|
723
|
+
if (this.#host.nativeElement.contains(next)) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
this.ctx().markTouched();
|
|
728
|
+
}
|
|
729
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDatePickerTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
730
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForDatePickerTrigger, isStandalone: true, selector: "[forDatePickerTrigger]", inputs: { forDatePickerTrigger: { classPropertyName: "forDatePickerTrigger", publicName: "forDatePickerTrigger", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button" }, listeners: { "click": "onClick()", "focusout": "onFocusOut($event)" }, properties: { "id": "ctx().triggerId()", "attr.aria-haspopup": "\"dialog\"", "attr.aria-expanded": "ctx().open() ? \"true\" : \"false\"", "attr.aria-controls": "ctx().open() ? ctx().contentId() : null", "attr.aria-disabled": "ctx().effectiveDisabled() ? \"true\" : null", "attr.aria-readonly": "ctx().readonly() ? \"true\" : null", "attr.aria-required": "ctx().required() ? \"true\" : null", "attr.aria-invalid": "ctx().invalid() ? \"true\" : null", "attr.aria-busy": "ctx().pending() ? \"true\" : null", "attr.data-state": "ctx().open() ? \"open\" : \"closed\"", "attr.data-disabled": "ctx().effectiveDisabled() ? \"\" : null" } }, exportAs: ["forDatePickerTrigger"], ngImport: i0 });
|
|
731
|
+
}
|
|
732
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDatePickerTrigger, decorators: [{
|
|
733
|
+
type: Directive,
|
|
734
|
+
args: [{
|
|
735
|
+
selector: '[forDatePickerTrigger]',
|
|
736
|
+
exportAs: 'forDatePickerTrigger',
|
|
737
|
+
host: {
|
|
738
|
+
type: 'button',
|
|
739
|
+
'[id]': 'ctx().triggerId()',
|
|
740
|
+
'[attr.aria-haspopup]': '"dialog"',
|
|
741
|
+
'[attr.aria-expanded]': 'ctx().open() ? "true" : "false"',
|
|
742
|
+
'[attr.aria-controls]': 'ctx().open() ? ctx().contentId() : null',
|
|
743
|
+
'[attr.aria-disabled]': 'ctx().effectiveDisabled() ? "true" : null',
|
|
744
|
+
'[attr.aria-readonly]': 'ctx().readonly() ? "true" : null',
|
|
745
|
+
'[attr.aria-required]': 'ctx().required() ? "true" : null',
|
|
746
|
+
'[attr.aria-invalid]': 'ctx().invalid() ? "true" : null',
|
|
747
|
+
'[attr.aria-busy]': 'ctx().pending() ? "true" : null',
|
|
748
|
+
'[attr.data-state]': 'ctx().open() ? "open" : "closed"',
|
|
749
|
+
'[attr.data-disabled]': 'ctx().effectiveDisabled() ? "" : null',
|
|
750
|
+
'(click)': 'onClick()',
|
|
751
|
+
'(focusout)': 'onFocusOut($event)',
|
|
752
|
+
},
|
|
753
|
+
}]
|
|
754
|
+
}], ctorParameters: () => [], propDecorators: { forDatePickerTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "forDatePickerTrigger", required: false }] }] } });
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* The date-picker surface. Carries `role="dialog"`, is portaled to
|
|
758
|
+
* `document.body`, and wraps the projected `ForCalendar`. It defers all of its
|
|
759
|
+
* lifecycle to one of the two shared shells, picked once on construction from
|
|
760
|
+
* `[forDatePicker].modal`:
|
|
761
|
+
*
|
|
762
|
+
* - **non-modal (default)** — `injectOverlayShell` anchors the surface to the
|
|
763
|
+
* trigger via `@floating-ui/dom`, activates a `DismissableLayer` (Escape,
|
|
764
|
+
* pointer-down outside, focus outside), moves focus to the calendar's roving
|
|
765
|
+
* cell on open, and returns focus to the trigger on close.
|
|
766
|
+
* - **modal** — `injectModalShell` traps focus, inerts the background, and
|
|
767
|
+
* locks body scroll (a centered dialog the consumer positions with CSS, not
|
|
768
|
+
* trigger-anchored). Same Escape / outside-pointer dismiss and return-focus.
|
|
769
|
+
*
|
|
770
|
+
* The mode is read in the constructor rather than reactively because the two
|
|
771
|
+
* shells are structurally different and switching at runtime would require a
|
|
772
|
+
* remount. This is reliable: the surface mounts lazily (the consumer wraps it
|
|
773
|
+
* with `@if (open())`), well after the root's `modal` input has settled.
|
|
774
|
+
*
|
|
775
|
+
* Mount/unmount of the surface is the consumer's responsibility — wrap with
|
|
776
|
+
* `@if (open())` so `animate.enter` / `animate.leave` fire on the natural
|
|
777
|
+
* mount cycle.
|
|
778
|
+
*/
|
|
779
|
+
class ForDatePickerContent {
|
|
780
|
+
ctx = injectDatePickerContext('ForDatePickerContent');
|
|
781
|
+
#host = inject(ElementRef);
|
|
782
|
+
constructor() {
|
|
783
|
+
const ctx = this.ctx;
|
|
784
|
+
registerHandle(this.#host.nativeElement, (el) => ctx.registerContent(el), (el) => ctx.unregisterContent(el));
|
|
785
|
+
if (ctx.modal()) {
|
|
786
|
+
injectModalShell({
|
|
787
|
+
modal: ctx.modal,
|
|
788
|
+
returnFocus: ctx.returnFocus,
|
|
789
|
+
// Land on the calendar's active cell with a first-focusable fallback,
|
|
790
|
+
// matching the non-modal path below — both modes focus the same cell.
|
|
791
|
+
initialFocus: {
|
|
792
|
+
move: () => ctx.focusCalendarCell(),
|
|
793
|
+
veto: () => ctx.emitAutoFocusOnOpen(),
|
|
794
|
+
},
|
|
795
|
+
autoFocusOnOpen: () => (event) => {
|
|
796
|
+
if (ctx.emitAutoFocusOnOpen()) {
|
|
797
|
+
event.preventDefault();
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
autoFocusOnClose: () => (event) => {
|
|
801
|
+
if (ctx.emitAutoFocusOnClose()) {
|
|
802
|
+
event.preventDefault();
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
dismiss: {
|
|
806
|
+
dismissible: ctx.dismissible,
|
|
807
|
+
requestClose: () => ctx.requestClose(),
|
|
808
|
+
emitEscapeKeyDown: (veto) => ctx.forwardEscapeKeyDown(veto),
|
|
809
|
+
emitPointerDownOutside: (veto) => ctx.emitPointerDownOutside(veto),
|
|
810
|
+
emitFocusOutside: (veto) => ctx.emitFocusOutside(veto),
|
|
811
|
+
emitInteractOutside: (veto) => ctx.emitInteractOutside(veto),
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
injectOverlayShell({
|
|
817
|
+
positioner: {
|
|
818
|
+
kind: 'floating',
|
|
819
|
+
reference: ctx.reference,
|
|
820
|
+
open: ctx.open,
|
|
821
|
+
side: ctx.side,
|
|
822
|
+
align: ctx.align,
|
|
823
|
+
sideOffset: ctx.sideOffset,
|
|
824
|
+
alignOffset: ctx.alignOffset,
|
|
825
|
+
avoidCollisions: ctx.avoidCollisions,
|
|
826
|
+
collisionPadding: ctx.collisionPadding,
|
|
827
|
+
sticky: ctx.sticky,
|
|
828
|
+
hideWhenDetached: ctx.hideWhenDetached,
|
|
829
|
+
clipUntilPositioned: ctx.clipUntilPositioned,
|
|
830
|
+
},
|
|
831
|
+
dismiss: {
|
|
832
|
+
dismissible: ctx.dismissible,
|
|
833
|
+
requestClose: () => ctx.requestClose(),
|
|
834
|
+
emitEscapeKeyDown: (event) => ctx.emitEscapeKeyDown(event),
|
|
835
|
+
emitPointerDownOutside: (veto) => ctx.emitPointerDownOutside(veto),
|
|
836
|
+
emitFocusOutside: (veto) => ctx.emitFocusOutside(veto),
|
|
837
|
+
emitInteractOutside: (veto) => ctx.emitInteractOutside(veto),
|
|
838
|
+
// Trigger is exempt — its own click toggles open/close; without
|
|
839
|
+
// exemption pointer-down-outside would race and double-close.
|
|
840
|
+
exemptElements: () => {
|
|
841
|
+
const trigger = ctx.trigger();
|
|
842
|
+
return trigger ? [trigger] : [];
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
// Move focus to the calendar's roving cell on open; the shell falls back
|
|
846
|
+
// to the first focusable descendant when no cell is found.
|
|
847
|
+
initialFocus: {
|
|
848
|
+
move: () => ctx.focusCalendarCell(),
|
|
849
|
+
veto: () => ctx.emitAutoFocusOnOpen(),
|
|
850
|
+
},
|
|
851
|
+
returnFocus: {
|
|
852
|
+
enabled: ctx.returnFocus,
|
|
853
|
+
target: () => ctx.trigger(),
|
|
854
|
+
veto: () => ctx.emitAutoFocusOnClose(),
|
|
855
|
+
},
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDatePickerContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
859
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDatePickerContent, isStandalone: true, selector: "[forDatePickerContent]", host: { attributes: { "role": "dialog", "tabindex": "-1" }, properties: { "id": "ctx.contentId()", "attr.aria-label": "ctx.ariaLabel()", "attr.aria-labelledby": "ctx.ariaLabel() ? null : ctx.triggerId()", "attr.aria-modal": "ctx.modal() ? \"true\" : null", "attr.data-state": "ctx.open() ? \"open\" : \"closed\"" } }, exportAs: ["forDatePickerContent"], ngImport: i0 });
|
|
860
|
+
}
|
|
861
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDatePickerContent, decorators: [{
|
|
862
|
+
type: Directive,
|
|
863
|
+
args: [{
|
|
864
|
+
selector: '[forDatePickerContent]',
|
|
865
|
+
exportAs: 'forDatePickerContent',
|
|
866
|
+
host: {
|
|
867
|
+
role: 'dialog',
|
|
868
|
+
tabindex: '-1',
|
|
869
|
+
'[id]': 'ctx.contentId()',
|
|
870
|
+
'[attr.aria-label]': 'ctx.ariaLabel()',
|
|
871
|
+
'[attr.aria-labelledby]': 'ctx.ariaLabel() ? null : ctx.triggerId()',
|
|
872
|
+
'[attr.aria-modal]': 'ctx.modal() ? "true" : null',
|
|
873
|
+
'[attr.data-state]': 'ctx.open() ? "open" : "closed"',
|
|
874
|
+
},
|
|
875
|
+
}]
|
|
876
|
+
}], ctorParameters: () => [] });
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Renders the selected date — formatted through the adapter with the root's
|
|
880
|
+
* `formatOptions` — or the configured placeholder when nothing is selected,
|
|
881
|
+
* into its host element via `textContent`. Apply on a `<span>` (or any inline
|
|
882
|
+
* element) inside `[forDatePickerTrigger]`:
|
|
883
|
+
*
|
|
884
|
+
* ```html
|
|
885
|
+
* <button forDatePickerTrigger>
|
|
886
|
+
* <span forDatePickerValue placeholder="Pick a date…"></span>
|
|
887
|
+
* </button>
|
|
888
|
+
* ```
|
|
889
|
+
*
|
|
890
|
+
* For fully custom rendering, drop this directive and read
|
|
891
|
+
* `forDatePicker.formattedValue()` / `forDatePicker.value()` from your template.
|
|
892
|
+
*/
|
|
893
|
+
class ForDatePickerValue {
|
|
894
|
+
#ctx = injectDatePickerContext('ForDatePickerValue');
|
|
895
|
+
/** Text shown when no date is selected. Falls back to `[forDatePicker][placeholder]`. */
|
|
896
|
+
placeholder = input('', /* @ts-ignore */
|
|
897
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
898
|
+
isPlaceholder = computed(() => this.#ctx.formattedValue() === null, /* @ts-ignore */
|
|
899
|
+
...(ngDevMode ? [{ debugName: "isPlaceholder" }] : /* istanbul ignore next */ []));
|
|
900
|
+
displayText = computed(() => {
|
|
901
|
+
const formatted = this.#ctx.formattedValue();
|
|
902
|
+
if (formatted !== null) {
|
|
903
|
+
return formatted;
|
|
904
|
+
}
|
|
905
|
+
return this.placeholder() || this.#ctx.placeholder();
|
|
906
|
+
}, /* @ts-ignore */
|
|
907
|
+
...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
|
|
908
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDatePickerValue, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
909
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForDatePickerValue, isStandalone: true, selector: "[forDatePickerValue]", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "textContent": "displayText()", "attr.data-placeholder": "isPlaceholder() ? \"\" : null" } }, exportAs: ["forDatePickerValue"], ngImport: i0 });
|
|
910
|
+
}
|
|
911
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDatePickerValue, decorators: [{
|
|
912
|
+
type: Directive,
|
|
913
|
+
args: [{
|
|
914
|
+
selector: '[forDatePickerValue]',
|
|
915
|
+
exportAs: 'forDatePickerValue',
|
|
916
|
+
host: {
|
|
917
|
+
'[textContent]': 'displayText()',
|
|
918
|
+
'[attr.data-placeholder]': 'isPlaceholder() ? "" : null',
|
|
919
|
+
},
|
|
920
|
+
}]
|
|
921
|
+
}], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }] } });
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Exact public names of every `ForDatePicker` input, its models included. Spread it into the
|
|
925
|
+
* `inputs` array of a `hostDirectives` entry so a wrapper component re-exposes the
|
|
926
|
+
* primitive's full surface — the Signal Forms members `[formField]` binds among them —
|
|
927
|
+
* without hand-maintaining the list. Always spread into an inline object literal as shown
|
|
928
|
+
* below: the literal is what keeps the entry statically analyzable for consumers compiling
|
|
929
|
+
* against the published package. An anti-drift spec fails when this list no longer matches
|
|
930
|
+
* the directive's actual API. See `docs/wrapping-form-primitives.md` for both supported
|
|
931
|
+
* wrapping patterns.
|
|
932
|
+
*
|
|
933
|
+
* @example
|
|
934
|
+
* ```ts
|
|
935
|
+
* @Component({
|
|
936
|
+
* selector: 'div[myDatePicker]',
|
|
937
|
+
* template: '',
|
|
938
|
+
* hostDirectives: [
|
|
939
|
+
* {
|
|
940
|
+
* directive: ForDatePicker,
|
|
941
|
+
* inputs: [...FOR_DATE_PICKER_HOST_DIRECTIVE_INPUTS],
|
|
942
|
+
* outputs: [...FOR_DATE_PICKER_HOST_DIRECTIVE_OUTPUTS],
|
|
943
|
+
* },
|
|
944
|
+
* ],
|
|
945
|
+
* })
|
|
946
|
+
* export class MyDatePicker {}
|
|
947
|
+
* ```
|
|
948
|
+
*/
|
|
949
|
+
const FOR_DATE_PICKER_HOST_DIRECTIVE_INPUTS = [
|
|
950
|
+
'value',
|
|
951
|
+
'open',
|
|
952
|
+
'range',
|
|
953
|
+
'align',
|
|
954
|
+
'alignOffset',
|
|
955
|
+
'ariaLabel',
|
|
956
|
+
'avoidCollisions',
|
|
957
|
+
'clipUntilPositioned',
|
|
958
|
+
'closeOnSelect',
|
|
959
|
+
'collisionPadding',
|
|
960
|
+
'dir',
|
|
961
|
+
'dirty',
|
|
962
|
+
'disabled',
|
|
963
|
+
'dismissible',
|
|
964
|
+
'errors',
|
|
965
|
+
'formatOptions',
|
|
966
|
+
'granularity',
|
|
967
|
+
'hideWhenDetached',
|
|
968
|
+
'hourCycle',
|
|
969
|
+
'invalid',
|
|
970
|
+
'isDateUnavailable',
|
|
971
|
+
'maxDate',
|
|
972
|
+
'minDate',
|
|
973
|
+
'modal',
|
|
974
|
+
'name',
|
|
975
|
+
'pending',
|
|
976
|
+
'placeholder',
|
|
977
|
+
'rangeSeparator',
|
|
978
|
+
'readonly',
|
|
979
|
+
'required',
|
|
980
|
+
'returnFocus',
|
|
981
|
+
'selectionMode',
|
|
982
|
+
'side',
|
|
983
|
+
'sideOffset',
|
|
984
|
+
'sticky',
|
|
985
|
+
'touched',
|
|
986
|
+
];
|
|
987
|
+
/**
|
|
988
|
+
* Exact public names of every `ForDatePicker` output, the Signal Forms `touch` output
|
|
989
|
+
* included. Spread it into the `outputs` array of the same `hostDirectives` entry as
|
|
990
|
+
* {@link FOR_DATE_PICKER_HOST_DIRECTIVE_INPUTS}.
|
|
991
|
+
*/
|
|
992
|
+
const FOR_DATE_PICKER_HOST_DIRECTIVE_OUTPUTS = [
|
|
993
|
+
'valueChange',
|
|
994
|
+
'openChange',
|
|
995
|
+
'rangeChange',
|
|
996
|
+
'escapeKeyDown',
|
|
997
|
+
'pointerDownOutside',
|
|
998
|
+
'focusOutside',
|
|
999
|
+
'interactOutside',
|
|
1000
|
+
'autoFocusOnOpen',
|
|
1001
|
+
'autoFocusOnClose',
|
|
1002
|
+
'touchedChange',
|
|
1003
|
+
'touch',
|
|
1004
|
+
];
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Generated bundle index. Do not edit.
|
|
1008
|
+
*/
|
|
1009
|
+
|
|
1010
|
+
export { FOR_DATE_PICKER_CONTEXT, FOR_DATE_PICKER_DEFAULTS, FOR_DATE_PICKER_HOST_DIRECTIVE_INPUTS, FOR_DATE_PICKER_HOST_DIRECTIVE_OUTPUTS, ForDatePicker, ForDatePickerAnchor, ForDatePickerContent, ForDatePickerTrigger, ForDatePickerValue, provideForDatePickerDefaults };
|
|
1011
|
+
//# sourceMappingURL=forty-cdk-date-picker.mjs.map
|