@koobiq/components 20.3.1 → 20.3.2

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.
Files changed (92) hide show
  1. package/accordion/accordion.scss +4 -1
  2. package/accordion/index.d.ts +36 -12
  3. package/alert/index.d.ts +8 -0
  4. package/breadcrumbs/index.d.ts +2 -0
  5. package/button-toggle/button-toggle-tokens.scss +14 -0
  6. package/button-toggle/button-toggle.scss +75 -3
  7. package/core/index.d.ts +33 -1
  8. package/core/styles/common/_select.scss +98 -3
  9. package/core/styles/typography/_typography.scss +8 -0
  10. package/ellipsis-center/index.d.ts +35 -2
  11. package/fesm2022/koobiq-components-accordion.mjs +143 -44
  12. package/fesm2022/koobiq-components-accordion.mjs.map +1 -1
  13. package/fesm2022/koobiq-components-alert.mjs +17 -1
  14. package/fesm2022/koobiq-components-alert.mjs.map +1 -1
  15. package/fesm2022/koobiq-components-autocomplete.mjs +2 -2
  16. package/fesm2022/koobiq-components-autocomplete.mjs.map +1 -1
  17. package/fesm2022/koobiq-components-breadcrumbs.mjs +8 -3
  18. package/fesm2022/koobiq-components-breadcrumbs.mjs.map +1 -1
  19. package/fesm2022/koobiq-components-button-toggle.mjs +2 -2
  20. package/fesm2022/koobiq-components-button-toggle.mjs.map +1 -1
  21. package/fesm2022/koobiq-components-code-block.mjs +10 -5
  22. package/fesm2022/koobiq-components-code-block.mjs.map +1 -1
  23. package/fesm2022/koobiq-components-core.mjs +43 -5
  24. package/fesm2022/koobiq-components-core.mjs.map +1 -1
  25. package/fesm2022/koobiq-components-dropdown.mjs +2 -2
  26. package/fesm2022/koobiq-components-dropdown.mjs.map +1 -1
  27. package/fesm2022/koobiq-components-ellipsis-center.mjs +77 -22
  28. package/fesm2022/koobiq-components-ellipsis-center.mjs.map +1 -1
  29. package/fesm2022/koobiq-components-file-upload.mjs +7 -7
  30. package/fesm2022/koobiq-components-file-upload.mjs.map +1 -1
  31. package/fesm2022/koobiq-components-filter-bar.mjs +76 -41
  32. package/fesm2022/koobiq-components-filter-bar.mjs.map +1 -1
  33. package/fesm2022/koobiq-components-list.mjs +3 -3
  34. package/fesm2022/koobiq-components-list.mjs.map +1 -1
  35. package/fesm2022/koobiq-components-markdown.mjs +6 -2
  36. package/fesm2022/koobiq-components-markdown.mjs.map +1 -1
  37. package/fesm2022/koobiq-components-modal.mjs +48 -7
  38. package/fesm2022/koobiq-components-modal.mjs.map +1 -1
  39. package/fesm2022/koobiq-components-select.mjs +98 -11
  40. package/fesm2022/koobiq-components-select.mjs.map +1 -1
  41. package/fesm2022/koobiq-components-sidebar.mjs +5 -1
  42. package/fesm2022/koobiq-components-sidebar.mjs.map +1 -1
  43. package/fesm2022/koobiq-components-sidepanel.mjs +25 -6
  44. package/fesm2022/koobiq-components-sidepanel.mjs.map +1 -1
  45. package/fesm2022/koobiq-components-tabs.mjs +7 -1
  46. package/fesm2022/koobiq-components-tabs.mjs.map +1 -1
  47. package/fesm2022/koobiq-components-time-range.mjs +388 -74
  48. package/fesm2022/koobiq-components-time-range.mjs.map +1 -1
  49. package/fesm2022/koobiq-components-timezone.mjs +2 -2
  50. package/fesm2022/koobiq-components-timezone.mjs.map +1 -1
  51. package/fesm2022/koobiq-components-title.mjs +15 -3
  52. package/fesm2022/koobiq-components-title.mjs.map +1 -1
  53. package/fesm2022/koobiq-components-tree-select.mjs +2 -2
  54. package/fesm2022/koobiq-components-tree-select.mjs.map +1 -1
  55. package/fesm2022/koobiq-components-tree.mjs +14 -4
  56. package/fesm2022/koobiq-components-tree.mjs.map +1 -1
  57. package/file-upload/file-upload.scss +11 -0
  58. package/file-upload/index.d.ts +1 -1
  59. package/file-upload/multiple-file-upload.component.scss +15 -3
  60. package/file-upload/single-file-upload.component.scss +10 -2
  61. package/filter-bar/filter-bar-tokens.scss +2 -1
  62. package/filter-bar/index.d.ts +25 -4
  63. package/filter-bar/pipes/base-pipe.scss +13 -6
  64. package/filter-bar/pipes/pipe-input.scss +3 -1
  65. package/link/_link-theme.scss +50 -15
  66. package/markdown/index.d.ts +2 -0
  67. package/modal/index.d.ts +16 -7
  68. package/navbar/_navbar-item_horizontal.scss +5 -0
  69. package/package.json +3 -3
  70. package/prebuilt-themes/dark-theme.css +1 -1
  71. package/prebuilt-themes/light-theme.css +1 -1
  72. package/prebuilt-themes/theme.css +1 -1
  73. package/schematics/migrations/alert-signals/index.js +4 -1
  74. package/schematics/migrations/autocomplete-panel-width-auto/index.js +3 -34
  75. package/schematics/migrations/empty-state-size-attr/index.js +3 -34
  76. package/schematics/migrations/filter-bar-signals/index.js +4 -1
  77. package/schematics/migrations/loader-overlay-size-attr/index.js +3 -34
  78. package/schematics/migrations/v20-upgrade/index.js +4 -1
  79. package/schematics/ng-add/index.js +1 -1
  80. package/schematics/utils/angular-parsing.js +11 -12
  81. package/schematics/utils/package-config.js +5 -4
  82. package/schematics/utils/typescript.js +24 -0
  83. package/select/_select-theme.scss +9 -1
  84. package/select/index.d.ts +12 -2
  85. package/select/select.scss +13 -0
  86. package/sidebar/index.d.ts +1 -0
  87. package/sidepanel/index.d.ts +9 -0
  88. package/tabs/index.d.ts +1 -0
  89. package/time-range/index.d.ts +140 -5
  90. package/time-range/time-range.scss +16 -1
  91. package/title/index.d.ts +10 -2
  92. package/tree/index.d.ts +1 -0
@@ -1,5 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Injectable, input, computed, viewChildren, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, booleanAttribute, output, signal, Directive, NgModule } from '@angular/core';
2
+ import { InjectionToken, Injectable, inject, signal, computed, ElementRef, input, viewChildren, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, booleanAttribute, output, Directive, NgModule } from '@angular/core';
3
+ import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
3
4
  import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
4
5
  import * as i1 from '@angular/forms';
5
6
  import { FormGroup, FormControl, ReactiveFormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS, NgControl } from '@angular/forms';
@@ -8,21 +9,20 @@ import { KbqButtonModule } from '@koobiq/components/button';
8
9
  import { DateAdapter, DateFormatter, ruRULocaleData, KBQ_LOCALE_SERVICE, PopUpSizes, PopUpPlacements } from '@koobiq/components/core';
9
10
  import * as i2$2 from '@koobiq/components/popover';
10
11
  import { KbqPopoverTrigger, KbqPopoverModule } from '@koobiq/components/popover';
11
- import { NgTemplateOutlet } from '@angular/common';
12
12
  import * as i2 from '@koobiq/components/datepicker';
13
13
  import { KbqDatepickerModule } from '@koobiq/components/datepicker';
14
14
  import * as i3 from '@koobiq/components/form-field';
15
- import { KbqFieldset, KbqFieldsetItem, KbqFormFieldControl } from '@koobiq/components/form-field';
15
+ import { KbqError, KbqFieldset, KbqFieldsetItem, KbqHint, KbqFormFieldControl } from '@koobiq/components/form-field';
16
16
  import * as i2$1 from '@koobiq/components/icon';
17
17
  import { KbqIcon, KbqIconModule } from '@koobiq/components/icon';
18
18
  import * as i5 from '@koobiq/components/radio';
19
19
  import { KbqRadioModule } from '@koobiq/components/radio';
20
20
  import * as i4 from '@koobiq/components/timepicker';
21
21
  import { KbqTimepicker, TimeFormats, KbqTimepickerModule } from '@koobiq/components/timepicker';
22
- import { merge, Subject } from 'rxjs';
23
22
  import { map, distinctUntilChanged } from 'rxjs/operators';
24
23
  import * as i1$1 from '@koobiq/components/link';
25
24
  import { KbqLinkModule } from '@koobiq/components/link';
25
+ import { Subject } from 'rxjs';
26
26
 
27
27
  /** Preset of selectable time ranges, provided by DI. */
28
28
  const KBQ_DEFAULT_TIME_RANGE_TYPES = new InjectionToken('KBQ_DEFAULT_TIME_RANGE_TYPES');
@@ -59,12 +59,39 @@ const rangeValidator = (timeRangeService) => {
59
59
  }
60
60
  const fromDateTime = timeRangeService.combineDateAndTime(form.fromDate, form.fromTime);
61
61
  const toDateTime = timeRangeService.combineDateAndTime(form.toDate, form.toTime);
62
- return timeRangeService.dateAdapter.compareDate(fromDateTime, toDateTime) > 0
62
+ return timeRangeService.dateAdapter.compareDateTime(fromDateTime, toDateTime) > 0
63
63
  ? { fromIsGreaterThanTo: fromDateTime }
64
64
  : null;
65
65
  };
66
66
  };
67
67
 
68
+ /**
69
+ * Channel between the popover footer, which `KbqTimeRange` owns, and the editor rendered inside the
70
+ * popover. Provided by `KbqTimeRange`, so there is exactly one instance per component.
71
+ *
72
+ * @docs-private
73
+ */
74
+ class KbqTimeRangeEditorBridge {
75
+ constructor() {
76
+ /**
77
+ * Raised while a pointer is held down on the footer. `focusout` on its own cannot tell "the user is
78
+ * pressing apply" apart from "the user clicked a blank spot": Safari does not move focus onto a
79
+ * `<button>` when it is clicked, so both arrive with a `null` `relatedTarget`.
80
+ */
81
+ this.applyGestureInProgress = false;
82
+ /**
83
+ * Registered by the editor. Reveals the errors of both borders and moves focus to the first field
84
+ * carrying one; returns whether the editor is free of errors.
85
+ */
86
+ this.revealErrors = () => true;
87
+ }
88
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.30", ngImport: i0, type: KbqTimeRangeEditorBridge, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
89
+ /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.30", ngImport: i0, type: KbqTimeRangeEditorBridge }); }
90
+ }
91
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.30", ngImport: i0, type: KbqTimeRangeEditorBridge, decorators: [{
92
+ type: Injectable
93
+ }] });
94
+
68
95
  class KbqTimeRangeService {
69
96
  constructor() {
70
97
  // Optional so the constructor can name the missing provider instead of letting DI throw a bare
@@ -74,6 +101,18 @@ class KbqTimeRangeService {
74
101
  this.providedDefaultTimeRangeTypes = inject(KBQ_DEFAULT_TIME_RANGE_TYPES, { optional: true }) || defaultTimeRangeTypes;
75
102
  this.customTimeRangeTypes = inject(KBQ_CUSTOM_TIME_RANGE_TYPES, { optional: true });
76
103
  this.DEFAULT_RANGE_TYPE = 'lastHour';
104
+ this.boundsSource = signal({
105
+ min: signal(null),
106
+ max: signal(null)
107
+ }, ...(ngDevMode ? [{ debugName: "boundsSource" }] : []));
108
+ /**
109
+ * Bounds of the owning component, so that every default and every check reads one pair. Both are
110
+ * exact instants rather than whole days: a `maxDate` that should admit its own day has to carry the
111
+ * end of that day.
112
+ */
113
+ this.minDate = computed(() => this.boundsSource().min() ?? null, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
114
+ /** @see minDate */
115
+ this.maxDate = computed(() => this.boundsSource().max() ?? null, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
77
116
  this.timeRangeConfig = {
78
117
  lastMinute: { units: { minutes: -1 }, translationType: 'minutes' },
79
118
  last5Minutes: { units: { minutes: -5 }, translationType: 'minutes' },
@@ -111,21 +150,39 @@ class KbqTimeRangeService {
111
150
  add({ type, ...customTimeRangeConfig }) {
112
151
  this.timeRangeConfig[type] = customTimeRangeConfig;
113
152
  }
153
+ /**
154
+ * Hands the owning component's own bound inputs over, rather than a copy of their values: a signal
155
+ * input reads as its initial value until the first change detection, so a mirror would leave every
156
+ * default computed in a constructor clamped against no bounds at all.
157
+ */
158
+ bindBounds(min, max) {
159
+ this.boundsSource.set({ min, max });
160
+ }
114
161
  getTimeRangeTypeUnits(type) {
115
162
  return this.timeRangeConfig[type].units;
116
163
  }
117
164
  getTimeRangeUnitByType(type) {
118
165
  return this.timeRangeConfig[type].translationType;
119
166
  }
167
+ /**
168
+ * "Yesterday to today", pulled inside {@link minDate}/{@link maxDate} when they are set - otherwise
169
+ * the editor opens on a range it would itself report as out of bounds.
170
+ *
171
+ * Milliseconds are dropped from "today" *before* clamping, so a clamped end lands exactly on its
172
+ * bound rather than a fraction of a second below it.
173
+ */
120
174
  getDefaultRangeValue() {
121
- const from = this.dateAdapter.addCalendarUnits(this.dateAdapter.today(), { days: -1 });
122
- const to = this.dateAdapter.today();
123
- return {
124
- fromTime: this.omitMilliseconds(from),
125
- fromDate: this.omitMilliseconds(from),
126
- toTime: this.omitMilliseconds(to),
127
- toDate: this.omitMilliseconds(to)
128
- };
175
+ const today = this.omitMilliseconds(this.dateAdapter.today());
176
+ // `from` is derived from the clamped `to`, so the one-day window survives bounds that lie
177
+ // entirely in the past - clamping both ends independently would collapse it onto `maxDate`.
178
+ const to = this.clampToBounds(today);
179
+ const from = this.clampToBounds(this.dateAdapter.addCalendarUnits(to, { days: -1 }));
180
+ // Bounds lying entirely in the future leave no room below `to`, and both ends land on `minDate`.
181
+ // The window is opened upwards instead, which is the same day in the other direction.
182
+ const end = this.dateAdapter.compareDateTime(from, to) === 0
183
+ ? this.clampToBounds(this.dateAdapter.addCalendarUnits(from, { days: 1 }))
184
+ : to;
185
+ return { fromTime: from, fromDate: from, toTime: end, toDate: end };
129
186
  }
130
187
  getTimeRangeDefaultValue(rangeValue, availableTimeRangeTypes = []) {
131
188
  const defaultType = availableTimeRangeTypes[0] ?? this.providedDefaultTimeRangeTypes[0] ?? this.DEFAULT_RANGE_TYPE;
@@ -182,6 +239,30 @@ class KbqTimeRangeService {
182
239
  }
183
240
  return result;
184
241
  }
242
+ /** Which bound, if any, an instant falls outside of. */
243
+ checkBounds(dateTime) {
244
+ const minDate = this.minDate();
245
+ const maxDate = this.maxDate();
246
+ if (minDate && this.dateAdapter.compareDateTime(dateTime, minDate) < 0)
247
+ return 'min';
248
+ if (maxDate && this.dateAdapter.compareDateTime(dateTime, maxDate) > 0)
249
+ return 'max';
250
+ return null;
251
+ }
252
+ /**
253
+ * `DateAdapter.clampDate` compares whole days only, while a border is rejected by its time as well -
254
+ * a value clamped to the day would still land outside the bounds.
255
+ */
256
+ clampToBounds(date) {
257
+ switch (this.checkBounds(date)) {
258
+ case 'min':
259
+ return this.minDate();
260
+ case 'max':
261
+ return this.maxDate();
262
+ default:
263
+ return date;
264
+ }
265
+ }
185
266
  omitMilliseconds(date) {
186
267
  return this.dateAdapter.createDateTime(this.dateAdapter.getYear(date), this.dateAdapter.getMonth(date), this.dateAdapter.getDate(date), this.dateAdapter.getHours(date), this.dateAdapter.getMinutes(date), this.dateAdapter.getSeconds(date), 0);
187
268
  }
@@ -192,15 +273,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.30", ngImpo
192
273
  type: Injectable
193
274
  }], ctorParameters: () => [] });
194
275
 
276
+ const borders = ['from', 'to'];
277
+ const halves = ['Time', 'Date'];
278
+ /**
279
+ * Errors are reported only once the user has left the border they belong to, and go quiet again as soon
280
+ * as typing resumes. The validators themselves always run, so that `validate()` stays honest about
281
+ * whether the range could be saved - only the painting waits.
282
+ */
195
283
  class RangeErrorStateMatcher {
196
- isErrorState(control, form) {
197
- return !!form?.invalid || !!control?.invalid;
284
+ isErrorState(control) {
285
+ return !!control?.invalid && !!control.touched;
198
286
  }
199
287
  }
200
288
  /** @docs-private */
201
289
  class KbqTimeRangeEditor {
202
290
  constructor() {
203
291
  this.timeRangeService = inject(KbqTimeRangeService);
292
+ this.editorBridge = inject(KbqTimeRangeEditorBridge, { optional: true });
293
+ this.elementRef = inject(ElementRef);
294
+ this.document = inject(DOCUMENT);
204
295
  /** The maximum selectable date. */
205
296
  this.maxDate = input(null, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
206
297
  /** The minimum selectable date. */
@@ -227,13 +318,31 @@ class KbqTimeRangeEditor {
227
318
  formattedValue: this.getFormattedOption(type, localeConfig)
228
319
  }));
229
320
  }, ...(ngDevMode ? [{ debugName: "timeRangeTypesWithoutRange" }] : []));
321
+ /**
322
+ * Caption under the "range" option, spelling out the bounds the border has to stay within. Read from
323
+ * the service rather than from this component's own inputs, so that it can never disagree with what
324
+ * the border validators go on to enforce.
325
+ * @docs-private
326
+ */
327
+ this.boundsHint = computed(() => {
328
+ const { dateFormatter } = this.timeRangeService;
329
+ const minDate = this.timeRangeService.minDate();
330
+ const maxDate = this.timeRangeService.maxDate();
331
+ if (!minDate && !maxDate)
332
+ return '';
333
+ // The time half earns a place in the caption only once a bound actually carries one.
334
+ return [minDate, maxDate].some((bound) => bound && this.carriesTime(bound))
335
+ ? dateFormatter.rangeShortDateTime(minDate, maxDate ?? undefined)
336
+ : dateFormatter.rangeShortDate(minDate, maxDate ?? undefined);
337
+ }, ...(ngDevMode ? [{ debugName: "boundsHint" }] : []));
230
338
  /** @docs-private */
231
339
  this.timepickerList = viewChildren(KbqTimepicker, ...(ngDevMode ? [{ debugName: "timepickerList" }] : []));
232
340
  /** @docs-private */
233
341
  this.timepickerFormat = TimeFormats.HHmmss;
234
342
  /** @docs-private */
235
343
  this.rangeStateMatcher = new RangeErrorStateMatcher();
236
- this.lastValidationErrorOnEmit = null;
344
+ /** Raised by the first "apply", after which a border left wholly empty counts as an error too. */
345
+ this.applyAttempted = false;
237
346
  /** @docs-private */
238
347
  this.onChange = (_value) => { };
239
348
  /** @docs-private */
@@ -247,21 +356,21 @@ class KbqTimeRangeEditor {
247
356
  fromDate: new FormControl(defaultRangeValue.fromDate, { nonNullable: true }),
248
357
  toTime: new FormControl(defaultRangeValue.toTime, { nonNullable: true }),
249
358
  toDate: new FormControl(defaultRangeValue.toDate, { nonNullable: true })
250
- }, { validators: rangeValidator(this.timeRangeService) });
251
- const rangeControls = [
252
- this.form.controls.fromTime,
253
- this.form.controls.fromDate,
254
- this.form.controls.toTime,
255
- this.form.controls.toDate
256
- ];
257
- merge(...rangeControls.map((control) => control.statusChanges))
258
- .pipe(takeUntilDestroyed())
259
- .subscribe(() => (this.lastValidationErrorOnEmit = this.concatControlValidationErrors()));
359
+ });
360
+ const { fromTime, fromDate, toTime, toDate } = this.form.controls;
361
+ this.borderControls = {
362
+ from: { Time: fromTime, Date: fromDate },
363
+ to: { Time: toTime, Date: toDate }
364
+ };
365
+ this.rangeControls = borders.flatMap((border) => this.borderHalves(border));
366
+ borders.forEach((border) => halves.forEach((half) => this.borderControls[border][half].addValidators(this.borderValidator(border, half))));
367
+ if (this.editorBridge)
368
+ this.editorBridge.revealErrors = () => this.revealErrors();
260
369
  this.form.valueChanges
261
370
  .pipe(map((formValue) => formValue.type), distinctUntilChanged(), takeUntilDestroyed())
262
371
  .subscribe((type) => {
263
372
  const isDisabled = type !== 'range';
264
- rangeControls.forEach((control) => {
373
+ this.rangeControls.forEach((control) => {
265
374
  if (isDisabled) {
266
375
  control.disable({ emitEvent: false });
267
376
  }
@@ -271,16 +380,13 @@ class KbqTimeRangeEditor {
271
380
  });
272
381
  });
273
382
  this.form.valueChanges.pipe(takeUntilDestroyed()).subscribe((formValue) => {
383
+ // Each half is checked against its sibling, so editing one has to re-run the other. Silent,
384
+ // or this would re-enter through `valueChanges`.
385
+ this.revalidateRange();
274
386
  const range = this.mapTimeRange(formValue);
275
387
  if (range)
276
388
  this.onChange(range);
277
389
  });
278
- this.form.statusChanges.pipe(takeUntilDestroyed()).subscribe((status) => {
279
- const timepickerList = this.timepickerList();
280
- if (timepickerList.at(0)) {
281
- timepickerList.at(0).errorState = status === 'INVALID';
282
- }
283
- });
284
390
  }
285
391
  ngOnInit() {
286
392
  const defaultRangeValue = this.rangeValue();
@@ -295,7 +401,7 @@ class KbqTimeRangeEditor {
295
401
  }
296
402
  /** @docs-private */
297
403
  validate() {
298
- return this.form.errors || this.lastValidationErrorOnEmit;
404
+ return this.concatControlValidationErrors();
299
405
  }
300
406
  /** Implemented as part of ControlValueAccessor */
301
407
  writeValue(value) {
@@ -304,8 +410,10 @@ class KbqTimeRangeEditor {
304
410
  const corrected = this.timeRangeService.checkAndCorrectTimeRangeValue(value, this.availableTimeRangeTypes(), this.form.value);
305
411
  this.form.controls.type.setValue(corrected.type);
306
412
  if (corrected.type === 'range' && corrected.startDateTime && corrected.endDateTime) {
307
- const from = this.timeRangeService.dateAdapter.deserialize(corrected.startDateTime);
308
- const to = this.timeRangeService.dateAdapter.deserialize(corrected.endDateTime);
413
+ const { dateAdapter } = this.timeRangeService;
414
+ // An unparseable end leaves its halves empty, which the border validators then report.
415
+ const from = dateAdapter.deserialize(corrected.startDateTime);
416
+ const to = dateAdapter.deserialize(corrected.endDateTime);
309
417
  this.form.patchValue({
310
418
  fromTime: from,
311
419
  fromDate: from,
@@ -322,19 +430,200 @@ class KbqTimeRangeEditor {
322
430
  registerOnTouched(fn) {
323
431
  this.onTouch = fn;
324
432
  }
433
+ /**
434
+ * Leaving one border reports what is wrong with it. Angular marks a control touched on its own blur,
435
+ * which is a finer grain than the spec asks for - so moving between a border's own date and time
436
+ * takes the touch back off, and the pair is judged only once focus is out of both.
437
+ * @docs-private
438
+ */
439
+ onBorderFocusOut(border, { currentTarget, relatedTarget }) {
440
+ this.setBorderTouched(border, !this.staysWithin(currentTarget, relatedTarget));
441
+ }
442
+ /**
443
+ * Typing puts a border back to its neutral state, the way `validation-on-blur` does for a single
444
+ * field. What is wrong with it is said again when focus leaves it.
445
+ * @docs-private
446
+ */
447
+ onBorderInput(border) {
448
+ this.setBorderTouched(border, false);
449
+ }
450
+ /**
451
+ * What is said when the range as a whole has left the bounds. Said once, below the last of the two
452
+ * ends, and only when both of them are at fault: a single end out of bounds is already pointed at by
453
+ * the fields painted around it. Silent until the ends have been left, keeping step with that
454
+ * painting.
455
+ * @docs-private
456
+ */
457
+ outOfBoundsMessage() {
458
+ const bothOutOfBounds = borders.every((border) => this.borderHalves(border).some((control) => control.touched && !!control.errors?.kbqTimeRangeOutOfBounds));
459
+ if (!bothOutOfBounds)
460
+ return '';
461
+ const template = this.localeConfiguration().editor.outOfBoundsError ??
462
+ ruRULocaleData.timeRange.editor.outOfBoundsError ??
463
+ '';
464
+ return template.replace('{{ value }}', this.boundsHint());
465
+ }
466
+ /**
467
+ * Once focus leaves the manual range fields, a reversed range is silently swapped back into order -
468
+ * except on the way to "apply", where the swap is left to the emitted value so that the popover does
469
+ * not visibly reorder itself in the same gesture that closes it.
470
+ * @docs-private
471
+ */
472
+ onRangeFocusOut({ currentTarget, relatedTarget }) {
473
+ const next = relatedTarget;
474
+ if (this.staysWithin(currentTarget, next))
475
+ return;
476
+ // Safari reports a `null` `relatedTarget` for a click on a button, so the footer is recognised by
477
+ // the pointer gesture it started rather than by where focus went.
478
+ if (this.editorBridge?.applyGestureInProgress || next?.closest('.kbq-time-range__buttons'))
479
+ return;
480
+ // Focus went nowhere because the whole window lost it - the user is still mid-edit.
481
+ if (!next && !this.document.hasFocus())
482
+ return;
483
+ this.swapIfReversed();
484
+ }
485
+ /**
486
+ * Reveals both borders and sends the user to the first field at fault, reporting whether there is
487
+ * anything to fix at all.
488
+ */
489
+ revealErrors() {
490
+ this.applyAttempted = true;
491
+ // `applyAttempted` is read by the validators, so they have to be run again before anything is said
492
+ // about the result.
493
+ this.revalidateRange();
494
+ borders.forEach((border) => this.setBorderTouched(border, true));
495
+ const firstInvalid = borders
496
+ .flatMap((border) => halves.map((half) => ({ border, half })))
497
+ .find(({ border, half }) => this.borderControls[border][half].invalid);
498
+ if (!firstInvalid)
499
+ return true;
500
+ // The only place the two halves of a name are put back together, and it builds a selector rather
501
+ // than a control key: the attribute in the template is spelled the same way.
502
+ this.elementRef.nativeElement
503
+ .querySelector(`[data-time-range-field="${firstInvalid.border}${firstInvalid.half}"]`)
504
+ ?.focus();
505
+ return false;
506
+ }
507
+ /** Whether a bound is a moment within its day rather than the day itself. */
508
+ carriesTime(date) {
509
+ const { dateAdapter } = this.timeRangeService;
510
+ return !!(dateAdapter.getHours(date) || dateAdapter.getMinutes(date) || dateAdapter.getSeconds(date));
511
+ }
512
+ /** Whether focus is still inside `group`, counting the datepicker calendar as part of it. */
513
+ staysWithin(group, next) {
514
+ // The calendar is rendered in a separate overlay, so focus moving into it is still editing.
515
+ return group.contains(next) || !!next?.closest('.kbq-datepicker__popup');
516
+ }
517
+ /** Both halves of a border are painted, or neither: the pair is what the user is judged on. */
518
+ setBorderTouched(border, touched) {
519
+ this.borderHalves(border).forEach((control) => {
520
+ if (touched) {
521
+ control.markAsTouched();
522
+ }
523
+ else {
524
+ control.markAsUntouched();
525
+ }
526
+ });
527
+ }
528
+ /** Both halves of one end, in the order they are painted. */
529
+ borderHalves(border) {
530
+ return halves.map((half) => this.borderControls[border][half]);
531
+ }
532
+ /** Each half is checked against its sibling, so a change to one has to re-run the other. */
533
+ revalidateRange() {
534
+ this.rangeControls.forEach((control) => control.updateValueAndValidity({ emitEvent: false }));
535
+ }
536
+ swapIfReversed() {
537
+ if (this.hasBorderProblem())
538
+ return;
539
+ const { fromTime, fromDate, toTime, toDate } = this.form.getRawValue();
540
+ if (!this.isReversed(fromDate, fromTime, toDate, toTime))
541
+ return;
542
+ this.form.patchValue({ fromTime: toTime, fromDate: toDate, toTime: fromTime, toDate: fromDate });
543
+ }
544
+ /**
545
+ * Whether anything about the borders would stop the range being saved. Deliberately blind to which
546
+ * borders have been revealed: a swap rewrites the user's values, so it has to answer "is this range
547
+ * actually sound", not "has the user been shown the problem yet".
548
+ */
549
+ hasBorderProblem() {
550
+ if (this.form.controls.type.value !== 'range')
551
+ return false;
552
+ // The validators run whether or not the user has been shown the result, so the controls already
553
+ // hold the answer - including the pickers' own format errors.
554
+ return this.rangeControls.some((control) => !!control.errors);
555
+ }
556
+ /**
557
+ * What is wrong with each half of a border, whether or not the user has been shown it. A missing half
558
+ * is flagged on its own; a bound is judged in two steps, so that the field actually at fault is the
559
+ * one painted.
560
+ */
561
+ borderErrors(border) {
562
+ const { value: date } = this.borderControls[border].Date;
563
+ const { value: time } = this.borderControls[border].Time;
564
+ if (!date && !time) {
565
+ const error = this.applyAttempted ? { kbqTimeRangeRequired: true } : null;
566
+ return { Date: error, Time: error };
567
+ }
568
+ // Only the missing half is painted, so the user is pointed at the field to fill in.
569
+ if (!date || !time) {
570
+ const error = { kbqTimeRangeIncomplete: true };
571
+ return { Date: date ? null : error, Time: time ? null : error };
572
+ }
573
+ const outOfBounds = this.checkBorderBounds(date, time);
574
+ if (!outOfBounds)
575
+ return { Date: null, Time: null };
576
+ const error = { kbqTimeRangeOutOfBounds: { bound: outOfBounds.bound } };
577
+ // A day outside the bounds is wrong whatever time sits next to it, so both halves are painted.
578
+ // A day on the boundary itself can only be pushed out by its time, so only the time is.
579
+ return { Date: outOfBounds.scope === 'date' ? error : null, Time: error };
580
+ }
581
+ /** Which bound a border falls outside of, and whether its day or only its time is to blame. */
582
+ checkBorderBounds(date, time) {
583
+ const service = this.timeRangeService;
584
+ const { dateAdapter } = service;
585
+ const minDate = service.minDate();
586
+ const maxDate = service.maxDate();
587
+ if (minDate && dateAdapter.compareDate(date, minDate) < 0)
588
+ return { bound: 'min', scope: 'date' };
589
+ if (maxDate && dateAdapter.compareDate(date, maxDate) > 0)
590
+ return { bound: 'max', scope: 'date' };
591
+ const combined = service.combineDateAndTime(date, time);
592
+ if (minDate && dateAdapter.compareDateTime(combined, minDate) < 0)
593
+ return { bound: 'min', scope: 'time' };
594
+ if (maxDate && dateAdapter.compareDateTime(combined, maxDate) > 0)
595
+ return { bound: 'max', scope: 'time' };
596
+ return null;
597
+ }
598
+ isReversed(fromDate, fromTime, toDate, toTime) {
599
+ if (!fromDate || !fromTime || !toDate || !toTime)
600
+ return false;
601
+ const service = this.timeRangeService;
602
+ return (service.dateAdapter.compareDateTime(service.combineDateAndTime(fromDate, fromTime), service.combineDateAndTime(toDate, toTime)) > 0);
603
+ }
604
+ /**
605
+ * A border reports only what the pickers do not: an empty half next to a filled one, a border left
606
+ * wholly empty once "apply" was pressed, and a border outside the bounds. Format is the pickers' own
607
+ * business, and so is the calendar restriction.
608
+ */
609
+ borderValidator(border, half) {
610
+ return () => {
611
+ if (this.form.controls.type.value !== 'range')
612
+ return null;
613
+ return this.borderErrors(border)[half];
614
+ };
615
+ }
325
616
  mapTimeRange({ type }) {
326
617
  if (!type)
327
618
  return;
328
- return {
329
- type,
330
- ...this.timeRangeService.calculateTimeRange(type, {
331
- // use control.value, since via form.value control values can be undefined
332
- fromTime: this.form.controls.fromTime.value,
333
- fromDate: this.form.controls.fromDate.value,
334
- toDate: this.form.controls.toTime.value,
335
- toTime: this.form.controls.toTime.value
336
- })
337
- };
619
+ const { fromTime, fromDate, toTime, toDate } = this.form.getRawValue();
620
+ // Ordered here as well as on blur, so that pressing "apply" straight out of a reversed field is
621
+ // safe. A range with something wrong in it is left exactly as typed: it cannot be saved anyway,
622
+ // and reordering it would move the fields out from under the user while they fix it.
623
+ const ordered = !this.hasBorderProblem() && this.isReversed(fromDate, fromTime, toDate, toTime)
624
+ ? { fromTime: toTime, fromDate: toDate, toTime: fromTime, toDate: fromDate }
625
+ : { fromTime, fromDate, toTime, toDate };
626
+ return { type, ...this.timeRangeService.calculateTimeRange(type, ordered) };
338
627
  }
339
628
  getFormattedOption(type, localeConfig) {
340
629
  const translationType = this.timeRangeService.getTimeRangeUnitByType(type);
@@ -344,7 +633,7 @@ class KbqTimeRangeEditor {
344
633
  return this.timeRangeService.dateFormatter.duration(this.timeRangeService.dateAdapter.deserialize(range.startDateTime), this.timeRangeService.dateAdapter.today(), [translationType], false, localeConfig.durationTemplate.option);
345
634
  }
346
635
  concatControlValidationErrors() {
347
- let result = this.form.errors;
636
+ let result = null;
348
637
  Object.values(this.form.controls).forEach((control) => {
349
638
  if (control.errors) {
350
639
  result = {
@@ -367,15 +656,17 @@ class KbqTimeRangeEditor {
367
656
  provide: NG_VALIDATORS,
368
657
  useExisting: KbqTimeRangeEditor
369
658
  }
370
- ], viewQueries: [{ propertyName: "timepickerList", predicate: KbqTimepicker, descendants: true, isSignal: true }], ngImport: i0, template: "<form [formGroup]=\"form\">\n <kbq-radio-group class=\"kbq-time-range-editor__container\" [formControl]=\"form.controls.type\">\n @let types = timeRangeTypesWithoutRange();\n\n @if (types.length) {\n <div class=\"kbq-time-range-editor__predefined\">\n @for (type of types; track type) {\n <kbq-radio-button class=\"kbq-time-range-editor__predefined-option\" [value]=\"type.type\">\n <ng-container\n *ngTemplateOutlet=\"optionTemplate() || defaultOptionTemplate; context: { $implicit: type }\"\n />\n </kbq-radio-button>\n }\n </div>\n }\n\n @if (isRangeVisible()) {\n @let localeConfig = localeConfiguration();\n\n <div class=\"kbq-time-range-editor__range\" role=\"group\">\n @if (types.length) {\n <kbq-radio-button [value]=\"'range'\">{{ localeConfig.editor.rangeLabel }}</kbq-radio-button>\n }\n\n <div class=\"kbq-time-range-editor__date-time\">\n <span class=\"kbq-time-range-editor__date-time-prefix\" [attr.aria-label]=\"localeConfig.editor.from\">\n {{ localeConfig.editor.from }}\n </span>\n <kbq-fieldset>\n <kbq-form-field kbqFieldsetItem>\n <i kbq-icon=\"kbq-clock_16\" kbqPrefix [autoColor]=\"true\"></i>\n <input kbqTimepicker [format]=\"timepickerFormat\" [formControl]=\"form.controls.fromTime\" />\n </kbq-form-field>\n\n <kbq-form-field>\n <input\n [min]=\"minDate()\"\n [max]=\"maxDate()\"\n [kbqDatepicker]=\"datepickerFrom\"\n [formControl]=\"form.controls.fromDate\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n <kbq-datepicker-toggle-icon kbqSuffix [for]=\"datepickerFrom\" />\n <kbq-datepicker #datepickerFrom [minDate]=\"minDate()\" [maxDate]=\"maxDate()\" />\n </kbq-form-field>\n </kbq-fieldset>\n </div>\n\n <div class=\"kbq-time-range-editor__date-time\">\n <span class=\"kbq-time-range-editor__date-time-prefix\" [attr.aria-label]=\"localeConfig.editor.to\">\n {{ localeConfig.editor.to }}\n </span>\n <kbq-fieldset>\n <kbq-form-field kbqFieldsetItem>\n <i kbq-icon=\"kbq-clock_16\" kbqPrefix [autoColor]=\"true\"></i>\n <input kbqTimepicker [format]=\"timepickerFormat\" [formControl]=\"form.controls.toTime\" />\n </kbq-form-field>\n\n <kbq-form-field>\n <input\n [min]=\"minDate()\"\n [max]=\"maxDate()\"\n [kbqDatepicker]=\"datepickerTo\"\n [formControl]=\"form.controls.toDate\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n <kbq-datepicker-toggle-icon kbqSuffix [for]=\"datepickerTo\" />\n <kbq-datepicker #datepickerTo [minDate]=\"minDate()\" [maxDate]=\"maxDate()\" />\n </kbq-form-field>\n </kbq-fieldset>\n </div>\n </div>\n }\n </kbq-radio-group>\n</form>\n\n<ng-template #defaultOptionTemplate let-context>\n @let localeConfig = localeConfiguration();\n @switch (context.translationType) {\n @case ('other') {\n @switch (context.type) {\n @case ('allTime') {\n {{ localeConfig.editor.allTimeOption ?? localeConfig.editor.allTime }}\n }\n @case ('currentQuarter') {\n {{ localeConfig.editor.currentQuarterOption ?? localeConfig.editor.currentQuarter }}\n }\n @case ('currentYear') {\n {{ localeConfig.editor.currentYearOption ?? localeConfig.editor.currentYear }}\n }\n }\n }\n @default {\n {{ context.formattedValue }}\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: KbqFieldset, selector: "kbq-fieldset" }, { kind: "directive", type: KbqFieldsetItem, selector: "[kbqFieldsetItem]" }, { kind: "ngmodule", type: KbqDatepickerModule }, { kind: "component", type: i2.KbqDatepicker, selector: "kbq-datepicker", inputs: ["hasBackdrop", "startAt", "disabled", "opened", "minDate", "maxDate", "panelClass", "dateClass", "backdropClass"], outputs: ["yearSelected", "monthSelected", "opened", "closed"], exportAs: ["kbqDatepicker"] }, { kind: "directive", type: i2.KbqDatepickerInput, selector: "input[kbqDatepicker], input[kbqCalendar]", inputs: ["errorStateMatcher", "placeholder", "required", "kbqDatepicker", "kbqCalendar", "kbqDatepickerFilter", "value", "min", "max", "disabled", "id", "kbqValidationTooltip"], outputs: ["incorrectInput", "dateChange", "dateInput"], exportAs: ["kbqDatepickerInput"] }, { kind: "component", type: i2.KbqDatepickerToggleIconComponent, selector: "kbq-datepicker-toggle-icon", inputs: ["disabled", "for"] }, { kind: "component", type: i3.KbqFormField, selector: "kbq-form-field", inputs: ["noBorders", "inOverlay", "horizontal", "labelClass", "contentClass"], outputs: ["inOverlayChange"], exportAs: ["kbqFormField"] }, { kind: "directive", type: i3.KbqPrefix, selector: "[kbqPrefix]", exportAs: ["kbqPrefix"] }, { kind: "directive", type: i3.KbqSuffix, selector: "[kbqSuffix]" }, { kind: "ngmodule", type: KbqTimepickerModule }, { kind: "directive", type: i4.KbqTimepicker, selector: "input[kbqTimepicker]", inputs: ["errorStateMatcher", "placeholder", "disabled", "id", "required", "format", "min", "max", "value", "kbqValidationTooltip"], outputs: ["incorrectInput"], exportAs: ["kbqTimepicker"] }, { kind: "ngmodule", type: KbqRadioModule }, { kind: "directive", type: i5.KbqRadioGroup, selector: "kbq-radio-group", inputs: ["big", "name", "labelPosition", "value", "selected", "disabled", "required"], outputs: ["change"], exportAs: ["kbqRadioGroup"] }, { kind: "component", type: i5.KbqRadioButton, selector: "kbq-radio-button", inputs: ["checked", "value", "disabled", "tabIndex", "required", "labelPosition", "name", "isFocused", "id"], outputs: ["change"], exportAs: ["kbqRadioButton"] }, { kind: "component", type: KbqIcon, selector: "[kbq-icon]", inputs: ["small", "autoColor", "kbq-icon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
659
+ ], viewQueries: [{ propertyName: "timepickerList", predicate: KbqTimepicker, descendants: true, isSignal: true }], ngImport: i0, template: "<form [formGroup]=\"form\">\n <kbq-radio-group class=\"kbq-time-range-editor__container\" [formControl]=\"form.controls.type\">\n @let types = timeRangeTypesWithoutRange();\n\n @if (types.length) {\n <div class=\"kbq-time-range-editor__predefined\">\n @for (type of types; track type) {\n <kbq-radio-button class=\"kbq-time-range-editor__predefined-option\" [value]=\"type.type\">\n <ng-container\n *ngTemplateOutlet=\"optionTemplate() || defaultOptionTemplate; context: { $implicit: type }\"\n />\n </kbq-radio-button>\n }\n </div>\n }\n\n @if (isRangeVisible()) {\n @let localeConfig = localeConfiguration();\n\n <div class=\"kbq-time-range-editor__range\" role=\"group\" (focusout)=\"onRangeFocusOut($event)\">\n @let bounds = boundsHint();\n\n @if (types.length) {\n <kbq-radio-button [value]=\"'range'\">\n {{ localeConfig.editor.rangeLabel }}\n @if (bounds) {\n <kbq-hint>{{ bounds }}</kbq-hint>\n }\n </kbq-radio-button>\n }\n\n <div\n class=\"kbq-time-range-editor__date-time\"\n (input)=\"onBorderInput('from')\"\n (focusout)=\"onBorderFocusOut('from', $event)\"\n >\n <span class=\"kbq-time-range-editor__date-time-prefix\" [attr.aria-label]=\"localeConfig.editor.from\">\n {{ localeConfig.editor.from }}\n </span>\n <kbq-fieldset>\n <kbq-form-field kbqFieldsetItem>\n <i kbq-icon=\"kbq-clock_16\" kbqPrefix [autoColor]=\"true\"></i>\n <input\n kbqTimepicker\n data-time-range-field=\"fromTime\"\n [format]=\"timepickerFormat\"\n [formControl]=\"form.controls.fromTime\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n </kbq-form-field>\n\n <kbq-form-field>\n <input\n data-time-range-field=\"fromDate\"\n [kbqDatepicker]=\"datepickerFrom\"\n [formControl]=\"form.controls.fromDate\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n <kbq-datepicker-toggle-icon kbqSuffix [for]=\"datepickerFrom\" />\n <kbq-datepicker #datepickerFrom [minDate]=\"minDate()\" [maxDate]=\"maxDate()\" />\n </kbq-form-field>\n </kbq-fieldset>\n </div>\n\n <div\n class=\"kbq-time-range-editor__date-time\"\n (input)=\"onBorderInput('to')\"\n (focusout)=\"onBorderFocusOut('to', $event)\"\n >\n <span class=\"kbq-time-range-editor__date-time-prefix\" [attr.aria-label]=\"localeConfig.editor.to\">\n {{ localeConfig.editor.to }}\n </span>\n <kbq-fieldset>\n <kbq-form-field kbqFieldsetItem>\n <i kbq-icon=\"kbq-clock_16\" kbqPrefix [autoColor]=\"true\"></i>\n <input\n kbqTimepicker\n data-time-range-field=\"toTime\"\n [format]=\"timepickerFormat\"\n [formControl]=\"form.controls.toTime\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n </kbq-form-field>\n\n <kbq-form-field>\n <input\n data-time-range-field=\"toDate\"\n [kbqDatepicker]=\"datepickerTo\"\n [formControl]=\"form.controls.toDate\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n <kbq-datepicker-toggle-icon kbqSuffix [for]=\"datepickerTo\" />\n <kbq-datepicker #datepickerTo [minDate]=\"minDate()\" [maxDate]=\"maxDate()\" />\n </kbq-form-field>\n\n <!-- The range is judged as a whole, so what is wrong with it is said once, below the\n last of its two ends - in place of the caption, which spells the same bounds out. -->\n @if (outOfBoundsMessage(); as message) {\n <kbq-error>{{ message }}</kbq-error>\n } @else if (!types.length && bounds) {\n <!-- With no presets there is no option to hang the bounds off, so they go in the\n fieldset's own hint area rather than loose in the block. -->\n <kbq-hint>{{ bounds }}</kbq-hint>\n }\n </kbq-fieldset>\n </div>\n </div>\n }\n </kbq-radio-group>\n</form>\n\n<ng-template #defaultOptionTemplate let-context>\n @let localeConfig = localeConfiguration();\n @switch (context.translationType) {\n @case ('other') {\n @switch (context.type) {\n @case ('allTime') {\n {{ localeConfig.editor.allTimeOption ?? localeConfig.editor.allTime }}\n }\n @case ('currentQuarter') {\n {{ localeConfig.editor.currentQuarterOption ?? localeConfig.editor.currentQuarter }}\n }\n @case ('currentYear') {\n {{ localeConfig.editor.currentYearOption ?? localeConfig.editor.currentYear }}\n }\n }\n }\n @default {\n {{ context.formattedValue }}\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: KbqError, selector: "kbq-error", exportAs: ["kbqError"] }, { kind: "component", type: KbqFieldset, selector: "kbq-fieldset" }, { kind: "directive", type: KbqFieldsetItem, selector: "[kbqFieldsetItem]" }, { kind: "component", type: KbqHint, selector: "kbq-hint", inputs: ["id", "fillTextOff", "compact"], exportAs: ["kbqHint"] }, { kind: "ngmodule", type: KbqDatepickerModule }, { kind: "component", type: i2.KbqDatepicker, selector: "kbq-datepicker", inputs: ["hasBackdrop", "startAt", "disabled", "opened", "minDate", "maxDate", "panelClass", "dateClass", "backdropClass"], outputs: ["yearSelected", "monthSelected", "opened", "closed"], exportAs: ["kbqDatepicker"] }, { kind: "directive", type: i2.KbqDatepickerInput, selector: "input[kbqDatepicker], input[kbqCalendar]", inputs: ["errorStateMatcher", "placeholder", "required", "kbqDatepicker", "kbqCalendar", "kbqDatepickerFilter", "value", "min", "max", "disabled", "id", "kbqValidationTooltip"], outputs: ["incorrectInput", "dateChange", "dateInput"], exportAs: ["kbqDatepickerInput"] }, { kind: "component", type: i2.KbqDatepickerToggleIconComponent, selector: "kbq-datepicker-toggle-icon", inputs: ["disabled", "for"] }, { kind: "component", type: i3.KbqFormField, selector: "kbq-form-field", inputs: ["noBorders", "inOverlay", "horizontal", "labelClass", "contentClass"], outputs: ["inOverlayChange"], exportAs: ["kbqFormField"] }, { kind: "directive", type: i3.KbqPrefix, selector: "[kbqPrefix]", exportAs: ["kbqPrefix"] }, { kind: "directive", type: i3.KbqSuffix, selector: "[kbqSuffix]" }, { kind: "ngmodule", type: KbqTimepickerModule }, { kind: "directive", type: i4.KbqTimepicker, selector: "input[kbqTimepicker]", inputs: ["errorStateMatcher", "placeholder", "disabled", "id", "required", "format", "min", "max", "value", "kbqValidationTooltip"], outputs: ["incorrectInput"], exportAs: ["kbqTimepicker"] }, { kind: "ngmodule", type: KbqRadioModule }, { kind: "directive", type: i5.KbqRadioGroup, selector: "kbq-radio-group", inputs: ["big", "name", "labelPosition", "value", "selected", "disabled", "required"], outputs: ["change"], exportAs: ["kbqRadioGroup"] }, { kind: "component", type: i5.KbqRadioButton, selector: "kbq-radio-button", inputs: ["checked", "value", "disabled", "tabIndex", "required", "labelPosition", "name", "isFocused", "id"], outputs: ["change"], exportAs: ["kbqRadioButton"] }, { kind: "component", type: KbqIcon, selector: "[kbq-icon]", inputs: ["small", "autoColor", "kbq-icon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
371
660
  }
372
661
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.30", ngImport: i0, type: KbqTimeRangeEditor, decorators: [{
373
662
  type: Component,
374
663
  args: [{ selector: 'kbq-time-range-editor', imports: [
375
664
  NgTemplateOutlet,
376
665
  ReactiveFormsModule,
666
+ KbqError,
377
667
  KbqFieldset,
378
668
  KbqFieldsetItem,
669
+ KbqHint,
379
670
  KbqDatepickerModule,
380
671
  KbqTimepickerModule,
381
672
  KbqRadioModule,
@@ -393,7 +684,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.30", ngImpo
393
684
  }
394
685
  ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
395
686
  class: 'kbq-time-range__editor'
396
- }, template: "<form [formGroup]=\"form\">\n <kbq-radio-group class=\"kbq-time-range-editor__container\" [formControl]=\"form.controls.type\">\n @let types = timeRangeTypesWithoutRange();\n\n @if (types.length) {\n <div class=\"kbq-time-range-editor__predefined\">\n @for (type of types; track type) {\n <kbq-radio-button class=\"kbq-time-range-editor__predefined-option\" [value]=\"type.type\">\n <ng-container\n *ngTemplateOutlet=\"optionTemplate() || defaultOptionTemplate; context: { $implicit: type }\"\n />\n </kbq-radio-button>\n }\n </div>\n }\n\n @if (isRangeVisible()) {\n @let localeConfig = localeConfiguration();\n\n <div class=\"kbq-time-range-editor__range\" role=\"group\">\n @if (types.length) {\n <kbq-radio-button [value]=\"'range'\">{{ localeConfig.editor.rangeLabel }}</kbq-radio-button>\n }\n\n <div class=\"kbq-time-range-editor__date-time\">\n <span class=\"kbq-time-range-editor__date-time-prefix\" [attr.aria-label]=\"localeConfig.editor.from\">\n {{ localeConfig.editor.from }}\n </span>\n <kbq-fieldset>\n <kbq-form-field kbqFieldsetItem>\n <i kbq-icon=\"kbq-clock_16\" kbqPrefix [autoColor]=\"true\"></i>\n <input kbqTimepicker [format]=\"timepickerFormat\" [formControl]=\"form.controls.fromTime\" />\n </kbq-form-field>\n\n <kbq-form-field>\n <input\n [min]=\"minDate()\"\n [max]=\"maxDate()\"\n [kbqDatepicker]=\"datepickerFrom\"\n [formControl]=\"form.controls.fromDate\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n <kbq-datepicker-toggle-icon kbqSuffix [for]=\"datepickerFrom\" />\n <kbq-datepicker #datepickerFrom [minDate]=\"minDate()\" [maxDate]=\"maxDate()\" />\n </kbq-form-field>\n </kbq-fieldset>\n </div>\n\n <div class=\"kbq-time-range-editor__date-time\">\n <span class=\"kbq-time-range-editor__date-time-prefix\" [attr.aria-label]=\"localeConfig.editor.to\">\n {{ localeConfig.editor.to }}\n </span>\n <kbq-fieldset>\n <kbq-form-field kbqFieldsetItem>\n <i kbq-icon=\"kbq-clock_16\" kbqPrefix [autoColor]=\"true\"></i>\n <input kbqTimepicker [format]=\"timepickerFormat\" [formControl]=\"form.controls.toTime\" />\n </kbq-form-field>\n\n <kbq-form-field>\n <input\n [min]=\"minDate()\"\n [max]=\"maxDate()\"\n [kbqDatepicker]=\"datepickerTo\"\n [formControl]=\"form.controls.toDate\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n <kbq-datepicker-toggle-icon kbqSuffix [for]=\"datepickerTo\" />\n <kbq-datepicker #datepickerTo [minDate]=\"minDate()\" [maxDate]=\"maxDate()\" />\n </kbq-form-field>\n </kbq-fieldset>\n </div>\n </div>\n }\n </kbq-radio-group>\n</form>\n\n<ng-template #defaultOptionTemplate let-context>\n @let localeConfig = localeConfiguration();\n @switch (context.translationType) {\n @case ('other') {\n @switch (context.type) {\n @case ('allTime') {\n {{ localeConfig.editor.allTimeOption ?? localeConfig.editor.allTime }}\n }\n @case ('currentQuarter') {\n {{ localeConfig.editor.currentQuarterOption ?? localeConfig.editor.currentQuarter }}\n }\n @case ('currentYear') {\n {{ localeConfig.editor.currentYearOption ?? localeConfig.editor.currentYear }}\n }\n }\n }\n @default {\n {{ context.formattedValue }}\n }\n }\n</ng-template>\n" }]
687
+ }, template: "<form [formGroup]=\"form\">\n <kbq-radio-group class=\"kbq-time-range-editor__container\" [formControl]=\"form.controls.type\">\n @let types = timeRangeTypesWithoutRange();\n\n @if (types.length) {\n <div class=\"kbq-time-range-editor__predefined\">\n @for (type of types; track type) {\n <kbq-radio-button class=\"kbq-time-range-editor__predefined-option\" [value]=\"type.type\">\n <ng-container\n *ngTemplateOutlet=\"optionTemplate() || defaultOptionTemplate; context: { $implicit: type }\"\n />\n </kbq-radio-button>\n }\n </div>\n }\n\n @if (isRangeVisible()) {\n @let localeConfig = localeConfiguration();\n\n <div class=\"kbq-time-range-editor__range\" role=\"group\" (focusout)=\"onRangeFocusOut($event)\">\n @let bounds = boundsHint();\n\n @if (types.length) {\n <kbq-radio-button [value]=\"'range'\">\n {{ localeConfig.editor.rangeLabel }}\n @if (bounds) {\n <kbq-hint>{{ bounds }}</kbq-hint>\n }\n </kbq-radio-button>\n }\n\n <div\n class=\"kbq-time-range-editor__date-time\"\n (input)=\"onBorderInput('from')\"\n (focusout)=\"onBorderFocusOut('from', $event)\"\n >\n <span class=\"kbq-time-range-editor__date-time-prefix\" [attr.aria-label]=\"localeConfig.editor.from\">\n {{ localeConfig.editor.from }}\n </span>\n <kbq-fieldset>\n <kbq-form-field kbqFieldsetItem>\n <i kbq-icon=\"kbq-clock_16\" kbqPrefix [autoColor]=\"true\"></i>\n <input\n kbqTimepicker\n data-time-range-field=\"fromTime\"\n [format]=\"timepickerFormat\"\n [formControl]=\"form.controls.fromTime\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n </kbq-form-field>\n\n <kbq-form-field>\n <input\n data-time-range-field=\"fromDate\"\n [kbqDatepicker]=\"datepickerFrom\"\n [formControl]=\"form.controls.fromDate\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n <kbq-datepicker-toggle-icon kbqSuffix [for]=\"datepickerFrom\" />\n <kbq-datepicker #datepickerFrom [minDate]=\"minDate()\" [maxDate]=\"maxDate()\" />\n </kbq-form-field>\n </kbq-fieldset>\n </div>\n\n <div\n class=\"kbq-time-range-editor__date-time\"\n (input)=\"onBorderInput('to')\"\n (focusout)=\"onBorderFocusOut('to', $event)\"\n >\n <span class=\"kbq-time-range-editor__date-time-prefix\" [attr.aria-label]=\"localeConfig.editor.to\">\n {{ localeConfig.editor.to }}\n </span>\n <kbq-fieldset>\n <kbq-form-field kbqFieldsetItem>\n <i kbq-icon=\"kbq-clock_16\" kbqPrefix [autoColor]=\"true\"></i>\n <input\n kbqTimepicker\n data-time-range-field=\"toTime\"\n [format]=\"timepickerFormat\"\n [formControl]=\"form.controls.toTime\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n </kbq-form-field>\n\n <kbq-form-field>\n <input\n data-time-range-field=\"toDate\"\n [kbqDatepicker]=\"datepickerTo\"\n [formControl]=\"form.controls.toDate\"\n [errorStateMatcher]=\"rangeStateMatcher\"\n />\n <kbq-datepicker-toggle-icon kbqSuffix [for]=\"datepickerTo\" />\n <kbq-datepicker #datepickerTo [minDate]=\"minDate()\" [maxDate]=\"maxDate()\" />\n </kbq-form-field>\n\n <!-- The range is judged as a whole, so what is wrong with it is said once, below the\n last of its two ends - in place of the caption, which spells the same bounds out. -->\n @if (outOfBoundsMessage(); as message) {\n <kbq-error>{{ message }}</kbq-error>\n } @else if (!types.length && bounds) {\n <!-- With no presets there is no option to hang the bounds off, so they go in the\n fieldset's own hint area rather than loose in the block. -->\n <kbq-hint>{{ bounds }}</kbq-hint>\n }\n </kbq-fieldset>\n </div>\n </div>\n }\n </kbq-radio-group>\n</form>\n\n<ng-template #defaultOptionTemplate let-context>\n @let localeConfig = localeConfiguration();\n @switch (context.translationType) {\n @case ('other') {\n @switch (context.type) {\n @case ('allTime') {\n {{ localeConfig.editor.allTimeOption ?? localeConfig.editor.allTime }}\n }\n @case ('currentQuarter') {\n {{ localeConfig.editor.currentQuarterOption ?? localeConfig.editor.currentQuarter }}\n }\n @case ('currentYear') {\n {{ localeConfig.editor.currentYearOption ?? localeConfig.editor.currentYear }}\n }\n }\n }\n @default {\n {{ context.formattedValue }}\n }\n }\n</ng-template>\n" }]
397
688
  }], ctorParameters: () => [], propDecorators: { maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], availableTimeRangeTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTimeRangeTypes", required: false }] }], rangeValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeValue", required: false }] }], showRangeAsDefault: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRangeAsDefault", required: true }] }], localeConfiguration: [{ type: i0.Input, args: [{ isSignal: true, alias: "localeConfiguration", required: true }] }], optionTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTemplate", required: false }] }], timepickerList: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => KbqTimepicker), { isSignal: true }] }] } });
398
689
 
399
690
  /** @docs-private */
@@ -509,6 +800,8 @@ class KbqTimeRange {
509
800
  constructor() {
510
801
  this.timeRangeService = inject(KbqTimeRangeService);
511
802
  this.localeService = inject(KBQ_LOCALE_SERVICE, { optional: true });
803
+ this.editorBridge = inject(KbqTimeRangeEditorBridge);
804
+ this.document = inject(DOCUMENT);
512
805
  /** @docs-private */
513
806
  this.ngControl = inject(NgControl, { optional: true, self: true });
514
807
  /** The minimum selectable date. */
@@ -555,14 +848,20 @@ class KbqTimeRange {
555
848
  if (types.includes(this.rangeEditorControl.value.type) || this.rangeEditorControl.value.type === 'range') {
556
849
  return;
557
850
  }
558
- const timeRangeDefaultValue = this.timeRangeService.getTimeRangeDefaultValue(this.normalizedDefaultRangeValue(), types.length ? types : ['range']);
559
- this.titleValue.set(timeRangeDefaultValue);
851
+ const timeRangeDefaultValue = this.defaultEditorValue(types);
852
+ // The editor still needs something to open on, whether or not the trigger shows a value.
560
853
  this.rangeEditorControl.setValue(timeRangeDefaultValue);
854
+ // A nullable trigger that has never been given a value keeps its placeholder: replacing the types
855
+ // is not the user picking a range, and emitting here would put one into the bound control.
856
+ if (!this.nonNullable() && this.titleValue() === null)
857
+ return;
858
+ this.titleValue.set(timeRangeDefaultValue);
561
859
  this.onChange(timeRangeDefaultValue);
562
860
  };
563
861
  if (this.ngControl) {
564
862
  this.ngControl.valueAccessor = this;
565
863
  }
864
+ this.timeRangeService.bindBounds(this.minDate, this.maxDate);
566
865
  const defaultValue = this.timeRangeService.getTimeRangeDefaultValue(this.normalizedDefaultRangeValue(), this.availableTimeRangeTypes());
567
866
  this.titleValue = signal(this.nonNullable() ? defaultValue : null, ...(ngDevMode ? [{ debugName: "titleValue" }] : []));
568
867
  this.rangeEditorControl = new FormControl(defaultValue, { nonNullable: true });
@@ -589,21 +888,42 @@ class KbqTimeRange {
589
888
  this.titleValue.set(nonNullable || value !== null ? corrected : null);
590
889
  this.rangeEditorControl.setValue(corrected);
591
890
  }
592
- /** @docs-private */
891
+ /**
892
+ * Lets the editor tell an "apply" gesture from a plain focus loss before the click even lands - the
893
+ * `focusout` it reacts to is dispatched between this `mousedown` and the `click` below.
894
+ * @docs-private
895
+ */
896
+ onFooterPointerDown() {
897
+ this.editorBridge.applyGestureInProgress = true;
898
+ this.document.addEventListener('mouseup', () => (this.editorBridge.applyGestureInProgress = false), {
899
+ once: true
900
+ });
901
+ }
902
+ /**
903
+ * Applying is always offered; an invalid editor keeps the popover open and sends the user to the
904
+ * first field at fault instead.
905
+ * @docs-private
906
+ */
593
907
  onApply(popover) {
908
+ this.editorBridge.applyGestureInProgress = false;
909
+ if (!this.editorBridge.revealErrors())
910
+ return;
594
911
  this.titleValue.set(this.rangeEditorControl.value);
595
912
  this.onChange(this.rangeEditorControl.value);
596
913
  popover.hide();
597
914
  }
598
915
  /** @docs-private */
599
916
  onCancel(popover) {
917
+ this.editorBridge.applyGestureInProgress = false;
600
918
  popover.hide();
601
919
  }
602
920
  onVisibleChange(isVisible) {
603
- const titleValue = this.titleValue();
604
- if (!isVisible && titleValue) {
605
- this.rangeEditorControl.setValue(titleValue);
606
- }
921
+ if (isVisible)
922
+ return;
923
+ // Closing throws the edits away, applied or not. With nothing on the trigger yet there is no value
924
+ // to go back to, so the editor returns to its default - otherwise an abandoned edit, which may well
925
+ // be one the editor refused to apply, would be waiting the next time it opens.
926
+ this.rangeEditorControl.setValue(this.titleValue() ?? this.defaultEditorValue(this.availableTimeRangeTypes()));
607
927
  }
608
928
  /** Implemented as part of ControlValueAccessor */
609
929
  registerOnChange(fn) {
@@ -613,8 +933,12 @@ class KbqTimeRange {
613
933
  registerOnTouched(fn) {
614
934
  this.onTouch = fn;
615
935
  }
936
+ /** What the editor opens on: the first offered type, or a plain range when none are offered. */
937
+ defaultEditorValue(types) {
938
+ return this.timeRangeService.getTimeRangeDefaultValue(this.normalizedDefaultRangeValue(), types.length ? types : ['range']);
939
+ }
616
940
  /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.30", ngImport: i0, type: KbqTimeRange, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
617
- /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.30", type: KbqTimeRange, isStandalone: true, selector: "kbq-time-range", inputs: { minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, defaultRangeValue: { classPropertyName: "defaultRangeValue", publicName: "defaultRangeValue", isSignal: true, isRequired: false, transformFunction: null }, availableTimeRangeTypes: { classPropertyName: "availableTimeRangeTypes", publicName: "availableTimeRangeTypes", isSignal: true, isRequired: false, transformFunction: null }, titleTemplate: { classPropertyName: "titleTemplate", publicName: "titleTemplate", isSignal: true, isRequired: false, transformFunction: null }, optionTemplate: { classPropertyName: "optionTemplate", publicName: "optionTemplate", isSignal: true, isRequired: false, transformFunction: null }, arrow: { classPropertyName: "arrow", publicName: "arrow", isSignal: true, isRequired: false, transformFunction: null }, showRangeAsDefault: { classPropertyName: "showRangeAsDefault", publicName: "showRangeAsDefault", isSignal: true, isRequired: false, transformFunction: null }, nonNullable: { classPropertyName: "nonNullable", publicName: "nonNullable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueCorrected: "valueCorrected" }, host: { classAttribute: "kbq-time-range" }, providers: [KbqTimeRangeService], ngImport: i0, template: `
941
+ /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.30", type: KbqTimeRange, isStandalone: true, selector: "kbq-time-range", inputs: { minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, defaultRangeValue: { classPropertyName: "defaultRangeValue", publicName: "defaultRangeValue", isSignal: true, isRequired: false, transformFunction: null }, availableTimeRangeTypes: { classPropertyName: "availableTimeRangeTypes", publicName: "availableTimeRangeTypes", isSignal: true, isRequired: false, transformFunction: null }, titleTemplate: { classPropertyName: "titleTemplate", publicName: "titleTemplate", isSignal: true, isRequired: false, transformFunction: null }, optionTemplate: { classPropertyName: "optionTemplate", publicName: "optionTemplate", isSignal: true, isRequired: false, transformFunction: null }, arrow: { classPropertyName: "arrow", publicName: "arrow", isSignal: true, isRequired: false, transformFunction: null }, showRangeAsDefault: { classPropertyName: "showRangeAsDefault", publicName: "showRangeAsDefault", isSignal: true, isRequired: false, transformFunction: null }, nonNullable: { classPropertyName: "nonNullable", publicName: "nonNullable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueCorrected: "valueCorrected" }, host: { classAttribute: "kbq-time-range" }, providers: [KbqTimeRangeService, KbqTimeRangeEditorBridge], ngImport: i0, template: `
618
942
  @let localeConfig = localeConfiguration();
619
943
  <kbq-time-range-title
620
944
  #popover="kbqPopover"
@@ -646,20 +970,15 @@ class KbqTimeRange {
646
970
  </ng-template>
647
971
 
648
972
  <ng-template #timeRangePopoverFooter>
649
- <div class="kbq-time-range__buttons" role="group">
650
- <button
651
- kbq-button
652
- [color]="'contrast'"
653
- [disabled]="rangeEditorControl.invalid"
654
- (click)="onApply(popover)"
655
- >
973
+ <div class="kbq-time-range__buttons" role="group" (mousedown)="onFooterPointerDown()">
974
+ <button kbq-button [color]="'contrast'" (click)="onApply(popover)">
656
975
  {{ localeConfig.editor.apply }}
657
976
  </button>
658
977
 
659
978
  <button kbq-button (click)="onCancel(popover)">{{ localeConfig.editor.cancel }}</button>
660
979
  </div>
661
980
  </ng-template>
662
- `, isInline: true, styles: [".kbq-time-range .kbq-time-range__trigger{display:block}.kbq-time-range__popover .kbq-time-range__buttons{display:flex;flex-direction:row;gap:var(--kbq-size-l)}.kbq-time-range__editor{display:block}.kbq-time-range__editor .kbq-time-range-editor__container{width:100%}.kbq-time-range__editor .kbq-time-range-editor__predefined{column-count:2}.kbq-time-range__editor .kbq-time-range-editor__predefined .kbq-time-range-editor__predefined-option{display:block;margin-bottom:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range{display:flex;flex-direction:column;gap:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range .kbq-time-range-editor__date-time{display:flex;flex-direction:row;justify-content:space-between;gap:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range .kbq-time-range-editor__date-time .kbq-time-range-editor__date-time-prefix{align-self:center;width:57px}.kbq-time-range-title-as-form-field{white-space:nowrap;text-overflow:ellipsis;min-height:calc(var(--kbq-form-field-size-height) - 2 * var(--kbq-size-border-width))}.kbq-time-range-title-as-form-field .kbq-time-range-title__placeholder{color:var(--kbq-foreground-contrast-tertiary)}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: KbqPopoverModule }, { kind: "directive", type: i2$2.KbqPopoverTrigger, selector: "[kbqPopover]", inputs: ["hideIfNotInViewPort", "kbqPopoverPreventClose", "defaultPaddings", "kbqPopoverVisible", "kbqPopoverPlacement", "kbqPopoverPlacementPriority", "kbqPopoverStickToWindow", "container", "hasBackdrop", "kbqPopoverHeader", "kbqPopoverContent", "kbqPopoverFooter", "kbqPopoverDisabled", "kbqTrigger", "kbqPopoverSize", "kbqPopoverClass", "kbqPopoverContext", "hasCloseButton", "closeOnScroll", "backdropClass", "kbqPopoverArrow", "kbqPopoverOffset", "kbqLeaveDelay"], outputs: ["kbqPopoverPlacementChange", "kbqPopoverVisibleChange"], exportAs: ["kbqPopover"] }, { kind: "ngmodule", type: KbqButtonModule }, { kind: "component", type: i3$1.KbqButton, selector: "[kbq-button]", inputs: ["kbqStyle", "disabled", "tabIndex"] }, { kind: "directive", type: i3$1.KbqButtonCssStyler, selector: "[kbq-button]" }, { kind: "component", type: KbqTimeRangeEditor, selector: "kbq-time-range-editor", inputs: ["maxDate", "minDate", "availableTimeRangeTypes", "rangeValue", "showRangeAsDefault", "localeConfiguration", "optionTemplate"] }, { kind: "component", type: KbqTimeRangeTitle, selector: "kbq-time-range-title", inputs: ["timeRange", "localeConfiguration", "titleTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
981
+ `, isInline: true, styles: [".kbq-time-range .kbq-time-range__trigger{display:block}.kbq-time-range__popover .kbq-time-range__buttons{display:flex;flex-direction:row;gap:var(--kbq-size-l)}.kbq-time-range__editor{display:block}.kbq-time-range__editor .kbq-time-range-editor__container{width:100%}.kbq-time-range__editor .kbq-time-range-editor__predefined{column-count:2}.kbq-time-range__editor .kbq-time-range-editor__predefined .kbq-time-range-editor__predefined-option{display:block;margin-bottom:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range{display:flex;flex-direction:column;gap:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range .kbq-time-range-editor__date-time{display:flex;flex-direction:row;justify-content:space-between;gap:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range .kbq-time-range-editor__date-time .kbq-time-range-editor__date-time-prefix{display:flex;align-items:center;align-self:flex-start;min-height:var(--kbq-size-3xl);flex:none;width:57px}.kbq-time-range__editor .kbq-time-range-editor__range .kbq-time-range-editor__date-time .kbq-fieldset{flex:1;min-width:0}.kbq-time-range-title-as-form-field{white-space:nowrap;text-overflow:ellipsis;min-height:calc(var(--kbq-form-field-size-height) - 2 * var(--kbq-size-border-width))}.kbq-time-range-title-as-form-field .kbq-time-range-title__placeholder{color:var(--kbq-foreground-contrast-tertiary)}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: KbqPopoverModule }, { kind: "directive", type: i2$2.KbqPopoverTrigger, selector: "[kbqPopover]", inputs: ["hideIfNotInViewPort", "kbqPopoverPreventClose", "defaultPaddings", "kbqPopoverVisible", "kbqPopoverPlacement", "kbqPopoverPlacementPriority", "kbqPopoverStickToWindow", "container", "hasBackdrop", "kbqPopoverHeader", "kbqPopoverContent", "kbqPopoverFooter", "kbqPopoverDisabled", "kbqTrigger", "kbqPopoverSize", "kbqPopoverClass", "kbqPopoverContext", "hasCloseButton", "closeOnScroll", "backdropClass", "kbqPopoverArrow", "kbqPopoverOffset", "kbqLeaveDelay"], outputs: ["kbqPopoverPlacementChange", "kbqPopoverVisibleChange"], exportAs: ["kbqPopover"] }, { kind: "ngmodule", type: KbqButtonModule }, { kind: "component", type: i3$1.KbqButton, selector: "[kbq-button]", inputs: ["kbqStyle", "disabled", "tabIndex"] }, { kind: "directive", type: i3$1.KbqButtonCssStyler, selector: "[kbq-button]" }, { kind: "component", type: KbqTimeRangeEditor, selector: "kbq-time-range-editor", inputs: ["maxDate", "minDate", "availableTimeRangeTypes", "rangeValue", "showRangeAsDefault", "localeConfiguration", "optionTemplate"] }, { kind: "component", type: KbqTimeRangeTitle, selector: "kbq-time-range-title", inputs: ["timeRange", "localeConfiguration", "titleTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
663
982
  }
664
983
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.30", ngImport: i0, type: KbqTimeRange, decorators: [{
665
984
  type: Component,
@@ -701,22 +1020,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.30", ngImpo
701
1020
  </ng-template>
702
1021
 
703
1022
  <ng-template #timeRangePopoverFooter>
704
- <div class="kbq-time-range__buttons" role="group">
705
- <button
706
- kbq-button
707
- [color]="'contrast'"
708
- [disabled]="rangeEditorControl.invalid"
709
- (click)="onApply(popover)"
710
- >
1023
+ <div class="kbq-time-range__buttons" role="group" (mousedown)="onFooterPointerDown()">
1024
+ <button kbq-button [color]="'contrast'" (click)="onApply(popover)">
711
1025
  {{ localeConfig.editor.apply }}
712
1026
  </button>
713
1027
 
714
1028
  <button kbq-button (click)="onCancel(popover)">{{ localeConfig.editor.cancel }}</button>
715
1029
  </div>
716
1030
  </ng-template>
717
- `, providers: [KbqTimeRangeService], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
1031
+ `, providers: [KbqTimeRangeService, KbqTimeRangeEditorBridge], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
718
1032
  class: 'kbq-time-range'
719
- }, styles: [".kbq-time-range .kbq-time-range__trigger{display:block}.kbq-time-range__popover .kbq-time-range__buttons{display:flex;flex-direction:row;gap:var(--kbq-size-l)}.kbq-time-range__editor{display:block}.kbq-time-range__editor .kbq-time-range-editor__container{width:100%}.kbq-time-range__editor .kbq-time-range-editor__predefined{column-count:2}.kbq-time-range__editor .kbq-time-range-editor__predefined .kbq-time-range-editor__predefined-option{display:block;margin-bottom:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range{display:flex;flex-direction:column;gap:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range .kbq-time-range-editor__date-time{display:flex;flex-direction:row;justify-content:space-between;gap:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range .kbq-time-range-editor__date-time .kbq-time-range-editor__date-time-prefix{align-self:center;width:57px}.kbq-time-range-title-as-form-field{white-space:nowrap;text-overflow:ellipsis;min-height:calc(var(--kbq-form-field-size-height) - 2 * var(--kbq-size-border-width))}.kbq-time-range-title-as-form-field .kbq-time-range-title__placeholder{color:var(--kbq-foreground-contrast-tertiary)}\n"] }]
1033
+ }, styles: [".kbq-time-range .kbq-time-range__trigger{display:block}.kbq-time-range__popover .kbq-time-range__buttons{display:flex;flex-direction:row;gap:var(--kbq-size-l)}.kbq-time-range__editor{display:block}.kbq-time-range__editor .kbq-time-range-editor__container{width:100%}.kbq-time-range__editor .kbq-time-range-editor__predefined{column-count:2}.kbq-time-range__editor .kbq-time-range-editor__predefined .kbq-time-range-editor__predefined-option{display:block;margin-bottom:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range{display:flex;flex-direction:column;gap:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range .kbq-time-range-editor__date-time{display:flex;flex-direction:row;justify-content:space-between;gap:var(--kbq-size-s)}.kbq-time-range__editor .kbq-time-range-editor__range .kbq-time-range-editor__date-time .kbq-time-range-editor__date-time-prefix{display:flex;align-items:center;align-self:flex-start;min-height:var(--kbq-size-3xl);flex:none;width:57px}.kbq-time-range__editor .kbq-time-range-editor__range .kbq-time-range-editor__date-time .kbq-fieldset{flex:1;min-width:0}.kbq-time-range-title-as-form-field{white-space:nowrap;text-overflow:ellipsis;min-height:calc(var(--kbq-form-field-size-height) - 2 * var(--kbq-size-border-width))}.kbq-time-range-title-as-form-field .kbq-time-range-title__placeholder{color:var(--kbq-foreground-contrast-tertiary)}\n"] }]
720
1034
  }], ctorParameters: () => [], propDecorators: { minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], defaultRangeValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultRangeValue", required: false }] }], availableTimeRangeTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTimeRangeTypes", required: false }] }], titleTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleTemplate", required: false }] }], optionTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTemplate", required: false }] }], arrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrow", required: false }] }], showRangeAsDefault: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRangeAsDefault", required: false }] }], nonNullable: [{ type: i0.Input, args: [{ isSignal: true, alias: "nonNullable", required: false }] }], valueCorrected: [{ type: i0.Output, args: ["valueCorrected"] }] } });
721
1035
 
722
1036
  /** Directive for easy using styles of time-range placeholder publicly. */