ngwr 14.6.0 → 14.8.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.
Files changed (169) hide show
  1. package/alert/styles/_index.scss +14 -3
  2. package/avatar/styles/_index.scss +44 -0
  3. package/bar-chart/styles/_index.scss +1 -0
  4. package/calendar-heatmap/styles/_index.scss +1 -0
  5. package/cascader/styles/_rules.scss +86 -18
  6. package/collapse/styles/_index.scss +0 -1
  7. package/date-picker/styles/_index.scss +30 -0
  8. package/density/styles/_index.scss +4 -2
  9. package/donut-chart/styles/_index.scss +10 -0
  10. package/dropdown/styles/_index.scss +67 -5
  11. package/event-calendar/styles/_index.scss +209 -35
  12. package/fesm2022/ngwr-avatar.mjs +19 -2
  13. package/fesm2022/ngwr-avatar.mjs.map +1 -1
  14. package/fesm2022/ngwr-bar-chart.mjs +29 -4
  15. package/fesm2022/ngwr-bar-chart.mjs.map +1 -1
  16. package/fesm2022/ngwr-calendar-heatmap.mjs +59 -4
  17. package/fesm2022/ngwr-calendar-heatmap.mjs.map +1 -1
  18. package/fesm2022/ngwr-cascader.mjs +2 -2
  19. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  20. package/fesm2022/ngwr-checkbox.mjs +12 -2
  21. package/fesm2022/ngwr-checkbox.mjs.map +1 -1
  22. package/fesm2022/ngwr-date-picker.mjs +705 -46
  23. package/fesm2022/ngwr-date-picker.mjs.map +1 -1
  24. package/fesm2022/ngwr-donut-chart.mjs +46 -4
  25. package/fesm2022/ngwr-donut-chart.mjs.map +1 -1
  26. package/fesm2022/ngwr-dropdown.mjs +166 -17
  27. package/fesm2022/ngwr-dropdown.mjs.map +1 -1
  28. package/fesm2022/ngwr-event-calendar.mjs +9 -2
  29. package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
  30. package/fesm2022/ngwr-form.mjs +52 -1
  31. package/fesm2022/ngwr-form.mjs.map +1 -1
  32. package/fesm2022/ngwr-gauge.mjs +33 -4
  33. package/fesm2022/ngwr-gauge.mjs.map +1 -1
  34. package/fesm2022/ngwr-i18n-ar.mjs +2 -0
  35. package/fesm2022/ngwr-i18n-ar.mjs.map +1 -1
  36. package/fesm2022/ngwr-i18n-cs.mjs +2 -0
  37. package/fesm2022/ngwr-i18n-cs.mjs.map +1 -1
  38. package/fesm2022/ngwr-i18n-de.mjs +2 -0
  39. package/fesm2022/ngwr-i18n-de.mjs.map +1 -1
  40. package/fesm2022/ngwr-i18n-en.mjs +5 -0
  41. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  42. package/fesm2022/ngwr-i18n-es.mjs +2 -0
  43. package/fesm2022/ngwr-i18n-es.mjs.map +1 -1
  44. package/fesm2022/ngwr-i18n-fr.mjs +2 -0
  45. package/fesm2022/ngwr-i18n-fr.mjs.map +1 -1
  46. package/fesm2022/ngwr-i18n-he.mjs +2 -0
  47. package/fesm2022/ngwr-i18n-he.mjs.map +1 -1
  48. package/fesm2022/ngwr-i18n-hi.mjs +2 -0
  49. package/fesm2022/ngwr-i18n-hi.mjs.map +1 -1
  50. package/fesm2022/ngwr-i18n-id.mjs +2 -0
  51. package/fesm2022/ngwr-i18n-id.mjs.map +1 -1
  52. package/fesm2022/ngwr-i18n-it.mjs +2 -0
  53. package/fesm2022/ngwr-i18n-it.mjs.map +1 -1
  54. package/fesm2022/ngwr-i18n-ja.mjs +2 -0
  55. package/fesm2022/ngwr-i18n-ja.mjs.map +1 -1
  56. package/fesm2022/ngwr-i18n-ko.mjs +2 -0
  57. package/fesm2022/ngwr-i18n-ko.mjs.map +1 -1
  58. package/fesm2022/ngwr-i18n-nl.mjs +2 -0
  59. package/fesm2022/ngwr-i18n-nl.mjs.map +1 -1
  60. package/fesm2022/ngwr-i18n-pl.mjs +2 -0
  61. package/fesm2022/ngwr-i18n-pl.mjs.map +1 -1
  62. package/fesm2022/ngwr-i18n-pt.mjs +2 -0
  63. package/fesm2022/ngwr-i18n-pt.mjs.map +1 -1
  64. package/fesm2022/ngwr-i18n-ru.mjs +2 -0
  65. package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
  66. package/fesm2022/ngwr-i18n-sv.mjs +2 -0
  67. package/fesm2022/ngwr-i18n-sv.mjs.map +1 -1
  68. package/fesm2022/ngwr-i18n-tr.mjs +2 -0
  69. package/fesm2022/ngwr-i18n-tr.mjs.map +1 -1
  70. package/fesm2022/ngwr-i18n-uk.mjs +2 -0
  71. package/fesm2022/ngwr-i18n-uk.mjs.map +1 -1
  72. package/fesm2022/ngwr-i18n-vi.mjs +2 -0
  73. package/fesm2022/ngwr-i18n-vi.mjs.map +1 -1
  74. package/fesm2022/ngwr-i18n-zh-TW.mjs +2 -0
  75. package/fesm2022/ngwr-i18n-zh-TW.mjs.map +1 -1
  76. package/fesm2022/ngwr-i18n-zh.mjs +2 -0
  77. package/fesm2022/ngwr-i18n-zh.mjs.map +1 -1
  78. package/fesm2022/ngwr-image-cropper.mjs +149 -62
  79. package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
  80. package/fesm2022/ngwr-input-number.mjs +12 -0
  81. package/fesm2022/ngwr-input-number.mjs.map +1 -1
  82. package/fesm2022/ngwr-lightbox.mjs +2 -2
  83. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  84. package/fesm2022/ngwr-line-chart.mjs +16 -5
  85. package/fesm2022/ngwr-line-chart.mjs.map +1 -1
  86. package/fesm2022/ngwr-meter-group-testing.mjs +7 -3
  87. package/fesm2022/ngwr-meter-group-testing.mjs.map +1 -1
  88. package/fesm2022/ngwr-meter-group.mjs +24 -4
  89. package/fesm2022/ngwr-meter-group.mjs.map +1 -1
  90. package/fesm2022/ngwr-page-header.mjs +10 -2
  91. package/fesm2022/ngwr-page-header.mjs.map +1 -1
  92. package/fesm2022/ngwr-popover.mjs +569 -7
  93. package/fesm2022/ngwr-popover.mjs.map +1 -1
  94. package/fesm2022/ngwr-select.mjs +73 -22
  95. package/fesm2022/ngwr-select.mjs.map +1 -1
  96. package/fesm2022/ngwr-sidebar.mjs +7 -0
  97. package/fesm2022/ngwr-sidebar.mjs.map +1 -1
  98. package/fesm2022/ngwr-sparkline.mjs +82 -8
  99. package/fesm2022/ngwr-sparkline.mjs.map +1 -1
  100. package/fesm2022/ngwr-table.mjs +1 -1
  101. package/fesm2022/ngwr-table.mjs.map +1 -1
  102. package/fesm2022/ngwr-toolbar.mjs +7 -0
  103. package/fesm2022/ngwr-toolbar.mjs.map +1 -1
  104. package/fesm2022/ngwr-version.mjs +1 -1
  105. package/fesm2022/ngwr-version.mjs.map +1 -1
  106. package/form/styles/_index.scss +11 -1
  107. package/gauge/styles/_index.scss +1 -0
  108. package/i18n/ar.json +3 -1
  109. package/i18n/cs.json +3 -1
  110. package/i18n/de.json +3 -1
  111. package/i18n/en.json +3 -1
  112. package/i18n/es.json +3 -1
  113. package/i18n/fr.json +3 -1
  114. package/i18n/he.json +3 -1
  115. package/i18n/hi.json +3 -1
  116. package/i18n/id.json +3 -1
  117. package/i18n/it.json +3 -1
  118. package/i18n/ja.json +3 -1
  119. package/i18n/ko.json +3 -1
  120. package/i18n/nl.json +3 -1
  121. package/i18n/pl.json +3 -1
  122. package/i18n/pt.json +3 -1
  123. package/i18n/ru.json +3 -1
  124. package/i18n/sv.json +3 -1
  125. package/i18n/tr.json +3 -1
  126. package/i18n/uk.json +3 -1
  127. package/i18n/vi.json +3 -1
  128. package/i18n/zh-TW.json +3 -1
  129. package/i18n/zh.json +3 -1
  130. package/input/styles/_index.scss +27 -2
  131. package/input-number/styles/_index.scss +69 -0
  132. package/lightbox/styles/_rules.scss +7 -1
  133. package/line-chart/styles/_index.scss +30 -2
  134. package/llms-full.txt +3 -3
  135. package/meter-group/styles/_index.scss +1 -0
  136. package/overlay/styles/_arrow.scss +27 -2
  137. package/package.json +1 -1
  138. package/page-header/styles/_index.scss +37 -1
  139. package/pagination/styles/_index.scss +9 -5
  140. package/popover/styles/_index.scss +73 -5
  141. package/select/styles/_index.scss +64 -13
  142. package/sidebar/styles/_index.scss +34 -2
  143. package/sparkline/styles/_index.scss +19 -0
  144. package/table/styles/_index.scss +55 -0
  145. package/toast/styles/_index.scss +9 -3
  146. package/toolbar/styles/_index.scss +35 -1
  147. package/tree/styles/_index.scss +45 -9
  148. package/types/ngwr-avatar.d.ts +15 -1
  149. package/types/ngwr-bar-chart.d.ts +19 -1
  150. package/types/ngwr-calendar-heatmap.d.ts +29 -1
  151. package/types/ngwr-checkbox.d.ts +2 -0
  152. package/types/ngwr-date-picker.d.ts +308 -43
  153. package/types/ngwr-donut-chart.d.ts +24 -1
  154. package/types/ngwr-dropdown.d.ts +22 -2
  155. package/types/ngwr-event-calendar.d.ts +8 -0
  156. package/types/ngwr-form.d.ts +35 -5
  157. package/types/ngwr-gauge.d.ts +20 -1
  158. package/types/ngwr-image-cropper.d.ts +48 -0
  159. package/types/ngwr-input-number.d.ts +7 -0
  160. package/types/ngwr-line-chart.d.ts +9 -1
  161. package/types/ngwr-meter-group-testing.d.ts +3 -2
  162. package/types/ngwr-meter-group.d.ts +14 -1
  163. package/types/ngwr-page-header.d.ts +10 -2
  164. package/types/ngwr-popover.d.ts +154 -3
  165. package/types/ngwr-select.d.ts +63 -6
  166. package/types/ngwr-sidebar.d.ts +7 -0
  167. package/types/ngwr-sparkline.d.ts +43 -1
  168. package/types/ngwr-toolbar.d.ts +7 -0
  169. package/types/ngwr-version.d.ts +1 -1
@@ -3,14 +3,15 @@ import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coerci
3
3
  import { ScrollStrategyOptions } from '@angular/cdk/overlay';
4
4
  import { ComponentPortal } from '@angular/cdk/portal';
5
5
  import * as i0 from '@angular/core';
6
- import { input, inject, ElementRef, model, output, signal, computed, afterNextRender, effect, untracked, ViewEncapsulation, Component, DestroyRef, Injector, viewChild, Directive } from '@angular/core';
6
+ import { input, inject, ElementRef, model, output, signal, computed, afterNextRender, effect, untracked, ViewEncapsulation, Component, Injectable, DestroyRef, Injector, viewChild, Directive } from '@angular/core';
7
7
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
8
  import { WrCalendar } from 'ngwr/calendar';
9
9
  import { WrDateAdapter, WR_DATE_LOCALE } from 'ngwr/date';
10
- import { readI18nText, useI18nText } from 'ngwr/i18n';
10
+ import { readI18nText, useI18nFormatter, useI18nText } from 'ngwr/i18n';
11
11
  import { WrInput, WrInputGroup, WrInputSuffix } from 'ngwr/input';
12
12
  import { WR_OVERLAY, WrOutsideClick, wrFollowDirection } from 'ngwr/overlay';
13
13
  import { clamp } from 'ngwr/utils';
14
+ import { transformedValue } from '@angular/forms/signals';
14
15
  import { WR_FORM_FIELD } from 'ngwr/form';
15
16
 
16
17
  /**
@@ -403,6 +404,200 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
403
404
  args: [{ selector: 'wr-date-time-panel', encapsulation: ViewEncapsulation.None, host: { class: 'wr-date-time-panel' }, imports: [WrCalendar, WrTimePanel], template: "<wr-calendar\n [date]=\"calendarValue()\"\n [min]=\"min()\"\n [max]=\"max()\"\n [dateFilter]=\"dateFilter()\"\n [autoFocus]=\"autoFocus()\"\n (dateChange)=\"onCalendarSelect($event)\"\n/>\n\n<div class=\"wr-date-time-panel__time\">\n <wr-time-picker\n [value]=\"timeValue()\"\n [format]=\"timeFormat()\"\n [showSeconds]=\"showSeconds()\"\n [step]=\"step()\"\n (valueChange)=\"onTimeChange($event)\"\n />\n</div>\n" }]
404
405
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], showSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeconds", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], autoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocus", required: false }] }], changed: [{ type: i0.Output, args: ["changed"] }] } });
405
406
 
407
+ /**
408
+ * @license
409
+ *
410
+ * Use of this source code is governed by an MIT-style license that can be
411
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
412
+ */
413
+ /** Per-picker live-region ids — see {@link WrDateInputField.statusId}. */
414
+ let statusUid = 0;
415
+ /**
416
+ * The refusals both pickers raise, from one place — the calendar, the single
417
+ * picker and the range picker have to agree on what "refused" means, and the
418
+ * message has to be the one `<wr-form-field>` would show for the same key.
419
+ *
420
+ * Must be called from an injection context.
421
+ *
422
+ * @internal
423
+ */
424
+ function useDateInputRefusals() {
425
+ const adapter = inject(WrDateAdapter);
426
+ // The same keys and the same English as `WR_FORM_ERROR_FALLBACKS`: the message a
427
+ // picker announces on its own and the one a surrounding field renders are one
428
+ // sentence, whichever of the two ends up saying it.
429
+ const formatText = useI18nFormatter('validation.dateFormat', 'Expected format: {{example}}');
430
+ const minText = useI18nFormatter('validation.minDate', 'Choose a later date.');
431
+ const maxText = useI18nFormatter('validation.maxDate', 'Choose an earlier date.');
432
+ const filterText = useI18nFormatter('validation.dateFilter', 'This date is not available.');
433
+ return {
434
+ unreadable(format) {
435
+ // The 31st of December, at 18:45: a day past 12 cannot be mistaken for a month,
436
+ // so the example shows the ORDER of the fields as well as their separators —
437
+ // which is the whole question for someone typing `07.09.1994` into a field
438
+ // that reads `9/7/1994`. The year is the current one so it never looks stale.
439
+ const reference = adapter.setTime(adapter.createDate(adapter.getYear(adapter.today()), 11, 31), 18, 45, 0);
440
+ const example = adapter.format(reference, format);
441
+ return { kind: 'dateFormat', example, message: formatText({ example }) };
442
+ },
443
+ outOfBounds(date, { min, max, filter }) {
444
+ if (min && adapter.compareDate(date, min) < 0)
445
+ return { kind: 'minDate', date, min, message: minText() };
446
+ if (max && adapter.compareDate(date, max) > 0)
447
+ return { kind: 'maxDate', date, max, message: maxText() };
448
+ if (filter && !filter(date))
449
+ return { kind: 'dateFilter', date, message: filterText() };
450
+ return null;
451
+ },
452
+ };
453
+ }
454
+ /**
455
+ * Hands the picker's refusals to the form it is bound to, so a refused entry makes
456
+ * the FORM invalid rather than only the field's border.
457
+ *
458
+ * Angular's `transformedValue` is the public way a custom control reports parse
459
+ * errors: bound with `[formField]` they join the field's errors, and bound with
460
+ * `[formControl]`, `formControlName` or `[(ngModel)]` they become a validator on
461
+ * the control. It is used here for that channel ONLY. The picker keeps its own text
462
+ * model — the echo guard, and the range picker's per-end ordering, are older and
463
+ * pinned — so `parse` never returns a `value`, which Angular documents as leaving the
464
+ * model untouched.
465
+ *
466
+ * The returned function reports the picker's WHOLE current list, every time. The form
467
+ * hears it from the keystroke — the picker's own wait for Enter or blur is about what
468
+ * it shows, and a form whose validity lagged the text in its field could submit the old
469
+ * value under text nobody meant.
470
+ *
471
+ * Angular's copy does not simply hold what was reported. It is a `linkedSignal` over the
472
+ * model, so ANY model write clears it, and a form reset clears it without a model write
473
+ * at all:
474
+ *
475
+ * - a write of the picker's own is followed by a report of its own, synchronously — the
476
+ * range picker committing one end while the other still holds refused text is the
477
+ * case that needs it;
478
+ * - a write from outside replaces the text a refusal was about, and the picker reports
479
+ * its emptied list;
480
+ * - what is left is a reset (`FormControl.reset()`, a Signal Forms field's `reset()`):
481
+ * the raw signal moved while the model did not. Angular has thrown the error away, and
482
+ * `onReset` is where the picker throws away the text it was about.
483
+ *
484
+ * Unbound, nothing resets it and it costs a signal and an effect.
485
+ *
486
+ * Must be called from an injection context.
487
+ *
488
+ * @internal
489
+ */
490
+ function useFormParseErrors(value, onReset) {
491
+ const channel = transformedValue(value, {
492
+ parse: errors => ({ error: errors.length ? errors : undefined }),
493
+ format: () => [],
494
+ });
495
+ /** What the raw signal held after the last report, and the model it was reported against. */
496
+ let held = untracked(channel);
497
+ let against = untracked(value);
498
+ const report = (errors) => {
499
+ channel.set(errors);
500
+ held = untracked(channel);
501
+ against = untracked(value);
502
+ };
503
+ effect(() => {
504
+ const now = channel();
505
+ untracked(() => {
506
+ if (now === held)
507
+ return;
508
+ held = now;
509
+ if (Object.is(value(), against))
510
+ onReset();
511
+ against = value();
512
+ });
513
+ });
514
+ return report;
515
+ }
516
+ /**
517
+ * What a picker's own text input sees as its `<wr-form-field>`: the real field, with
518
+ * the refusal on show added to it. Provided per picker, and handed to the input
519
+ * through `viewProviders`.
520
+ *
521
+ * `wrInput` writes `aria-invalid` and `aria-describedby` from the field it injects, and
522
+ * a second binding on either attribute leaves it holding whichever wrote last — so
523
+ * the picker cannot bind them beside `wrInput` while a field is there. It stands
524
+ * between the two instead. The input then reads ONE state: invalid when the field
525
+ * has an error OR the picker is showing a refusal, and described by the field's
526
+ * message together with the picker's own reason while the field is not yet saying it.
527
+ *
528
+ * "Not yet" is ordinary rather than rare. A refusal reaches a bound form as a parse
529
+ * error, and `<wr-form-field>` shows an error only once its control is touched or
530
+ * dirty; a reactive `FormControl` learns of it through `updateValueAndValidity` with
531
+ * `emitEvent: false`, which the field does not hear until the control's next event.
532
+ * Until the field says it, the picker does — so the refusal is never silent,
533
+ * whichever of the two ends up announcing it.
534
+ *
535
+ * With no field around the picker, there is nothing to stand between and `context` is
536
+ * `null`: the input's `wrInput` then writes neither attribute, and the picker binds
537
+ * them itself.
538
+ *
539
+ * @internal
540
+ */
541
+ class WrDateInputField {
542
+ /** The surrounding `<wr-form-field>` — skipping the picker's own node, whose view it is providing for. */
543
+ outer = inject(WR_FORM_FIELD, { optional: true, skipSelf: true });
544
+ /** Id of the picker's live region, which doubles as the reason `aria-describedby` points at. */
545
+ statusId = `wr-date-picker-status-${++statusUid}`;
546
+ source = signal(null, /* @ts-ignore */
547
+ ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
548
+ /** The refusal on show for the input the field describes — the only input, or a range's start. */
549
+ described = computed(() => this.source()?.() ?? null, /* @ts-ignore */
550
+ ...(ngDevMode ? [{ debugName: "described" }] : /* istanbul ignore next */ []));
551
+ /**
552
+ * Hand over the signal holding that refusal. Called from the picker's constructor:
553
+ * this service is built before the component it serves, so it cannot read the
554
+ * component's state at its own construction.
555
+ */
556
+ describe(source) {
557
+ this.source.set(source);
558
+ }
559
+ /** Whether the field is already rendering a message for this kind of error. */
560
+ saysAlready(error) {
561
+ return this.outer?.errorKeys().includes(error.kind) ?? false;
562
+ }
563
+ context = this.outer ? this.wrap(this.outer) : null;
564
+ wrap(outer) {
565
+ return {
566
+ controlId: outer.controlId,
567
+ adoptControlId: id => outer.adoptControlId(id),
568
+ labelId: outer.labelId,
569
+ hintId: outer.hintId,
570
+ errorKeys: computed(() => {
571
+ const keys = outer.errorKeys();
572
+ const own = this.described();
573
+ return own && !keys.includes(own.kind) ? [...keys, own.kind] : keys;
574
+ }),
575
+ describedBy: computed(() => {
576
+ const own = this.described();
577
+ const ids = [outer.describedBy(), own && !this.saysAlready(own) ? this.statusId : null].filter(Boolean);
578
+ return ids.length ? ids.join(' ') : null;
579
+ }),
580
+ };
581
+ }
582
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDateInputField, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
583
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDateInputField });
584
+ }
585
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDateInputField, decorators: [{
586
+ type: Injectable
587
+ }] });
588
+ /**
589
+ * The view half of {@link WrDateInputField}: what the picker's own template sees as
590
+ * its field. A VIEW provider, so it reaches the text inputs the picker renders and
591
+ * never content projected into it; the host half is `WrDateInputField` itself, in
592
+ * the component's `providers`.
593
+ *
594
+ * @internal
595
+ */
596
+ const WR_DATE_INPUT_FIELD_VIEW_PROVIDER = {
597
+ provide: WR_FORM_FIELD,
598
+ useFactory: () => inject(WrDateInputField).context,
599
+ };
600
+
406
601
  /**
407
602
  * @license
408
603
  *
@@ -410,6 +605,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
410
605
  * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
411
606
  */
412
607
  let panelUid = 0;
608
+ /** How long a live region stays blank before a repeated refusal is written back. */
609
+ const SILENCE_MS$1 = 100;
413
610
  /**
414
611
  * Unified date / time / date-time picker. Same `<input>` + popover skeleton
415
612
  * for every mode — the overlay content swaps based on `[mode]`:
@@ -427,8 +624,12 @@ let panelUid = 0;
427
624
  * `[(ngModel)]` / reactive forms keep working through Angular's bridge. Value
428
625
  * type is `Date | null` for every mode.
429
626
  *
430
- * Parses the input on every keystroke (silently — only emits when valid) and
431
- * re-formats canonical on blur. Format is driven by {@link WrDateAdapter}: it
627
+ * Parses the input on every keystroke and emits only a date it can read and the
628
+ * calendar would accept. Anything else is REFUSED, never guessed: the bound value
629
+ * stays where it was, and the text stays in the field so it can be corrected.
630
+ * Enter and leaving the field turn a refusal visible — see {@link inputError}.
631
+ * A committed date is re-formatted canonical on blur and on Enter. Format is
632
+ * driven by {@link WrDateAdapter}: it
432
633
  * accepts both named keys (`'shortDate'`, `'mediumDateTime'`, …) and raw
433
634
  * token strings (`'dd.MM.yyyy'`, `'HH:mm'`). When `format` is left at the
434
635
  * default (`null`), the picker derives the right named key from `mode`:
@@ -611,10 +812,99 @@ class WrDatePicker {
611
812
  return this.panelLabelDate();
612
813
  }, /* @ts-ignore */
613
814
  ...(ngDevMode ? [{ debugName: "resolvedPanelLabel" }] : /* istanbul ignore next */ []));
815
+ refusals = useDateInputRefusals();
816
+ reportToForm = useFormParseErrors(this.value, () => this.onFormReset());
817
+ inputField = inject(WrDateInputField);
818
+ /**
819
+ * Why the text in the field was not committed — `null` while the text is the value.
820
+ * Raised on the keystroke that makes the text unusable, and reported to a bound form
821
+ * from that keystroke on.
822
+ */
823
+ refusal = signal(null, /* @ts-ignore */
824
+ ...(ngDevMode ? [{ debugName: "refusal" }] : /* istanbul ignore next */ []));
825
+ /**
826
+ * Whether the refusal is on show. A refusal is raised on the keystroke but shown
827
+ * only once the user has finished — on Enter, or on leaving the field — because
828
+ * every date is unreadable for most of the time it takes to type one, and a field
829
+ * that turned red on the first digit would be telling the user off for typing.
830
+ * Once shown it follows the text live, so a correction clears it immediately.
831
+ *
832
+ * Leaving counts wherever focus goes, including by way of the open panel: moving
833
+ * into the panel waits, since a pick there replaces the text, but focus that leaves
834
+ * the panel for anything other than the field — or a panel that closes without a
835
+ * pick and without handing focus back to the field — has left too.
836
+ */
837
+ revealed = signal(false, /* @ts-ignore */
838
+ ...(ngDevMode ? [{ debugName: "revealed" }] : /* istanbul ignore next */ []));
839
+ /**
840
+ * Why the text in the field is not the bound value, once the picker is showing it —
841
+ * `null` while the text is the value, and `null` while the user is still typing.
842
+ *
843
+ * A refusal is raised on the keystroke that makes the text unreadable (`dateFormat`),
844
+ * out of `min` / `max` (`minDate` / `maxDate`) or rejected by `dateFilter`, and goes
845
+ * on show on Enter or when the user leaves the field — the same moment the border
846
+ * turns and the reason is announced, so a message rendered from this signal never
847
+ * disagrees with them. Once on show it follows the text, and it clears the moment the
848
+ * text becomes a date the picker commits, is emptied, or is replaced by a pick from
849
+ * the panel, a value written from outside, or a form reset. The value itself is never
850
+ * touched by a refusal.
851
+ *
852
+ * A bound form does NOT wait: `[formField]`, `[formControl]`, `formControlName` and
853
+ * `[(ngModel)]` see the refusal as a parse error from the keystroke, so the form is
854
+ * invalid the whole time the field holds text it could not use, and
855
+ * `<wr-form-field>` shows `message` by its own rule — once the control is touched or
856
+ * dirty. Read this through a template reference when the picker is bound with
857
+ * `[(value)]` alone and the reason should be on screen.
858
+ *
859
+ * @example
860
+ * ```html
861
+ * <wr-date-picker #due [(value)]="due" format="dd.MM.yyyy" />
862
+ * @if (due.inputError(); as error) {
863
+ * <small>{{ error.message }}</small>
864
+ * }
865
+ * ```
866
+ */
867
+ inputError = computed(() => (this.revealed() ? this.refusal() : null), /* @ts-ignore */
868
+ ...(ngDevMode ? [{ debugName: "inputError" }] : /* istanbul ignore next */ []));
869
+ /**
870
+ * Blanked briefly so Enter on an unchanged refusal is announced again. A live region
871
+ * does not repeat text it already holds, and blanking it for one render is not
872
+ * enough: both changes land in the same task, and the accessibility tree sees
873
+ * only the end state. The gap is a timer for that reason — the CDK's
874
+ * `LiveAnnouncer` clears its region the same way.
875
+ */
876
+ silenced = signal(false, /* @ts-ignore */
877
+ ...(ngDevMode ? [{ debugName: "silenced" }] : /* istanbul ignore next */ []));
878
+ silenceTimer;
879
+ statusId = this.inputField.statusId;
880
+ /**
881
+ * What the picker's own live region says: the refusal on show, unless a surrounding
882
+ * `<wr-form-field>` is already rendering a message for it — that message is a
883
+ * `role="alert"`, and a second voice would repeat it.
884
+ */
885
+ announcement = computed(() => {
886
+ const shown = this.inputError();
887
+ if (!shown || this.silenced() || this.inputField.saysAlready(shown))
888
+ return '';
889
+ return shown.message;
890
+ }, /* @ts-ignore */
891
+ ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
892
+ /**
893
+ * `aria-invalid` and `aria-describedby` for the text input, bound only with NO
894
+ * field around the picker. Inside one, `wrInput` writes both from the field — see
895
+ * `WrDateInputField` for why the picker feeds them through it rather than binding
896
+ * a second writer beside it.
897
+ */
898
+ ownAriaInvalid = computed(() => !this.inputField.outer && this.inputError() ? 'true' : null, /* @ts-ignore */
899
+ ...(ngDevMode ? [{ debugName: "ownAriaInvalid" }] : /* istanbul ignore next */ []));
900
+ ownDescribedBy = computed(() => !this.inputField.outer && this.announcement() ? this.statusId : null, /* @ts-ignore */
901
+ ...(ngDevMode ? [{ debugName: "ownDescribedBy" }] : /* istanbul ignore next */ []));
614
902
  classes = computed(() => {
615
903
  const parts = ['wr-date-picker', `wr-date-picker--${this.mode()}`];
616
904
  if (this.disabled())
617
905
  parts.push('wr-date-picker--disabled');
906
+ if (this.inputError())
907
+ parts.push('wr-date-picker--invalid');
618
908
  return parts.join(' ');
619
909
  }, /* @ts-ignore */
620
910
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
@@ -638,7 +928,10 @@ class WrDatePicker {
638
928
  * `[format]` / `[mode]` change makes it stale with the value untouched. */
639
929
  lastFormat = null;
640
930
  constructor() {
641
- this.destroyRef.onDestroy(() => this.dispose());
931
+ this.destroyRef.onDestroy(() => {
932
+ this.dispose();
933
+ clearTimeout(this.silenceTimer);
934
+ });
642
935
  // Mirror external writes to `value` (from `[formField]`, `[(value)]`, or a
643
936
  // classic-forms bridge) into the display text — this is the old
644
937
  // `writeValue` body. The echo of our own edits is skipped, and a
@@ -657,12 +950,20 @@ class WrDatePicker {
657
950
  const v = this.value();
658
951
  const fmt = this.resolvedFormat();
659
952
  untracked(() => {
953
+ // Our own commits hand `value` the very object `commitValue` remembered, so a
954
+ // DIFFERENT object holding the same date came from outside — a reset to a fresh
955
+ // copy of the day, say. That is no reason to re-render text the user is
956
+ // typing, but it does replace text the picker refused.
957
+ const outside = v !== this.lastValue;
660
958
  const echo = this.sameDate(v, this.lastValue) && fmt === this.lastFormat;
661
959
  this.lastValue = v;
662
960
  this.lastFormat = fmt;
663
- if (echo)
961
+ if (echo && !(outside && this.refusal()))
664
962
  return;
665
963
  this.text.set(v && this.adapter.isValid(v) ? this.adapter.format(v, fmt) : '');
964
+ // The text the refusal was about is gone — replaced by a value written from
965
+ // outside, or re-rendered in a new format.
966
+ this.refuse(null);
666
967
  });
667
968
  });
668
969
  // A panel that is up when `disabled` arrives has to go. `readonly` is
@@ -679,6 +980,8 @@ class WrDatePicker {
679
980
  this.closeOverlay();
680
981
  });
681
982
  });
983
+ // The input's field (see `WrDateInputField`) marks it invalid for the refusal on show.
984
+ this.inputField.describe(this.inputError);
682
985
  // While the calendar popover is open, push every valid typed value into it
683
986
  // so the displayed month follows the input live (the calendar snaps its
684
987
  // own viewDate to the bound `date`).
@@ -731,12 +1034,21 @@ class WrDatePicker {
731
1034
  this.text.set(raw);
732
1035
  if (!raw) {
733
1036
  this.commitValue(null);
1037
+ this.refuse(null);
734
1038
  return;
735
1039
  }
736
1040
  const parsed = this.adapter.parse(raw, this.resolvedFormat());
737
- if (parsed && this.adapter.isValid(parsed) && !this.isOutOfBounds(parsed)) {
738
- this.commitValue(this.withKeptDate(parsed));
1041
+ if (!parsed || !this.adapter.isValid(parsed)) {
1042
+ this.refuse(this.refusals.unreadable(this.resolvedFormat()));
1043
+ return;
1044
+ }
1045
+ const outOfBounds = this.outOfBounds(parsed);
1046
+ if (outOfBounds) {
1047
+ this.refuse(outOfBounds);
1048
+ return;
739
1049
  }
1050
+ this.commitValue(this.withKeptDate(parsed));
1051
+ this.refuse(null);
740
1052
  }
741
1053
  /**
742
1054
  * In `time` mode the typed string carries no date, so the adapter fills one in — and
@@ -752,9 +1064,58 @@ class WrDatePicker {
752
1064
  return parsed;
753
1065
  return this.adapter.setTime(current, this.adapter.getHours(parsed), this.adapter.getMinutes(parsed), this.adapter.getSeconds(parsed));
754
1066
  }
755
- onBlur() {
1067
+ onBlur(event) {
756
1068
  this.touch.emit();
1069
+ // A refused entry STAYS. Blur used to overwrite it with the last committed date,
1070
+ // which is how a refusal became invisible: `07.09.1994` typed into a field bounded
1071
+ // to 2026 simply turned back into the old date the moment the user moved on, with
1072
+ // nothing to say it had been refused or why. The text is what they have to
1073
+ // correct, and the value underneath was never changed. Focus moving INTO the open
1074
+ // panel is not leaving — a day click there is about to replace the text anyway;
1075
+ // `onPaneFocusOut` and `closeOverlay` decide when that visit ends.
1076
+ if (this.refusal()) {
1077
+ const next = event.relatedTarget;
1078
+ if (!next || !this.overlayRef?.overlayElement.contains(next))
1079
+ this.reveal();
1080
+ return;
1081
+ }
757
1082
  // Reformat to canonical on blur (cleans up `1/5/25` → `1/5/2025`).
1083
+ this.settleText();
1084
+ }
1085
+ /**
1086
+ * Enter is "this is the date". A date the picker committed is re-formatted
1087
+ * canonical and the panel closes; text it refused turns invalid on the spot —
1088
+ * border, `aria-invalid`, the reason announced — and emits `touch`, which is what
1089
+ * makes a surrounding `<wr-form-field>` show its message. The panel closes in both
1090
+ * cases: it opens directly below the field, which is exactly where that message
1091
+ * renders.
1092
+ *
1093
+ * Never `preventDefault`: Enter inside a `<form>` still submits it, and a form
1094
+ * bound to this picker is invalid while the refusal stands.
1095
+ */
1096
+ onEnter() {
1097
+ // Read before closing: a close can put the refusal on show itself.
1098
+ const wasShown = this.revealed();
1099
+ if (this.overlayRef)
1100
+ this.closeOverlay();
1101
+ const refusal = this.refusal();
1102
+ if (!refusal) {
1103
+ this.settleText();
1104
+ return;
1105
+ }
1106
+ this.touch.emit();
1107
+ if (wasShown) {
1108
+ // Already on show, so the live region already holds this sentence — and a live
1109
+ // region does not repeat text it already has. Blank it briefly so a second
1110
+ // Enter is answered rather than met with silence.
1111
+ this.silenced.set(true);
1112
+ clearTimeout(this.silenceTimer);
1113
+ this.silenceTimer = setTimeout(() => this.silenced.set(false), SILENCE_MS$1);
1114
+ }
1115
+ this.revealed.set(true);
1116
+ }
1117
+ /** Show the committed value canonically, or commit an emptied field as `null`. */
1118
+ settleText() {
758
1119
  const v = this.value();
759
1120
  if (v && this.adapter.isValid(v)) {
760
1121
  this.text.set(this.adapter.format(v, this.resolvedFormat()));
@@ -763,6 +1124,45 @@ class WrDatePicker {
763
1124
  this.commitValue(null);
764
1125
  }
765
1126
  }
1127
+ /**
1128
+ * Record why the current text was not committed, or that it was. Clearing a
1129
+ * refusal also takes it off show, so the next one waits for Enter or blur again.
1130
+ */
1131
+ refuse(next) {
1132
+ this.refusal.set(next);
1133
+ if (!next)
1134
+ this.revealed.set(false);
1135
+ this.reportToForm(next ? [next] : []);
1136
+ }
1137
+ /** Put a standing refusal on show — the user has left the field. */
1138
+ reveal() {
1139
+ if (this.refusal())
1140
+ this.revealed.set(true);
1141
+ }
1142
+ /**
1143
+ * The bound form was reset while the model stayed where it was — Angular has thrown
1144
+ * the parse error away, so the text it was about goes too, and the field shows the
1145
+ * value again. (A reset that MOVES the model reaches the text through the sync
1146
+ * effect instead, like any other outside write.)
1147
+ */
1148
+ onFormReset() {
1149
+ if (!this.refusal())
1150
+ return;
1151
+ const v = this.value();
1152
+ this.text.set(v && this.adapter.isValid(v) ? this.adapter.format(v, this.resolvedFormat()) : '');
1153
+ this.refuse(null);
1154
+ }
1155
+ /**
1156
+ * Focus leaving the open panel. Moving within it, or back to the text field, is
1157
+ * still the same visit; anywhere else — the page, the trigger, the next control — is
1158
+ * leaving, so a refusal that waited while focus went into the panel shows now.
1159
+ */
1160
+ onPaneFocusOut = (event) => {
1161
+ const next = event.relatedTarget;
1162
+ if (next && (this.overlayRef?.overlayElement.contains(next) || next === this.inputEl().nativeElement))
1163
+ return;
1164
+ this.reveal();
1165
+ };
766
1166
  /** Called by the input's click — opens the overlay if it isn't open already. */
767
1167
  openOnInput() {
768
1168
  if (this.disabled() || this.overlayRef)
@@ -795,6 +1195,10 @@ class WrDatePicker {
795
1195
  onFieldKey(event) {
796
1196
  if (this.disabled())
797
1197
  return;
1198
+ if (event.key === 'Enter') {
1199
+ this.onEnter();
1200
+ return;
1201
+ }
798
1202
  const vertical = event.key === 'ArrowDown' || event.key === 'ArrowUp';
799
1203
  if (!vertical)
800
1204
  return;
@@ -860,6 +1264,7 @@ class WrDatePicker {
860
1264
  pane.setAttribute('role', 'dialog');
861
1265
  pane.setAttribute('aria-modal', 'false');
862
1266
  pane.setAttribute('aria-label', this.resolvedPanelLabel());
1267
+ pane.addEventListener('focusout', this.onPaneFocusOut);
863
1268
  this.overlayOpen.set(true);
864
1269
  // Dispatch by mode — pick the panel + wire its emissions.
865
1270
  const m = this.mode();
@@ -950,6 +1355,7 @@ class WrDatePicker {
950
1355
  commit(next) {
951
1356
  this.text.set(this.adapter.format(next, this.resolvedFormat()));
952
1357
  this.commitValue(next);
1358
+ this.refuse(null);
953
1359
  }
954
1360
  /** Push a value to the model while remembering it, so the sync effect treats
955
1361
  * the resulting change as an echo and leaves the display text alone. */
@@ -981,9 +1387,18 @@ class WrDatePicker {
981
1387
  closeOverlay() {
982
1388
  const pane = this.overlayRef?.overlayElement;
983
1389
  const inside = !!pane && pane.contains(document.activeElement);
1390
+ // Taking the pane out of the document moves focus without the user doing anything,
1391
+ // so it must not read as focus leaving the panel.
1392
+ pane?.removeEventListener('focusout', this.onPaneFocusOut);
984
1393
  this.dispose();
985
1394
  if (inside)
986
1395
  this.restoreFocus();
1396
+ // A panel that closes is the end of the visit to it. Unless focus is back in the
1397
+ // field — Escape handing it there, or a close while the user never left it — the
1398
+ // user is somewhere else now, and a refusal that waited for the panel shows. A pick
1399
+ // has already cleared its refusal, so this only ever reaches text still refused.
1400
+ if (document.activeElement !== this.inputEl().nativeElement)
1401
+ this.reveal();
987
1402
  }
988
1403
  dispose() {
989
1404
  if (this.overlayRef) {
@@ -997,22 +1412,13 @@ class WrDatePicker {
997
1412
  this.autoFocusPanel = false;
998
1413
  }
999
1414
  // Helpers
1000
- isOutOfBounds(date) {
1415
+ outOfBounds(date) {
1001
1416
  if (this.mode() === 'time')
1002
- return false;
1003
- const min = this.min();
1004
- if (min && this.adapter.compareDate(date, min) < 0)
1005
- return true;
1006
- const max = this.max();
1007
- if (max && this.adapter.compareDate(date, max) > 0)
1008
- return true;
1417
+ return null;
1009
1418
  // Covers `dateFilter` as well as the bounds — the calendar disables filtered
1010
1419
  // days, so accepting them from the keyboard made the two entry paths
1011
1420
  // disagree. Same check, same reason, as `wr-date-range-picker`.
1012
- const filter = this.dateFilter();
1013
- if (filter && !filter(date))
1014
- return true;
1015
- return false;
1421
+ return this.refusals.outOfBounds(date, { min: this.min(), max: this.max(), filter: this.dateFilter() });
1016
1422
  }
1017
1423
  sameDate(a, b) {
1018
1424
  if (a === b)
@@ -1022,11 +1428,11 @@ class WrDatePicker {
1022
1428
  return a.getTime() === b.getTime();
1023
1429
  }
1024
1430
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDatePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
1025
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrDatePicker, isStandalone: true, selector: "wr-date-picker", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, panelAriaLabel: { classPropertyName: "panelAriaLabel", publicName: "panelAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { properties: { "class": "classes()" } }, viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["input"], descendants: true, isSignal: true }, { propertyName: "triggerEl", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: "<wr-input-group>\n <!--\n A plain textbox, per the APG date-picker-dialog pattern: `aria-haspopup` and\n `aria-expanded` are not permitted on `textbox` and belong to the trigger\n button below, which is the thing that opens the dialog.\n -->\n <input\n #input\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput()\"\n (keydown)=\"onFieldKey($event)\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur()\"\n />\n <button\n #trigger\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [attr.aria-controls]=\"overlayOpen() ? panelId : null\"\n [disabled]=\"disabled()\"\n (click)=\"toggleOverlay()\"\n >\n @if (isTime()) {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"5.75\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"7\" y1=\"7\" x2=\"7\" y2=\"3.75\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"7\" y1=\"7\" x2=\"9.5\" y2=\"7\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n </svg>\n } @else {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n }\n </button>\n</wr-input-group>\n", dependencies: [{ kind: "directive", type: WrInput, selector: "input[wrInput], textarea[wrInput]", inputs: ["size", "rounded"] }, { kind: "component", type: WrInputGroup, selector: "wr-input-group", inputs: ["rounded"] }, { kind: "directive", type: WrInputSuffix, selector: "[wrInputSuffix]" }], encapsulation: i0.ViewEncapsulation.None });
1431
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrDatePicker, isStandalone: true, selector: "wr-date-picker", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, panelAriaLabel: { classPropertyName: "panelAriaLabel", publicName: "panelAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { properties: { "class": "classes()" } }, providers: [WrDateInputField], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["input"], descendants: true, isSignal: true }, { propertyName: "triggerEl", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: "<wr-input-group>\n <!--\n A plain textbox, per the APG date-picker-dialog pattern: `aria-haspopup` and\n `aria-expanded` are not permitted on `textbox` and belong to the trigger\n button below, which is the thing that opens the dialog.\n -->\n <input\n #input\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n [attr.aria-invalid]=\"ownAriaInvalid()\"\n [attr.aria-describedby]=\"ownDescribedBy()\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput()\"\n (keydown)=\"onFieldKey($event)\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <button\n #trigger\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [attr.aria-controls]=\"overlayOpen() ? panelId : null\"\n [disabled]=\"disabled()\"\n (click)=\"toggleOverlay()\"\n >\n @if (isTime()) {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"5.75\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"7\" y1=\"7\" x2=\"7\" y2=\"3.75\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"7\" y1=\"7\" x2=\"9.5\" y2=\"7\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n </svg>\n } @else {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n }\n </button>\n</wr-input-group>\n<!--\n Why a typed entry was refused, for a screen reader: written when the refusal is\n shown (Enter, or leaving the field) and emptied when the text is corrected. Silent\n inside a <wr-form-field> bound to the same control, whose message says it instead.\n-->\n<span class=\"wr-date-picker__sr-only\" role=\"status\" [id]=\"statusId\">{{ announcement() }}</span>\n", dependencies: [{ kind: "directive", type: WrInput, selector: "input[wrInput], textarea[wrInput]", inputs: ["size", "rounded"] }, { kind: "component", type: WrInputGroup, selector: "wr-input-group", inputs: ["rounded"] }, { kind: "directive", type: WrInputSuffix, selector: "[wrInputSuffix]" }], viewProviders: [WR_DATE_INPUT_FIELD_VIEW_PROVIDER], encapsulation: i0.ViewEncapsulation.None });
1026
1432
  }
1027
1433
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDatePicker, decorators: [{
1028
1434
  type: Component,
1029
- args: [{ selector: 'wr-date-picker', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()' }, imports: [WrInput, WrInputGroup, WrInputSuffix], template: "<wr-input-group>\n <!--\n A plain textbox, per the APG date-picker-dialog pattern: `aria-haspopup` and\n `aria-expanded` are not permitted on `textbox` and belong to the trigger\n button below, which is the thing that opens the dialog.\n -->\n <input\n #input\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput()\"\n (keydown)=\"onFieldKey($event)\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur()\"\n />\n <button\n #trigger\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [attr.aria-controls]=\"overlayOpen() ? panelId : null\"\n [disabled]=\"disabled()\"\n (click)=\"toggleOverlay()\"\n >\n @if (isTime()) {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"5.75\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"7\" y1=\"7\" x2=\"7\" y2=\"3.75\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"7\" y1=\"7\" x2=\"9.5\" y2=\"7\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n </svg>\n } @else {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n }\n </button>\n</wr-input-group>\n" }]
1435
+ args: [{ selector: 'wr-date-picker', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()' }, imports: [WrInput, WrInputGroup, WrInputSuffix], providers: [WrDateInputField], viewProviders: [WR_DATE_INPUT_FIELD_VIEW_PROVIDER], template: "<wr-input-group>\n <!--\n A plain textbox, per the APG date-picker-dialog pattern: `aria-haspopup` and\n `aria-expanded` are not permitted on `textbox` and belong to the trigger\n button below, which is the thing that opens the dialog.\n -->\n <input\n #input\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n [attr.aria-invalid]=\"ownAriaInvalid()\"\n [attr.aria-describedby]=\"ownDescribedBy()\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput()\"\n (keydown)=\"onFieldKey($event)\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <button\n #trigger\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [attr.aria-controls]=\"overlayOpen() ? panelId : null\"\n [disabled]=\"disabled()\"\n (click)=\"toggleOverlay()\"\n >\n @if (isTime()) {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"5.75\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"7\" y1=\"7\" x2=\"7\" y2=\"3.75\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"7\" y1=\"7\" x2=\"9.5\" y2=\"7\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n </svg>\n } @else {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n }\n </button>\n</wr-input-group>\n<!--\n Why a typed entry was refused, for a screen reader: written when the refusal is\n shown (Enter, or leaving the field) and emptied when the text is corrected. Silent\n inside a <wr-form-field> bound to the same control, whose message says it instead.\n-->\n<span class=\"wr-date-picker__sr-only\" role=\"status\" [id]=\"statusId\">{{ announcement() }}</span>\n" }]
1030
1436
  }], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], showSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeconds", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], inputEl: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], triggerEl: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touch: [{ type: i0.Output, args: ["touch"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], panelAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelAriaLabel", required: false }] }] } });
1031
1437
 
1032
1438
  /**
@@ -1215,6 +1621,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
1215
1621
  */
1216
1622
  /** Per-instance popup ids, so `aria-controls` can point at one panel. */
1217
1623
  let rangePanelUid = 0;
1624
+ /** How long a live region stays blank before a repeated refusal is written back. */
1625
+ const SILENCE_MS = 100;
1218
1626
  /**
1219
1627
  * Date-range picker — two text inputs sharing one range calendar.
1220
1628
  *
@@ -1237,6 +1645,10 @@ let rangePanelUid = 0;
1237
1645
  * Either end may be `null` while the range is half-picked. Out-of-order ends
1238
1646
  * are swapped on commit, matching the calendar's own behaviour.
1239
1647
  *
1648
+ * Each end refuses text it cannot use exactly as `<wr-date-picker>` does — never
1649
+ * guessing, keeping the text, and showing the refusal on Enter or when focus
1650
+ * leaves that end. See {@link inputErrors}.
1651
+ *
1240
1652
  * @example
1241
1653
  * ```html
1242
1654
  * <!-- signal forms -->
@@ -1379,10 +1791,111 @@ class WrDateRangePicker {
1379
1791
  return this.isDateTime() ? this.panelLabelRangeDateTime() : this.panelLabelRange();
1380
1792
  }, /* @ts-ignore */
1381
1793
  ...(ngDevMode ? [{ debugName: "resolvedPanelLabel" }] : /* istanbul ignore next */ []));
1794
+ refusalBuilder = useDateInputRefusals();
1795
+ reportToForm = useFormParseErrors(this.value, () => this.onFormReset());
1796
+ inputField = inject(WrDateInputField);
1797
+ /** Why each end's text was not committed. */
1798
+ refusals = signal([null, null], /* @ts-ignore */
1799
+ ...(ngDevMode ? [{ debugName: "refusals" }] : /* istanbul ignore next */ []));
1800
+ /**
1801
+ * Every refusal standing, on show or not, each marked with its end — what a bound form
1802
+ * hears, from the keystroke.
1803
+ */
1804
+ raised = computed(() => {
1805
+ const [start, end] = this.refusals();
1806
+ const out = [];
1807
+ if (start)
1808
+ out.push({ ...start, end: 'start' });
1809
+ if (end)
1810
+ out.push({ ...end, end: 'end' });
1811
+ return out;
1812
+ }, /* @ts-ignore */
1813
+ ...(ngDevMode ? [{ debugName: "raised" }] : /* istanbul ignore next */ []));
1814
+ /**
1815
+ * Which ends have their refusal on show — raised on the keystroke, shown on Enter
1816
+ * or when focus leaves that end, for the reason `wr-date-picker` gives at the same
1817
+ * name: a date is unreadable for most of the time it takes to type one. Leaving
1818
+ * includes leaving by way of the open panel, as it does there.
1819
+ */
1820
+ revealed = signal([false, false], /* @ts-ignore */
1821
+ ...(ngDevMode ? [{ debugName: "revealed" }] : /* istanbul ignore next */ []));
1822
+ shownErrors = computed(() => {
1823
+ const [start, end] = this.refusals();
1824
+ const [showStart, showEnd] = this.revealed();
1825
+ return [showStart ? start : null, showEnd ? end : null];
1826
+ }, /* @ts-ignore */
1827
+ ...(ngDevMode ? [{ debugName: "shownErrors" }] : /* istanbul ignore next */ []));
1828
+ /**
1829
+ * Why the text in either field is not that end of the bound range, for the ends
1830
+ * whose refusal the picker is SHOWING — one entry per end, marked with the `end` it
1831
+ * came from, and empty while both fields hold their dates or the user is still
1832
+ * typing.
1833
+ *
1834
+ * An end's refusal is raised on the keystroke and goes on show on Enter or when
1835
+ * focus leaves that end — the moment its input turns invalid and the reason is
1836
+ * announced, so a message rendered from this never disagrees with them. It clears
1837
+ * the moment that end's text commits, is emptied, or is replaced by a pick from the
1838
+ * panel, a value written from outside, or a form reset.
1839
+ *
1840
+ * A bound form does NOT wait: it sees every refusal as a parse error from the
1841
+ * keystroke, so it is invalid the whole time either field holds text the picker
1842
+ * could not use — including while the OTHER end commits — and `<wr-form-field>`
1843
+ * shows `message` by its own rule. Read this through a template reference to put
1844
+ * the reason on screen for a picker bound with `[(value)]` alone.
1845
+ */
1846
+ inputErrors = computed(() => {
1847
+ const [showStart, showEnd] = this.revealed();
1848
+ return this.raised().filter(error => (error.end === 'start' ? showStart : showEnd));
1849
+ }, /* @ts-ignore */
1850
+ ...(ngDevMode ? [{ debugName: "inputErrors" }] : /* istanbul ignore next */ []));
1851
+ /**
1852
+ * Blanked briefly so Enter on an unchanged refusal is announced again. A live region
1853
+ * does not repeat text it already holds, and blanking it for one render is not
1854
+ * enough: both changes land in the same task, and the accessibility tree sees
1855
+ * only the end state. The gap is a timer for that reason — the CDK's
1856
+ * `LiveAnnouncer` clears its region the same way.
1857
+ */
1858
+ silenced = signal(false, /* @ts-ignore */
1859
+ ...(ngDevMode ? [{ debugName: "silenced" }] : /* istanbul ignore next */ []));
1860
+ silenceTimer;
1861
+ statusId = this.inputField.statusId;
1862
+ /**
1863
+ * The live region's text: every refusal on show that a surrounding `<wr-form-field>`
1864
+ * is not already rendering a message for, each sentence once.
1865
+ */
1866
+ announcement = computed(() => {
1867
+ if (this.silenced())
1868
+ return '';
1869
+ const messages = this.shownErrors()
1870
+ .filter((error) => !!error && !this.inputField.saysAlready(error))
1871
+ .map(error => error.message);
1872
+ return [...new Set(messages)].join(' ');
1873
+ }, /* @ts-ignore */
1874
+ ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
1875
+ /**
1876
+ * `aria-invalid` / `aria-describedby` for each input, where the picker binds them.
1877
+ *
1878
+ * The START input reads a surrounding field through `wrInput`, so inside one the
1879
+ * picker feeds its refusal through `WrDateInputField` rather than binding a second
1880
+ * writer beside `wrInput`, and binds these only with no field around it. The END
1881
+ * input is hidden from the field on purpose (see `wrDateRangeEnd`), so its `wrInput`
1882
+ * never writes either attribute and the picker always owns them: an unreadable end
1883
+ * date marks the end field, not only the start.
1884
+ */
1885
+ startAriaInvalid = computed(() => !this.inputField.outer && this.shownErrors()[0] ? 'true' : null, /* @ts-ignore */
1886
+ ...(ngDevMode ? [{ debugName: "startAriaInvalid" }] : /* istanbul ignore next */ []));
1887
+ endAriaInvalid = computed(() => (this.shownErrors()[1] ? 'true' : null), /* @ts-ignore */
1888
+ ...(ngDevMode ? [{ debugName: "endAriaInvalid" }] : /* istanbul ignore next */ []));
1889
+ startDescribedBy = computed(() => !this.inputField.outer && this.shownErrors()[0] && this.announcement() ? this.statusId : null, /* @ts-ignore */
1890
+ ...(ngDevMode ? [{ debugName: "startDescribedBy" }] : /* istanbul ignore next */ []));
1891
+ endDescribedBy = computed(() => this.shownErrors()[1] && this.announcement() ? this.statusId : null, /* @ts-ignore */
1892
+ ...(ngDevMode ? [{ debugName: "endDescribedBy" }] : /* istanbul ignore next */ []));
1382
1893
  classes = computed(() => {
1383
1894
  const parts = ['wr-date-range-picker', `wr-date-range-picker--${this.mode()}`];
1384
1895
  if (this.disabled())
1385
1896
  parts.push('wr-date-range-picker--disabled');
1897
+ if (this.shownErrors().some(Boolean))
1898
+ parts.push('wr-date-range-picker--invalid');
1386
1899
  return parts.join(' ');
1387
1900
  }, /* @ts-ignore */
1388
1901
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
@@ -1395,16 +1908,30 @@ class WrDateRangePicker {
1395
1908
  * the echo of our own edits and never reformat text mid-type. */
1396
1909
  lastValue = null;
1397
1910
  constructor() {
1398
- this.destroyRef.onDestroy(() => this.dispose());
1399
- // Mirror external writes to `value` into the two display texts.
1911
+ this.destroyRef.onDestroy(() => {
1912
+ this.dispose();
1913
+ clearTimeout(this.silenceTimer);
1914
+ });
1915
+ // The start input's field (see `WrDateInputField`) marks it invalid for the
1916
+ // start end's refusal on show; the end input carries its own.
1917
+ this.inputField.describe(computed(() => this.shownErrors()[0]));
1918
+ // Mirror external writes to `value` into the two display texts — and drop the
1919
+ // refusals, whose text that write has just replaced.
1400
1920
  effect(() => {
1401
1921
  const v = this.value();
1402
1922
  untracked(() => {
1403
- if (this.sameRange(v, this.lastValue))
1923
+ // An equal range in a DIFFERENT tuple came from outside — `commitRange` hands the
1924
+ // model the very tuple it remembered. It leaves text being typed alone, but it
1925
+ // does replace text the picker refused.
1926
+ const outside = v !== this.lastValue;
1927
+ const [refusedStart, refusedEnd] = this.refusals();
1928
+ if (this.sameRange(v, this.lastValue) && !(outside && (refusedStart || refusedEnd)))
1404
1929
  return;
1405
1930
  this.lastValue = v;
1406
1931
  this.startText.set(this.display(v?.[0] ?? null));
1407
1932
  this.endText.set(this.display(v?.[1] ?? null));
1933
+ this.refuse(0, null);
1934
+ this.refuse(1, null);
1408
1935
  });
1409
1936
  });
1410
1937
  // A panel that is up when `readonly` turns on has to go, and `disabled` is
@@ -1465,10 +1992,110 @@ class WrDateRangePicker {
1465
1992
  // typed and moved it into the field they were tabbing into, leaving the old
1466
1993
  // end date under their cursor. Same rule the time steppers follow.
1467
1994
  const [start, end] = this.commitRange(this.current(), { normalise: leaving });
1468
- // Reformat to canonical on blur (cleans up `1/5/25` → `1/5/2025`).
1995
+ // Reformat to canonical on blur (cleans up `1/5/25` → `1/5/2025`) — except a
1996
+ // refused end, whose text is what the user has to correct. Overwriting it with
1997
+ // the old date is how a refusal used to vanish without a word.
1998
+ const [refusedStart, refusedEnd] = this.refusals();
1999
+ if (!refusedStart)
2000
+ this.startText.set(this.display(start));
2001
+ if (!refusedEnd)
2002
+ this.endText.set(this.display(end));
2003
+ // Show the refusal of the end focus just left — both, once it leaves the pair.
2004
+ // Focus moving INTO the open panel is not leaving: a pick there replaces the text.
2005
+ if (next && this.overlayRef?.overlayElement.contains(next))
2006
+ return;
2007
+ const left = event.target === this.endEl().nativeElement ? 1 : 0;
2008
+ this.reveal(leaving ? [true, true] : left === 0 ? [true, false] : [false, true]);
2009
+ }
2010
+ /**
2011
+ * Enter in either field is "this is the range". Committed ends are settled — sorted
2012
+ * and re-formatted canonical — and the panel closes; a refused end turns invalid on
2013
+ * the spot, and `touch` is emitted so a surrounding `<wr-form-field>` shows its
2014
+ * message, which the panel would otherwise cover. Never `preventDefault`: Enter in a
2015
+ * `<form>` still submits it, and a bound form is invalid while a refusal stands.
2016
+ */
2017
+ onEnter() {
2018
+ // Read before closing: a close can put refusals on show itself.
2019
+ const [shownStart, shownEnd] = this.revealed();
2020
+ if (this.overlayRef)
2021
+ this.closeOverlay();
2022
+ const [start, end] = this.commitRange(this.current(), { normalise: true });
2023
+ const [refusedStart, refusedEnd] = this.refusals();
2024
+ if (!refusedStart)
2025
+ this.startText.set(this.display(start));
2026
+ if (!refusedEnd)
2027
+ this.endText.set(this.display(end));
2028
+ if (!refusedStart && !refusedEnd)
2029
+ return;
2030
+ this.touch.emit();
2031
+ if ((!refusedStart || shownStart) && (!refusedEnd || shownEnd)) {
2032
+ // Everything refused is already on show, so the live region already holds this
2033
+ // text and would not repeat it. Blank it briefly so Enter is answered.
2034
+ this.silenced.set(true);
2035
+ clearTimeout(this.silenceTimer);
2036
+ this.silenceTimer = setTimeout(() => this.silenced.set(false), SILENCE_MS);
2037
+ }
2038
+ this.reveal([true, true]);
2039
+ }
2040
+ /** Put the refusals of the given ends on show; an end with no refusal stays clear. */
2041
+ reveal([start, end]) {
2042
+ const [refusedStart, refusedEnd] = this.refusals();
2043
+ const [shownStart, shownEnd] = this.revealed();
2044
+ this.revealed.set([shownStart || (start && !!refusedStart), shownEnd || (end && !!refusedEnd)]);
2045
+ }
2046
+ /**
2047
+ * Record why one end's text was not committed, or that it was. Clearing a refusal
2048
+ * takes it off show too, so the next one waits for Enter or blur again.
2049
+ */
2050
+ refuse(end, next) {
2051
+ const current = this.refusals();
2052
+ if (current[end] === next)
2053
+ return;
2054
+ this.refusals.set(end === 0 ? [next, current[1]] : [current[0], next]);
2055
+ if (!next) {
2056
+ const [shownStart, shownEnd] = this.revealed();
2057
+ this.revealed.set(end === 0 ? [false, shownEnd] : [shownStart, false]);
2058
+ }
2059
+ this.reportToForm(this.raised());
2060
+ }
2061
+ /**
2062
+ * Put the refusals on show for wherever focus has gone: back into one field leaves
2063
+ * the OTHER end, and anywhere outside the pair leaves both.
2064
+ */
2065
+ revealFor(focus) {
2066
+ if (focus === this.startEl().nativeElement)
2067
+ this.reveal([false, true]);
2068
+ else if (focus === this.endEl().nativeElement)
2069
+ this.reveal([true, false]);
2070
+ else
2071
+ this.reveal([true, true]);
2072
+ }
2073
+ /**
2074
+ * The bound form was reset while the model stayed where it was — Angular has thrown
2075
+ * the parse errors away, so the text they were about goes too, and both fields show
2076
+ * the range again. (A reset that MOVES the model reaches the text through the sync
2077
+ * effect, like any other outside write.)
2078
+ */
2079
+ onFormReset() {
2080
+ const [refusedStart, refusedEnd] = this.refusals();
2081
+ if (!refusedStart && !refusedEnd)
2082
+ return;
2083
+ const [start, end] = this.current();
1469
2084
  this.startText.set(this.display(start));
1470
2085
  this.endText.set(this.display(end));
2086
+ this.refuse(0, null);
2087
+ this.refuse(1, null);
1471
2088
  }
2089
+ /**
2090
+ * Focus leaving the open panel. Moving within it is the same visit; anywhere else has
2091
+ * left whichever end focus is not going back to — see {@link revealFor}.
2092
+ */
2093
+ onPaneFocusOut = (event) => {
2094
+ const next = event.relatedTarget;
2095
+ if (next && this.overlayRef?.overlayElement.contains(next))
2096
+ return;
2097
+ this.revealFor(next);
2098
+ };
1472
2099
  /** Called by an input's click — opens the overlay if it isn't open already. */
1473
2100
  openOnInput(end) {
1474
2101
  if (this.disabled() || this.readonly() || this.overlayRef)
@@ -1501,6 +2128,10 @@ class WrDateRangePicker {
1501
2128
  onFieldKey(event, end) {
1502
2129
  if (this.disabled() || this.readonly())
1503
2130
  return;
2131
+ if (event.key === 'Enter') {
2132
+ this.onEnter();
2133
+ return;
2134
+ }
1504
2135
  const vertical = event.key === 'ArrowDown' || event.key === 'ArrowUp';
1505
2136
  if (!vertical)
1506
2137
  return;
@@ -1551,15 +2182,24 @@ class WrDateRangePicker {
1551
2182
  const [start, finish] = this.current();
1552
2183
  if (!raw) {
1553
2184
  this.commitRange(end === 0 ? [null, finish] : [start, null], { normalise: false });
2185
+ this.refuse(end, null);
1554
2186
  return;
1555
2187
  }
1556
2188
  const parsed = this.adapter.parse(raw, this.resolvedFormat());
1557
- if (!parsed || !this.adapter.isValid(parsed) || this.isOutOfBounds(parsed))
2189
+ if (!parsed || !this.adapter.isValid(parsed)) {
2190
+ this.refuse(end, this.refusalBuilder.unreadable(this.resolvedFormat()));
2191
+ return;
2192
+ }
2193
+ const outOfBounds = this.outOfBounds(parsed);
2194
+ if (outOfBounds) {
2195
+ this.refuse(end, outOfBounds);
1558
2196
  return;
2197
+ }
1559
2198
  // Never reorder mid-keystroke: a half-typed date can parse to an
1560
2199
  // out-of-order value, and swapping there would yank the text the user is
1561
2200
  // still typing over to the other input. Ordering is settled on blur.
1562
2201
  this.commitRange(end === 0 ? [parsed, finish] : [start, parsed], { normalise: false });
2202
+ this.refuse(end, null);
1563
2203
  }
1564
2204
  // Overlay
1565
2205
  openOverlay(openedFrom = null, autoFocus = false) {
@@ -1597,6 +2237,7 @@ class WrDateRangePicker {
1597
2237
  pane.setAttribute('role', 'dialog');
1598
2238
  pane.setAttribute('aria-modal', 'false');
1599
2239
  pane.setAttribute('aria-label', this.resolvedPanelLabel());
2240
+ pane.addEventListener('focusout', this.onPaneFocusOut);
1600
2241
  const ref = this.overlayRef.attach(new ComponentPortal(WrDateRangePanel));
1601
2242
  ref.setInput('value', this.value() ?? [null, null]);
1602
2243
  ref.setInput('autoFocus', this.autoFocusPanel);
@@ -1643,7 +2284,11 @@ class WrDateRangePicker {
1643
2284
  */
1644
2285
  closeOverlay() {
1645
2286
  const pane = this.overlayRef?.overlayElement;
1646
- const inside = !!pane && pane.contains(this.host.nativeElement.ownerDocument.activeElement);
2287
+ const doc = this.host.nativeElement.ownerDocument;
2288
+ const inside = !!pane && pane.contains(doc.activeElement);
2289
+ // Taking the pane out of the document moves focus without the user doing anything,
2290
+ // so it must not read as focus leaving the panel.
2291
+ pane?.removeEventListener('focusout', this.onPaneFocusOut);
1647
2292
  this.dispose();
1648
2293
  // The interaction is over, so settle the ordering an in-progress time edit
1649
2294
  // was allowed to leave inverted. Blur does the same for the text inputs;
@@ -1651,6 +2296,9 @@ class WrDateRangePicker {
1651
2296
  this.commitRange(this.current(), { normalise: true });
1652
2297
  if (inside)
1653
2298
  this.restoreFocus();
2299
+ // A panel that closes ends the visit to it: wherever focus is now, the ends it is
2300
+ // not in have been left. A pick has already cleared the refusals it replaced.
2301
+ this.revealFor(doc.activeElement);
1654
2302
  }
1655
2303
  dispose() {
1656
2304
  if (this.overlayRef) {
@@ -1671,6 +2319,7 @@ class WrDateRangePicker {
1671
2319
  * move their in-progress text into the other input.
1672
2320
  */
1673
2321
  commitRange(next, options) {
2322
+ const previous = this.current();
1674
2323
  const normalised = options.normalise ? this.normalise(next) : next;
1675
2324
  // Only write when a date actually moved, and never while `readonly`.
1676
2325
  //
@@ -1690,12 +2339,27 @@ class WrDateRangePicker {
1690
2339
  this.lastValue = normalised;
1691
2340
  this.value.set(normalised);
1692
2341
  }
2342
+ // An end whose DATE moved — a pick in the panel, a swap — has had its text
2343
+ // replaced by that date, so a refusal about the old text no longer applies.
2344
+ // An end that did not move keeps refused text as it is: committing the OTHER
2345
+ // end is no reason to throw away what the user is still correcting here.
2346
+ const [movedStart, movedEnd] = [0, 1].map(i => !this.sameDate(this.current()[i], previous[i]));
2347
+ if (movedStart)
2348
+ this.refuse(0, null);
2349
+ if (movedEnd)
2350
+ this.refuse(1, null);
2351
+ const [refusedStart, refusedEnd] = this.refusals();
2352
+ // Say the refusals again, whether or not either moved. Angular clears a control's
2353
+ // parse errors whenever its model changes, so committing ONE end used to leave the
2354
+ // form valid while the other end still held refused text — and able to submit the
2355
+ // old end under it.
2356
+ this.reportToForm(this.raised());
1693
2357
  // Only rewrite the text of an end whose date moved out from under it —
1694
2358
  // otherwise a half-typed date would be reformatted on every keystroke.
1695
- if (!this.sameDate(normalised[0], this.parseText(this.startText()))) {
2359
+ if (!refusedStart && !this.sameDate(normalised[0], this.parseText(this.startText()))) {
1696
2360
  this.startText.set(this.display(normalised[0]));
1697
2361
  }
1698
- if (!this.sameDate(normalised[1], this.parseText(this.endText()))) {
2362
+ if (!refusedEnd && !this.sameDate(normalised[1], this.parseText(this.endText()))) {
1699
2363
  this.endText.set(this.display(normalised[1]));
1700
2364
  }
1701
2365
  return normalised;
@@ -1733,21 +2397,16 @@ class WrDateRangePicker {
1733
2397
  return clock(a) - clock(b);
1734
2398
  }
1735
2399
  /**
1736
- * Whether a typed date must be rejected. Covers `dateFilter` as well as the
1737
- * bounds — the calendar disables filtered days, so accepting them from the
2400
+ * Why a typed date must be rejected, or `null`. Covers `dateFilter` as well as
2401
+ * the bounds — the calendar disables filtered days, so accepting them from the
1738
2402
  * keyboard would make the two entry paths disagree.
1739
2403
  */
1740
- isOutOfBounds(date) {
1741
- const min = this.minDate();
1742
- if (min && this.adapter.compareDate(date, min) < 0)
1743
- return true;
1744
- const max = this.maxDate();
1745
- if (max && this.adapter.compareDate(date, max) > 0)
1746
- return true;
1747
- const filter = this.dateFilter();
1748
- if (filter && !filter(date))
1749
- return true;
1750
- return false;
2404
+ outOfBounds(date) {
2405
+ return this.refusalBuilder.outOfBounds(date, {
2406
+ min: this.minDate(),
2407
+ max: this.maxDate(),
2408
+ filter: this.dateFilter(),
2409
+ });
1751
2410
  }
1752
2411
  sameDate(a, b) {
1753
2412
  if (a === b)
@@ -1773,11 +2432,11 @@ class WrDateRangePicker {
1773
2432
  return this.sameDate(aStart, bStart) && this.sameDate(aEnd, bEnd);
1774
2433
  }
1775
2434
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDateRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
1776
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrDateRangePicker, isStandalone: true, selector: "wr-date-range-picker", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", 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 }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, panelAriaLabel: { classPropertyName: "panelAriaLabel", publicName: "panelAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { properties: { "class": "classes()" } }, viewQueries: [{ propertyName: "startEl", first: true, predicate: ["startInput"], descendants: true, isSignal: true }, { propertyName: "endEl", first: true, predicate: ["endInput"], descendants: true, isSignal: true }, { propertyName: "triggerEl", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: "<wr-input-group>\n <input\n #startInput\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"startLabel()\"\n [value]=\"startText()\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput(0)\"\n (keydown)=\"onFieldKey($event, 0)\"\n (input)=\"onStartInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <span class=\"wr-date-range-picker__separator\" aria-hidden=\"true\">{{ separator() }}</span>\n <!--\n `wrDateRangeEnd` hides the surrounding `<wr-form-field>` from this input, so\n only the start one adopts its `controlId` \u2014 see the directive for why the\n second half of a range carries no id at all.\n -->\n <input\n #endInput\n wrInput\n wrDateRangeEnd\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"endLabel()\"\n [value]=\"endText()\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput(1)\"\n (keydown)=\"onFieldKey($event, 1)\"\n (input)=\"onEndInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <!--\n `readonly` refuses to open the popup, so the trigger is disabled with it \u2014\n otherwise it stays a focusable button advertising `aria-haspopup=\"dialog\"`\n for a dialog it will never show.\n -->\n <button\n #trigger\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [attr.aria-controls]=\"overlayOpen() ? panelId : null\"\n [disabled]=\"disabled() || readonly()\"\n (click)=\"toggleOverlay()\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n </button>\n</wr-input-group>\n", dependencies: [{ kind: "directive", type: WrDateRangeEndInput, selector: "input[wrDateRangeEnd]" }, { kind: "directive", type: WrInput, selector: "input[wrInput], textarea[wrInput]", inputs: ["size", "rounded"] }, { kind: "component", type: WrInputGroup, selector: "wr-input-group", inputs: ["rounded"] }, { kind: "directive", type: WrInputSuffix, selector: "[wrInputSuffix]" }], encapsulation: i0.ViewEncapsulation.None });
2435
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrDateRangePicker, isStandalone: true, selector: "wr-date-range-picker", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", 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 }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, panelAriaLabel: { classPropertyName: "panelAriaLabel", publicName: "panelAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { properties: { "class": "classes()" } }, providers: [WrDateInputField], viewQueries: [{ propertyName: "startEl", first: true, predicate: ["startInput"], descendants: true, isSignal: true }, { propertyName: "endEl", first: true, predicate: ["endInput"], descendants: true, isSignal: true }, { propertyName: "triggerEl", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: "<wr-input-group>\n <input\n #startInput\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"startLabel()\"\n [attr.aria-invalid]=\"startAriaInvalid()\"\n [attr.aria-describedby]=\"startDescribedBy()\"\n [value]=\"startText()\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput(0)\"\n (keydown)=\"onFieldKey($event, 0)\"\n (input)=\"onStartInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <span class=\"wr-date-range-picker__separator\" aria-hidden=\"true\">{{ separator() }}</span>\n <!--\n `wrDateRangeEnd` hides the surrounding `<wr-form-field>` from this input, so\n only the start one adopts its `controlId` \u2014 see the directive for why the\n second half of a range carries no id at all.\n -->\n <input\n #endInput\n wrInput\n wrDateRangeEnd\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"endLabel()\"\n [attr.aria-invalid]=\"endAriaInvalid()\"\n [attr.aria-describedby]=\"endDescribedBy()\"\n [value]=\"endText()\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput(1)\"\n (keydown)=\"onFieldKey($event, 1)\"\n (input)=\"onEndInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <!--\n `readonly` refuses to open the popup, so the trigger is disabled with it \u2014\n otherwise it stays a focusable button advertising `aria-haspopup=\"dialog\"`\n for a dialog it will never show.\n -->\n <button\n #trigger\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [attr.aria-controls]=\"overlayOpen() ? panelId : null\"\n [disabled]=\"disabled() || readonly()\"\n (click)=\"toggleOverlay()\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n </button>\n</wr-input-group>\n<!--\n Why a typed end was refused, for a screen reader \u2014 the same live region\n <wr-date-picker> carries, written when a refusal is shown and emptied when it is\n corrected. Silent inside a <wr-form-field> bound to the same control.\n-->\n<span class=\"wr-date-range-picker__sr-only\" role=\"status\" [id]=\"statusId\">{{ announcement() }}</span>\n", dependencies: [{ kind: "directive", type: WrDateRangeEndInput, selector: "input[wrDateRangeEnd]" }, { kind: "directive", type: WrInput, selector: "input[wrInput], textarea[wrInput]", inputs: ["size", "rounded"] }, { kind: "component", type: WrInputGroup, selector: "wr-input-group", inputs: ["rounded"] }, { kind: "directive", type: WrInputSuffix, selector: "[wrInputSuffix]" }], viewProviders: [WR_DATE_INPUT_FIELD_VIEW_PROVIDER], encapsulation: i0.ViewEncapsulation.None });
1777
2436
  }
1778
2437
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDateRangePicker, decorators: [{
1779
2438
  type: Component,
1780
- args: [{ selector: 'wr-date-range-picker', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()' }, imports: [WrDateRangeEndInput, WrInput, WrInputGroup, WrInputSuffix], template: "<wr-input-group>\n <input\n #startInput\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"startLabel()\"\n [value]=\"startText()\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput(0)\"\n (keydown)=\"onFieldKey($event, 0)\"\n (input)=\"onStartInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <span class=\"wr-date-range-picker__separator\" aria-hidden=\"true\">{{ separator() }}</span>\n <!--\n `wrDateRangeEnd` hides the surrounding `<wr-form-field>` from this input, so\n only the start one adopts its `controlId` \u2014 see the directive for why the\n second half of a range carries no id at all.\n -->\n <input\n #endInput\n wrInput\n wrDateRangeEnd\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"endLabel()\"\n [value]=\"endText()\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput(1)\"\n (keydown)=\"onFieldKey($event, 1)\"\n (input)=\"onEndInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <!--\n `readonly` refuses to open the popup, so the trigger is disabled with it \u2014\n otherwise it stays a focusable button advertising `aria-haspopup=\"dialog\"`\n for a dialog it will never show.\n -->\n <button\n #trigger\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [attr.aria-controls]=\"overlayOpen() ? panelId : null\"\n [disabled]=\"disabled() || readonly()\"\n (click)=\"toggleOverlay()\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n </button>\n</wr-input-group>\n" }]
2439
+ args: [{ selector: 'wr-date-range-picker', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()' }, imports: [WrDateRangeEndInput, WrInput, WrInputGroup, WrInputSuffix], providers: [WrDateInputField], viewProviders: [WR_DATE_INPUT_FIELD_VIEW_PROVIDER], template: "<wr-input-group>\n <input\n #startInput\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"startLabel()\"\n [attr.aria-invalid]=\"startAriaInvalid()\"\n [attr.aria-describedby]=\"startDescribedBy()\"\n [value]=\"startText()\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput(0)\"\n (keydown)=\"onFieldKey($event, 0)\"\n (input)=\"onStartInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <span class=\"wr-date-range-picker__separator\" aria-hidden=\"true\">{{ separator() }}</span>\n <!--\n `wrDateRangeEnd` hides the surrounding `<wr-form-field>` from this input, so\n only the start one adopts its `controlId` \u2014 see the directive for why the\n second half of a range carries no id at all.\n -->\n <input\n #endInput\n wrInput\n wrDateRangeEnd\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"endLabel()\"\n [attr.aria-invalid]=\"endAriaInvalid()\"\n [attr.aria-describedby]=\"endDescribedBy()\"\n [value]=\"endText()\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput(1)\"\n (keydown)=\"onFieldKey($event, 1)\"\n (input)=\"onEndInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <!--\n `readonly` refuses to open the popup, so the trigger is disabled with it \u2014\n otherwise it stays a focusable button advertising `aria-haspopup=\"dialog\"`\n for a dialog it will never show.\n -->\n <button\n #trigger\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [attr.aria-controls]=\"overlayOpen() ? panelId : null\"\n [disabled]=\"disabled() || readonly()\"\n (click)=\"toggleOverlay()\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n </button>\n</wr-input-group>\n<!--\n Why a typed end was refused, for a screen reader \u2014 the same live region\n <wr-date-picker> carries, written when a refusal is shown and emptied when it is\n corrected. Silent inside a <wr-form-field> bound to the same control.\n-->\n<span class=\"wr-date-range-picker__sr-only\" role=\"status\" [id]=\"statusId\">{{ announcement() }}</span>\n" }]
1781
2440
  }], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], startPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPlaceholder", required: false }] }], endPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "endPlaceholder", required: false }] }], separator: [{ type: i0.Input, args: [{ isSignal: true, alias: "separator", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], showSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeconds", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], startEl: [{ type: i0.ViewChild, args: ['startInput', { isSignal: true }] }], endEl: [{ type: i0.ViewChild, args: ['endInput', { isSignal: true }] }], triggerEl: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touch: [{ type: i0.Output, args: ["touch"] }], panelAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelAriaLabel", required: false }] }] } });
1782
2441
 
1783
2442
  /**