@cdevhub/ngx-tw 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
- package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
- package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
- package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
- package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
- package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
- package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
- package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
- package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
- package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
- package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
- package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
- package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
- package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
- package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
- package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
- package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
- package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
- package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
- package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
- package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
- package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
- package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
- package/package.json +265 -0
- package/theme/_base.css +394 -0
- package/theme/_dark.css +510 -0
- package/theme/_high-contrast.css +290 -0
- package/theme/_semantic.css +317 -0
- package/theme/_typography.css +9 -0
- package/theme/index.css +54 -0
- package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
- package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
- package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
- package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
- package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
- package/types/cdevhub-ngx-tw-button.d.ts +74 -0
- package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
- package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
- package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
- package/types/cdevhub-ngx-tw-card.d.ts +48 -0
- package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
- package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
- package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
- package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
- package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
- package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
- package/types/cdevhub-ngx-tw-core.d.ts +719 -0
- package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
- package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
- package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
- package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
- package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
- package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
- package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
- package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
- package/types/cdevhub-ngx-tw-input.d.ts +110 -0
- package/types/cdevhub-ngx-tw-item.d.ts +70 -0
- package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
- package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
- package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
- package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
- package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
- package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
- package/types/cdevhub-ngx-tw-select.d.ts +384 -0
- package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
- package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
- package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
- package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
- package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
- package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
- package/types/cdevhub-ngx-tw-split.d.ts +265 -0
- package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
- package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
- package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
- package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
- package/types/cdevhub-ngx-tw-table.d.ts +479 -0
- package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
- package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
- package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
- package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
- package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
- package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
- package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
- package/types/cdevhub-ngx-tw.d.ts +49 -0
|
@@ -0,0 +1,1344 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, input, booleanAttribute, model, output, inject, ElementRef, DestroyRef, viewChild, signal, linkedSignal, computed, effect, untracked, afterNextRender, isDevMode, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { NgControl, NgForm, FormGroupDirective, Validators } from '@angular/forms';
|
|
4
|
+
import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
|
|
5
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
|
+
import { merge } from 'rxjs';
|
|
7
|
+
import { tv } from 'tailwind-variants';
|
|
8
|
+
import { TW_ERROR_STATE_MATCHER, fieldMin, fieldMax, parseField, padTwo, to12h, appendDigit, isTerminalDigit, stepWithWrap, from12h, timeOfDaySeconds, clamp } from '@cdevhub/ngx-tw/core';
|
|
9
|
+
import { FormFieldControl, FormFieldComponent, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
|
|
10
|
+
import { DATE_ADAPTER } from '@cdevhub/ngx-tw/calendar';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Localized strings + ARIA announcements consumed by the time-picker.
|
|
14
|
+
*
|
|
15
|
+
* Override per-field via Angular DI — provide a custom instance, or supply a
|
|
16
|
+
* `Partial<TimePickerIntl>` through `provideTimePickerIntl`. Unset fields
|
|
17
|
+
* fall through to the English defaults shipped here.
|
|
18
|
+
*
|
|
19
|
+
* All members take primitives so consumers can write straight string
|
|
20
|
+
* literals or template helpers without depending on internal types.
|
|
21
|
+
*/
|
|
22
|
+
class TimePickerIntl {
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Group + field labels
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/** Accessible name for the fields group (the wrapper around hours/minutes/seconds). */
|
|
27
|
+
groupLabel = 'Time';
|
|
28
|
+
/** Accessible name for the hours field. */
|
|
29
|
+
hoursLabel = 'Hours';
|
|
30
|
+
/** Accessible name for the minutes field. */
|
|
31
|
+
minutesLabel = 'Minutes';
|
|
32
|
+
/** Accessible name for the seconds field. */
|
|
33
|
+
secondsLabel = 'Seconds';
|
|
34
|
+
/** Accessible name for the meridiem (AM/PM) radio group. */
|
|
35
|
+
meridiemGroupLabel = 'AM or PM';
|
|
36
|
+
/** Display + accessible name for the AM option. */
|
|
37
|
+
amLabel = 'AM';
|
|
38
|
+
/** Display + accessible name for the PM option. */
|
|
39
|
+
pmLabel = 'PM';
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Stepper + clear button labels
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
/** Aria label template for the up stepper. Receives the active field name (`'hours'`, `'minutes'`, `'seconds'`). */
|
|
44
|
+
increaseLabel(field) {
|
|
45
|
+
return `Increase ${field}`;
|
|
46
|
+
}
|
|
47
|
+
/** Aria label template for the down stepper. Receives the active field name (`'hours'`, `'minutes'`, `'seconds'`). */
|
|
48
|
+
decreaseLabel(field) {
|
|
49
|
+
return `Decrease ${field}`;
|
|
50
|
+
}
|
|
51
|
+
/** Aria label for the clear button. */
|
|
52
|
+
clearLabel = 'Clear time';
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Live announcements
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
/** Live-region message announced after the value is cleared. */
|
|
57
|
+
clearedAnnouncement = 'Time cleared';
|
|
58
|
+
/** Live-region message after a commit. Receives the pre-formatted time string (e.g. `'02:30 PM'`). */
|
|
59
|
+
selectedAnnouncement(formattedTime) {
|
|
60
|
+
return `${formattedTime} selected`;
|
|
61
|
+
}
|
|
62
|
+
/** Spoken text for an empty hour/minute/second field — used as `aria-valuetext` when the field is blank. */
|
|
63
|
+
emptyValueText = 'Empty';
|
|
64
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimePickerIntl, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
65
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimePickerIntl });
|
|
66
|
+
}
|
|
67
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimePickerIntl, decorators: [{
|
|
68
|
+
type: Injectable
|
|
69
|
+
}] });
|
|
70
|
+
/**
|
|
71
|
+
* Provides a custom `TimePickerIntl` instance. Place at any injector level —
|
|
72
|
+
* the time-picker resolves the closest one.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* providers: [
|
|
77
|
+
* provideTimePickerIntl({
|
|
78
|
+
* hoursLabel: 'Heures',
|
|
79
|
+
* minutesLabel: 'Minutes',
|
|
80
|
+
* meridiemGroupLabel: 'AM ou PM',
|
|
81
|
+
* }),
|
|
82
|
+
* ]
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
function provideTimePickerIntl(custom) {
|
|
86
|
+
return {
|
|
87
|
+
provide: TimePickerIntl,
|
|
88
|
+
useFactory: () => Object.assign(new TimePickerIntl(), custom),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── tv() config ───────────────────────────────────────────────────
|
|
93
|
+
const timePickerVariants = tv({
|
|
94
|
+
slots: {
|
|
95
|
+
root: 'inline-flex items-center text-fg transition-[color,border-color,box-shadow] duration-normal motion-reduce:transition-none',
|
|
96
|
+
fieldGroup: 'inline-flex items-center tabular-nums font-medium',
|
|
97
|
+
field: 'bg-transparent text-center outline-none border-0 p-0 m-0 text-fg placeholder:text-fg-subtle rounded-md caret-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
|
|
98
|
+
separator: 'text-fg-subtle select-none px-0.5',
|
|
99
|
+
stepperGroup: 'flex flex-col ml-0.5',
|
|
100
|
+
stepper: 'inline-flex items-center justify-center text-fg-muted hover:text-fg hover:bg-surface-muted rounded-md transition-colors duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 disabled:opacity-40 disabled:pointer-events-none',
|
|
101
|
+
stepperIcon: '',
|
|
102
|
+
meridiem: 'inline-flex items-center rounded-md border border-border overflow-hidden ml-2 shrink-0',
|
|
103
|
+
meridiemButton: 'font-medium text-fg-muted hover:bg-surface-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 transition-colors duration-normal motion-reduce:transition-none disabled:opacity-40 disabled:pointer-events-none',
|
|
104
|
+
clearButton: 'inline-flex items-center justify-center shrink-0 rounded-md text-fg-muted hover:text-fg hover:bg-surface-muted transition-colors duration-normal motion-reduce:transition-none size-6 ml-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
|
|
105
|
+
},
|
|
106
|
+
variants: {
|
|
107
|
+
size: {
|
|
108
|
+
xs: {
|
|
109
|
+
root: 'gap-1 text-xs',
|
|
110
|
+
// Numeric field width is sized to fit a 2-digit value at the current font scale. Off
|
|
111
|
+
// the standard width scale because the field is content-driven, not a layout primitive
|
|
112
|
+
// — see CLAUDE.md spacing scale rationale.
|
|
113
|
+
field: 'w-5 text-xs',
|
|
114
|
+
stepper: 'size-6',
|
|
115
|
+
stepperIcon: 'size-3',
|
|
116
|
+
meridiemButton: 'px-1.5 py-1 text-2xs',
|
|
117
|
+
},
|
|
118
|
+
sm: {
|
|
119
|
+
root: 'gap-1 text-sm',
|
|
120
|
+
// sm-density numeric field — sized to fit a 2-digit value at text-sm; see CLAUDE.md spacing scale rationale.
|
|
121
|
+
field: 'w-6 text-sm',
|
|
122
|
+
stepper: 'size-7',
|
|
123
|
+
stepperIcon: 'size-3',
|
|
124
|
+
meridiemButton: 'px-2 py-1 text-xs',
|
|
125
|
+
},
|
|
126
|
+
md: {
|
|
127
|
+
root: 'gap-1.5 text-sm',
|
|
128
|
+
// md-density numeric field — sized to fit a 2-digit value at text-sm; see CLAUDE.md spacing scale rationale.
|
|
129
|
+
field: 'w-7 text-sm',
|
|
130
|
+
stepper: 'size-7',
|
|
131
|
+
stepperIcon: 'size-3',
|
|
132
|
+
meridiemButton: 'px-2 py-1 text-xs',
|
|
133
|
+
},
|
|
134
|
+
lg: {
|
|
135
|
+
root: 'gap-1.5 text-base',
|
|
136
|
+
// lg-density numeric field — sized to fit a 2-digit value at text-base; see CLAUDE.md spacing scale rationale.
|
|
137
|
+
field: 'w-8 text-base',
|
|
138
|
+
stepper: 'size-8',
|
|
139
|
+
stepperIcon: 'size-4',
|
|
140
|
+
meridiemButton: 'px-2.5 py-1.5 text-sm',
|
|
141
|
+
},
|
|
142
|
+
xl: {
|
|
143
|
+
root: 'gap-2 text-base',
|
|
144
|
+
// xl-density numeric field — sized to fit a 2-digit value at text-base; see CLAUDE.md spacing scale rationale.
|
|
145
|
+
field: 'w-9 text-base',
|
|
146
|
+
stepper: 'size-8',
|
|
147
|
+
stepperIcon: 'size-4',
|
|
148
|
+
meridiemButton: 'px-3 py-1.5 text-sm',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
variant: {
|
|
152
|
+
default: {
|
|
153
|
+
root: 'rounded-md border border-border bg-surface px-3 py-2 hover:border-border-strong',
|
|
154
|
+
},
|
|
155
|
+
naked: {
|
|
156
|
+
root: 'bg-transparent border-0 rounded-none p-0 focus-within:outline-none',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
disabled: {
|
|
160
|
+
true: { root: 'opacity-50 pointer-events-none cursor-not-allowed' },
|
|
161
|
+
false: {},
|
|
162
|
+
},
|
|
163
|
+
focused: { true: {}, false: {} },
|
|
164
|
+
errorState: { true: {}, false: {} },
|
|
165
|
+
color: {
|
|
166
|
+
primary: {},
|
|
167
|
+
secondary: {},
|
|
168
|
+
accent: {},
|
|
169
|
+
neutral: {},
|
|
170
|
+
info: {},
|
|
171
|
+
success: {},
|
|
172
|
+
warning: {},
|
|
173
|
+
error: {},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
compoundVariants: [
|
|
177
|
+
{ variant: 'default', focused: true, color: 'primary', class: { root: 'border-primary-500' } },
|
|
178
|
+
{ variant: 'default', focused: true, color: 'secondary', class: { root: 'border-secondary-500' } },
|
|
179
|
+
{ variant: 'default', focused: true, color: 'accent', class: { root: 'border-accent-500' } },
|
|
180
|
+
{ variant: 'default', focused: true, color: 'neutral', class: { root: 'border-border-strong' } },
|
|
181
|
+
{ variant: 'default', focused: true, color: 'info', class: { root: 'border-info-500' } },
|
|
182
|
+
{ variant: 'default', focused: true, color: 'success', class: { root: 'border-success-500' } },
|
|
183
|
+
{ variant: 'default', focused: true, color: 'warning', class: { root: 'border-warning-500' } },
|
|
184
|
+
{ variant: 'default', focused: true, color: 'error', class: { root: 'border-error-500' } },
|
|
185
|
+
{ variant: 'default', errorState: true, class: { root: 'border-error-500' } },
|
|
186
|
+
],
|
|
187
|
+
defaultVariants: {
|
|
188
|
+
size: 'md',
|
|
189
|
+
variant: 'default',
|
|
190
|
+
disabled: false,
|
|
191
|
+
errorState: false,
|
|
192
|
+
focused: false,
|
|
193
|
+
color: 'primary',
|
|
194
|
+
},
|
|
195
|
+
}, { twMerge: true });
|
|
196
|
+
// ── Static active-state color lookup for the AM/PM meridiem button ─────────────
|
|
197
|
+
// Each entry is written out so Tailwind v4's static scanner picks the classes up.
|
|
198
|
+
// Mirrors the lookup-table pattern used by `checkbox.ts` and `radio.ts`.
|
|
199
|
+
const MERIDIEM_ACTIVE_COLOR = {
|
|
200
|
+
primary: 'bg-primary-500 text-on-primary hover:bg-primary-600',
|
|
201
|
+
secondary: 'bg-secondary-500 text-on-secondary hover:bg-secondary-600',
|
|
202
|
+
accent: 'bg-accent-500 text-on-accent hover:bg-accent-600',
|
|
203
|
+
neutral: 'bg-fg text-on-neutral hover:bg-fg/90',
|
|
204
|
+
info: 'bg-info-500 text-on-info hover:bg-info-600',
|
|
205
|
+
success: 'bg-success-500 text-on-success hover:bg-success-600',
|
|
206
|
+
warning: 'bg-warning-500 text-on-warning hover:bg-warning-600',
|
|
207
|
+
error: 'bg-error-500 text-on-error hover:bg-error-600',
|
|
208
|
+
};
|
|
209
|
+
let nextTimePickerId = 0;
|
|
210
|
+
/**
|
|
211
|
+
* Segmented time editor. Renders `HH : MM` (with an optional `: SS` and/or
|
|
212
|
+
* `AM/PM` toggle) and integrates with all three Angular form strategies via
|
|
213
|
+
* `ControlValueAccessor`. Also participates in `<tw-form-field>` through
|
|
214
|
+
* `FormFieldControl` — wrapping is optional.
|
|
215
|
+
*
|
|
216
|
+
* Value type is `D | null`. Every time operation flows through the injected
|
|
217
|
+
* `DateAdapter<D>`, so the component works with the default native adapter
|
|
218
|
+
* (`provideNativeDateAdapter()`) or any custom adapter a consumer ships.
|
|
219
|
+
*/
|
|
220
|
+
class TimePickerComponent extends FormFieldControl {
|
|
221
|
+
// ── Inputs ──
|
|
222
|
+
/** Id on the time-picker's host element. Auto-generated when not provided. Used by the form-field's `<label for>` attribute. */
|
|
223
|
+
idInput = input(undefined, { ...(ngDevMode ? { debugName: "idInput" } : /* istanbul ignore next */ {}), alias: 'id' });
|
|
224
|
+
/** When true, the whole component is disabled and every field sets `aria-disabled="true"`. Defaults to `false`. */
|
|
225
|
+
disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled',
|
|
226
|
+
transform: booleanAttribute });
|
|
227
|
+
/** When true, exposes `aria-required="true"`. Validators.required on a bound NgControl is also honoured. Defaults to `false`. */
|
|
228
|
+
requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required',
|
|
229
|
+
transform: booleanAttribute });
|
|
230
|
+
/** When true, blocks typing, stepping, and the AM/PM toggle — the value is still read-only visible. Defaults to `false`. */
|
|
231
|
+
readonlyInput = input(false, { ...(ngDevMode ? { debugName: "readonlyInput" } : /* istanbul ignore next */ {}), alias: 'readonly',
|
|
232
|
+
transform: booleanAttribute });
|
|
233
|
+
/** Controls field height, font size, and stepper density. Defaults to `'md'`. */
|
|
234
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
235
|
+
/** Semantic color used for the focused border and focus ring. Defaults to `'primary'`. */
|
|
236
|
+
color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
237
|
+
/** Visual style of the chrome. `'default'` draws its own border; `'naked'` strips chrome. Auto-resolves to `'naked'` inside `<tw-form-field>`. */
|
|
238
|
+
variant = input(undefined, ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
239
|
+
/** `'12h'` renders 1–12 hours with an AM/PM toggle; `'24h'` renders 00–23. Defaults to `'24h'`. */
|
|
240
|
+
format = input('24h', ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
|
|
241
|
+
/** When true, renders a seconds field after minutes. Defaults to `false`. */
|
|
242
|
+
showSeconds = input(false, ...(ngDevMode ? [{ debugName: "showSeconds" }] : /* istanbul ignore next */ []));
|
|
243
|
+
/** Amount to add/subtract when stepping hours. Defaults to `1`. */
|
|
244
|
+
hourStep = input(1, ...(ngDevMode ? [{ debugName: "hourStep" }] : /* istanbul ignore next */ []));
|
|
245
|
+
/** Amount to add/subtract when stepping minutes. Defaults to `1`. */
|
|
246
|
+
minuteStep = input(1, ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
|
|
247
|
+
/** Amount to add/subtract when stepping seconds. Defaults to `1`. */
|
|
248
|
+
secondStep = input(1, ...(ngDevMode ? [{ debugName: "secondStep" }] : /* istanbul ignore next */ []));
|
|
249
|
+
/** Earliest accepted time-of-day. Values earlier than this set `errorState`. Defaults to `null`. Note: when `showSeconds` is `false`, the seconds component of any value still participates in the range comparison — supply `minTime` with zeroed seconds to match the 2-field display. */
|
|
250
|
+
minTime = input(null, ...(ngDevMode ? [{ debugName: "minTime" }] : /* istanbul ignore next */ []));
|
|
251
|
+
/** Latest accepted time-of-day. Values later than this set `errorState`. Defaults to `null`. Note: when `showSeconds` is `false`, the seconds component of any value still participates in the range comparison — supply `maxTime` with zeroed seconds to match the 2-field display. */
|
|
252
|
+
maxTime = input(null, ...(ngDevMode ? [{ debugName: "maxTime" }] : /* istanbul ignore next */ []));
|
|
253
|
+
/** Date portion used when the user types a time while `value` is `null`. Defaults to today. */
|
|
254
|
+
referenceDate = input(null, ...(ngDevMode ? [{ debugName: "referenceDate" }] : /* istanbul ignore next */ []));
|
|
255
|
+
/** Placeholder shown in each field when empty. Defaults to `'--'`. */
|
|
256
|
+
placeholder = input(undefined, ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
257
|
+
/** Whether to render the up/down stepper column. Defaults to `true`. */
|
|
258
|
+
// TRUE-default: pointer users expect inline up/down chevrons next to a numeric editor — without them the picker reads as a static display, and consumers must rebuild stepping for every instance.
|
|
259
|
+
showSteppers = input(true, ...(ngDevMode ? [{ debugName: "showSteppers" }] : /* istanbul ignore next */ []));
|
|
260
|
+
/** Whether to render the clear affordance when a value is set. Defaults to `true`. */
|
|
261
|
+
// TRUE-default: matches `<tw-date-picker>` and `<tw-input>` clear behaviour; without the inline clear, every form has to wire its own reset surface for an obvious affordance.
|
|
262
|
+
showClear = input(true, ...(ngDevMode ? [{ debugName: "showClear" }] : /* istanbul ignore next */ []));
|
|
263
|
+
/** Accessible label for the clear button. Defaults to `'Clear time'`. */
|
|
264
|
+
clearLabel = input('Clear time', ...(ngDevMode ? [{ debugName: "clearLabel" }] : /* istanbul ignore next */ []));
|
|
265
|
+
/** Per-instance override of the `ErrorStateMatcher`. */
|
|
266
|
+
errorStateMatcher = input(undefined, ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
|
|
267
|
+
/** Accessible name for the fields group. Required when no visible label is supplied. Alias: `aria-label`. */
|
|
268
|
+
ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
|
|
269
|
+
/** ID of an external element that labels the group. Alias: `aria-labelledby`. */
|
|
270
|
+
ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
|
|
271
|
+
/** Consumer-supplied `aria-describedby` ids. The form-field preserves these when merging hint/error ids. Alias: `aria-describedby`. */
|
|
272
|
+
userAriaDescribedByInput = input(undefined, { ...(ngDevMode ? { debugName: "userAriaDescribedByInput" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
|
|
273
|
+
// ── Models ──
|
|
274
|
+
/** Two-way bound current time. `null` when empty. Setting programmatically updates the fields without firing `onChange`. */
|
|
275
|
+
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
276
|
+
// ── Outputs ──
|
|
277
|
+
/** Fires on every keystroke, stepper press, or AM/PM toggle — before the new value is committed. */
|
|
278
|
+
timeInput = output();
|
|
279
|
+
/** Fires after a committed change — digit + auto-advance, stepper, meridiem toggle, clear, or programmatic write. */
|
|
280
|
+
timeChange = output();
|
|
281
|
+
// ── Injected deps ──
|
|
282
|
+
adapter = inject(DATE_ADAPTER);
|
|
283
|
+
focusMonitor = inject(FocusMonitor);
|
|
284
|
+
liveAnnouncer = inject(LiveAnnouncer);
|
|
285
|
+
elementRef = inject((ElementRef));
|
|
286
|
+
destroyRef = inject(DestroyRef);
|
|
287
|
+
formField = inject(FormFieldComponent, { optional: true });
|
|
288
|
+
ngControl = inject(NgControl, { optional: true, self: true });
|
|
289
|
+
parentForm = inject(NgForm, { optional: true });
|
|
290
|
+
parentFormGroup = inject(FormGroupDirective, { optional: true });
|
|
291
|
+
defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
|
|
292
|
+
/** @internal — exposed for the template; consumers configure via `provideTimePickerIntl()`. */
|
|
293
|
+
intl = inject(TimePickerIntl, { optional: true }) ?? new TimePickerIntl();
|
|
294
|
+
// ── View refs ──
|
|
295
|
+
hourInputRef = viewChild.required('hourInput');
|
|
296
|
+
minuteInputRef = viewChild.required('minuteInput');
|
|
297
|
+
secondInputRef = viewChild('secondInput', ...(ngDevMode ? [{ debugName: "secondInputRef" }] : /* istanbul ignore next */ []));
|
|
298
|
+
// ── Identity ──
|
|
299
|
+
uid = nextTimePickerId++;
|
|
300
|
+
/** @internal */
|
|
301
|
+
hostId = `tw-time-picker-${this.uid}`;
|
|
302
|
+
// ── Internal state ──
|
|
303
|
+
/** @internal raw display text per field — always the numerals the user sees. */
|
|
304
|
+
hourText = signal('', ...(ngDevMode ? [{ debugName: "hourText" }] : /* istanbul ignore next */ []));
|
|
305
|
+
/** @internal */
|
|
306
|
+
minuteText = signal('', ...(ngDevMode ? [{ debugName: "minuteText" }] : /* istanbul ignore next */ []));
|
|
307
|
+
/** @internal */
|
|
308
|
+
secondText = signal('', ...(ngDevMode ? [{ debugName: "secondText" }] : /* istanbul ignore next */ []));
|
|
309
|
+
/** @internal */
|
|
310
|
+
meridiem = signal('AM', ...(ngDevMode ? [{ debugName: "meridiem" }] : /* istanbul ignore next */ []));
|
|
311
|
+
/** @internal */
|
|
312
|
+
focusedField = signal(null, ...(ngDevMode ? [{ debugName: "focusedField" }] : /* istanbul ignore next */ []));
|
|
313
|
+
/** @internal */
|
|
314
|
+
internalValue = linkedSignal(() => this.value(), ...(ngDevMode ? [{ debugName: "internalValue" }] : /* istanbul ignore next */ []));
|
|
315
|
+
cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
316
|
+
describedByIdsSignal = signal([], ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
|
|
317
|
+
focusedSignal = signal(false, ...(ngDevMode ? [{ debugName: "focusedSignal" }] : /* istanbul ignore next */ []));
|
|
318
|
+
_ngControlRev = signal(0, ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
|
|
319
|
+
_formSubmitRev = signal(0, ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
|
|
320
|
+
/** When true, an effect should not rewrite field text from the external value. */
|
|
321
|
+
editing = false;
|
|
322
|
+
onChange = () => { };
|
|
323
|
+
onTouched = () => { };
|
|
324
|
+
// ── Derived state ──
|
|
325
|
+
/** @internal */
|
|
326
|
+
isDisabled = computed(() => {
|
|
327
|
+
this._ngControlRev();
|
|
328
|
+
return this.disabledInput() || this.cvaDisabled() || !!this.ngControl?.disabled;
|
|
329
|
+
}, ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
330
|
+
/** @internal */
|
|
331
|
+
isEmpty = computed(() => this.internalValue() === null, ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
|
|
332
|
+
/** @internal Effective variant — auto-naked when wrapped in tw-form-field. */
|
|
333
|
+
resolvedVariant = computed(() => this.variant() ?? (this.formField ? 'naked' : 'default'), ...(ngDevMode ? [{ debugName: "resolvedVariant" }] : /* istanbul ignore next */ []));
|
|
334
|
+
/** @internal */
|
|
335
|
+
hourMin = computed(() => fieldMin('hour', this.format()), ...(ngDevMode ? [{ debugName: "hourMin" }] : /* istanbul ignore next */ []));
|
|
336
|
+
/** @internal */
|
|
337
|
+
hourMax = computed(() => fieldMax('hour', this.format()), ...(ngDevMode ? [{ debugName: "hourMax" }] : /* istanbul ignore next */ []));
|
|
338
|
+
/** @internal */
|
|
339
|
+
hourDisplay = computed(() => this.hourText(), ...(ngDevMode ? [{ debugName: "hourDisplay" }] : /* istanbul ignore next */ []));
|
|
340
|
+
/** @internal */
|
|
341
|
+
hourValueNow = computed(() => {
|
|
342
|
+
const n = parseField(this.hourText());
|
|
343
|
+
return n === null ? null : n;
|
|
344
|
+
}, ...(ngDevMode ? [{ debugName: "hourValueNow" }] : /* istanbul ignore next */ []));
|
|
345
|
+
/** @internal */
|
|
346
|
+
hourValueText = computed(() => {
|
|
347
|
+
const n = this.hourValueNow();
|
|
348
|
+
if (n === null)
|
|
349
|
+
return this.intl.emptyValueText;
|
|
350
|
+
const meridiemLabel = this.meridiem() === 'AM' ? this.intl.amLabel : this.intl.pmLabel;
|
|
351
|
+
const suffix = this.format() === '12h' ? ` ${meridiemLabel}` : '';
|
|
352
|
+
return `${padTwo(n)}${suffix}`;
|
|
353
|
+
}, ...(ngDevMode ? [{ debugName: "hourValueText" }] : /* istanbul ignore next */ []));
|
|
354
|
+
/** @internal */
|
|
355
|
+
minuteValueNow = computed(() => parseField(this.minuteText()), ...(ngDevMode ? [{ debugName: "minuteValueNow" }] : /* istanbul ignore next */ []));
|
|
356
|
+
/** @internal */
|
|
357
|
+
minuteValueText = computed(() => {
|
|
358
|
+
const n = this.minuteValueNow();
|
|
359
|
+
return n === null ? this.intl.emptyValueText : padTwo(n);
|
|
360
|
+
}, ...(ngDevMode ? [{ debugName: "minuteValueText" }] : /* istanbul ignore next */ []));
|
|
361
|
+
/** @internal */
|
|
362
|
+
secondValueNow = computed(() => parseField(this.secondText()), ...(ngDevMode ? [{ debugName: "secondValueNow" }] : /* istanbul ignore next */ []));
|
|
363
|
+
/** @internal */
|
|
364
|
+
secondValueText = computed(() => {
|
|
365
|
+
const n = this.secondValueNow();
|
|
366
|
+
return n === null ? this.intl.emptyValueText : padTwo(n);
|
|
367
|
+
}, ...(ngDevMode ? [{ debugName: "secondValueText" }] : /* istanbul ignore next */ []));
|
|
368
|
+
/** @internal */
|
|
369
|
+
groupAriaLabel = computed(() => this.ariaLabel() ?? this.intl.groupLabel, ...(ngDevMode ? [{ debugName: "groupAriaLabel" }] : /* istanbul ignore next */ []));
|
|
370
|
+
/** @internal Label of the currently focused field, used by stepper aria-labels. */
|
|
371
|
+
focusedFieldLabel = computed(() => {
|
|
372
|
+
const f = this.focusedField();
|
|
373
|
+
if (f === 'minute')
|
|
374
|
+
return this.intl.minutesLabel.toLowerCase();
|
|
375
|
+
if (f === 'second')
|
|
376
|
+
return this.intl.secondsLabel.toLowerCase();
|
|
377
|
+
return this.intl.hoursLabel.toLowerCase();
|
|
378
|
+
}, ...(ngDevMode ? [{ debugName: "focusedFieldLabel" }] : /* istanbul ignore next */ []));
|
|
379
|
+
/** @internal */
|
|
380
|
+
describedBy = computed(() => {
|
|
381
|
+
const extra = this.describedByIdsSignal();
|
|
382
|
+
const user = this.userAriaDescribedByInput();
|
|
383
|
+
const merged = [...extra];
|
|
384
|
+
if (user) {
|
|
385
|
+
for (const id of user.split(/\s+/))
|
|
386
|
+
if (id)
|
|
387
|
+
merged.push(id);
|
|
388
|
+
}
|
|
389
|
+
return merged.length ? merged.join(' ') : '';
|
|
390
|
+
}, ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
391
|
+
// ── Range / error state ──
|
|
392
|
+
rangeError = signal(false, ...(ngDevMode ? [{ debugName: "rangeError" }] : /* istanbul ignore next */ []));
|
|
393
|
+
// ── tv() output ──
|
|
394
|
+
variantResult = computed(() => timePickerVariants({
|
|
395
|
+
size: this.size(),
|
|
396
|
+
variant: this.resolvedVariant(),
|
|
397
|
+
disabled: this.isDisabled(),
|
|
398
|
+
errorState: this.errorState(),
|
|
399
|
+
focused: this.focusedSignal(),
|
|
400
|
+
color: this.color(),
|
|
401
|
+
}), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
|
|
402
|
+
/** @internal */ rootClasses = computed(() => this.variantResult().root(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
|
|
403
|
+
/** @internal */ fieldGroupClasses = computed(() => this.variantResult().fieldGroup(), ...(ngDevMode ? [{ debugName: "fieldGroupClasses" }] : /* istanbul ignore next */ []));
|
|
404
|
+
/** @internal */ fieldClasses = computed(() => this.variantResult().field(), ...(ngDevMode ? [{ debugName: "fieldClasses" }] : /* istanbul ignore next */ []));
|
|
405
|
+
/** @internal */ separatorClasses = computed(() => this.variantResult().separator(), ...(ngDevMode ? [{ debugName: "separatorClasses" }] : /* istanbul ignore next */ []));
|
|
406
|
+
/** @internal */ stepperGroupClasses = computed(() => this.variantResult().stepperGroup(), ...(ngDevMode ? [{ debugName: "stepperGroupClasses" }] : /* istanbul ignore next */ []));
|
|
407
|
+
/** @internal */ stepperClasses = computed(() => this.variantResult().stepper(), ...(ngDevMode ? [{ debugName: "stepperClasses" }] : /* istanbul ignore next */ []));
|
|
408
|
+
/** @internal */ stepperIconClasses = computed(() => this.variantResult().stepperIcon(), ...(ngDevMode ? [{ debugName: "stepperIconClasses" }] : /* istanbul ignore next */ []));
|
|
409
|
+
/** @internal */ meridiemClasses = computed(() => this.variantResult().meridiem(), ...(ngDevMode ? [{ debugName: "meridiemClasses" }] : /* istanbul ignore next */ []));
|
|
410
|
+
/** @internal */ clearButtonClasses = computed(() => this.variantResult().clearButton(), ...(ngDevMode ? [{ debugName: "clearButtonClasses" }] : /* istanbul ignore next */ []));
|
|
411
|
+
/** @internal */
|
|
412
|
+
meridiemButtonClasses(which) {
|
|
413
|
+
const base = this.variantResult().meridiemButton();
|
|
414
|
+
const active = this.meridiem() === which;
|
|
415
|
+
return active ? `${base} ${MERIDIEM_ACTIVE_COLOR[this.color()]}` : base;
|
|
416
|
+
}
|
|
417
|
+
// ── FormFieldControl impl ──
|
|
418
|
+
/** @internal */ id = computed(() => this.idInput() ?? this.hostId, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
|
|
419
|
+
/** @internal */ focused = this.focusedSignal.asReadonly();
|
|
420
|
+
/** @internal */ empty = this.isEmpty;
|
|
421
|
+
/** @internal */ disabled = this.isDisabled;
|
|
422
|
+
/** @internal */ required = computed(() => {
|
|
423
|
+
this._ngControlRev();
|
|
424
|
+
if (this.requiredInput())
|
|
425
|
+
return true;
|
|
426
|
+
return !!this.ngControl?.control?.hasValidator(Validators.required);
|
|
427
|
+
}, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
428
|
+
/** @internal */
|
|
429
|
+
errorState = computed(() => {
|
|
430
|
+
this._ngControlRev();
|
|
431
|
+
this._formSubmitRev();
|
|
432
|
+
this.focusedSignal();
|
|
433
|
+
if (this.rangeError())
|
|
434
|
+
return true;
|
|
435
|
+
const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
|
|
436
|
+
const form = this.parentFormGroup ??
|
|
437
|
+
this.parentForm;
|
|
438
|
+
return matcher.isErrorState(this.ngControl?.control ?? null, form);
|
|
439
|
+
}, ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
|
|
440
|
+
/** @internal */ controlType = 'time-picker';
|
|
441
|
+
/** @internal */
|
|
442
|
+
userAriaDescribedBy = computed(() => this.userAriaDescribedByInput(), ...(ngDevMode ? [{ debugName: "userAriaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
443
|
+
// ── Constructor ──
|
|
444
|
+
constructor() {
|
|
445
|
+
super();
|
|
446
|
+
if (this.ngControl) {
|
|
447
|
+
this.ngControl.valueAccessor = this;
|
|
448
|
+
}
|
|
449
|
+
// Sync the field text whenever the underlying value changes (programmatic).
|
|
450
|
+
effect(() => {
|
|
451
|
+
const v = this.internalValue();
|
|
452
|
+
const fmt = this.format();
|
|
453
|
+
const showS = this.showSeconds();
|
|
454
|
+
untracked(() => {
|
|
455
|
+
if (this.editing)
|
|
456
|
+
return;
|
|
457
|
+
if (v === null || !this.adapter.isValid(v)) {
|
|
458
|
+
this.hourText.set('');
|
|
459
|
+
this.minuteText.set('');
|
|
460
|
+
this.secondText.set('');
|
|
461
|
+
this.rangeError.set(false);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const h24 = this.adapter.getHours(v);
|
|
465
|
+
const m = this.adapter.getMinutes(v);
|
|
466
|
+
const s = this.adapter.getSeconds(v);
|
|
467
|
+
if (fmt === '12h') {
|
|
468
|
+
this.hourText.set(padTwo(to12h(h24)));
|
|
469
|
+
this.meridiem.set(h24 >= 12 ? 'PM' : 'AM');
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
this.hourText.set(padTwo(h24));
|
|
473
|
+
}
|
|
474
|
+
this.minuteText.set(padTwo(m));
|
|
475
|
+
this.secondText.set(showS ? padTwo(s) : '');
|
|
476
|
+
this.rangeError.set(!this.isInRange(v));
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
// Re-render the hour text when switching format (e.g., 24h → 12h) or showSeconds.
|
|
480
|
+
effect(() => {
|
|
481
|
+
this.format();
|
|
482
|
+
this.showSeconds();
|
|
483
|
+
untracked(() => {
|
|
484
|
+
const v = this.internalValue();
|
|
485
|
+
if (v === null || !this.adapter.isValid(v))
|
|
486
|
+
return;
|
|
487
|
+
const h24 = this.adapter.getHours(v);
|
|
488
|
+
if (this.format() === '12h') {
|
|
489
|
+
this.hourText.set(padTwo(to12h(h24)));
|
|
490
|
+
this.meridiem.set(h24 >= 12 ? 'PM' : 'AM');
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
this.hourText.set(padTwo(h24));
|
|
494
|
+
}
|
|
495
|
+
if (this.showSeconds() && !this.secondText()) {
|
|
496
|
+
this.secondText.set(padTwo(this.adapter.getSeconds(v)));
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
// Dev-mode warning for missing accessible name.
|
|
501
|
+
afterNextRender(() => {
|
|
502
|
+
if (!isDevMode())
|
|
503
|
+
return;
|
|
504
|
+
const hasLabel = !!this.ariaLabel() ||
|
|
505
|
+
!!this.ariaLabelledby() ||
|
|
506
|
+
!!this.formField?.labelChild();
|
|
507
|
+
if (!hasLabel) {
|
|
508
|
+
console.warn('[tw-time-picker] The time-picker has no accessible name. Set aria-label, aria-labelledby, or wrap the component in a <tw-form-field> with a <label twLabel>.');
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
// Dev-mode warning for bogus steps.
|
|
512
|
+
afterNextRender(() => {
|
|
513
|
+
if (!isDevMode())
|
|
514
|
+
return;
|
|
515
|
+
for (const [name, step] of [
|
|
516
|
+
['hourStep', this.hourStep()],
|
|
517
|
+
['minuteStep', this.minuteStep()],
|
|
518
|
+
['secondStep', this.secondStep()],
|
|
519
|
+
]) {
|
|
520
|
+
if (step < 1 || !Number.isFinite(step)) {
|
|
521
|
+
console.warn(`[tw-time-picker] ${name}=${step} is invalid. Using 1 instead.`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
// Host focus tracking.
|
|
526
|
+
const monitorSub = this.focusMonitor
|
|
527
|
+
.monitor(this.elementRef, true)
|
|
528
|
+
.subscribe((origin) => {
|
|
529
|
+
const wasFocused = this.focusedSignal();
|
|
530
|
+
this.focusedSignal.set(!!origin);
|
|
531
|
+
if (wasFocused && !origin) {
|
|
532
|
+
this.focusedField.set(null);
|
|
533
|
+
this.editing = false;
|
|
534
|
+
this.normalizeEmptyFields();
|
|
535
|
+
this.onTouched();
|
|
536
|
+
this._ngControlRev.update((n) => n + 1);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
this.destroyRef.onDestroy(() => {
|
|
540
|
+
monitorSub.unsubscribe();
|
|
541
|
+
this.focusMonitor.stopMonitoring(this.elementRef);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
ngOnInit() {
|
|
545
|
+
const ctrl = this.ngControl?.control;
|
|
546
|
+
if (ctrl) {
|
|
547
|
+
const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
|
|
548
|
+
if (streams.length) {
|
|
549
|
+
merge(...streams)
|
|
550
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
551
|
+
.subscribe(() => this._ngControlRev.update((n) => n + 1));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
|
|
555
|
+
if (submit) {
|
|
556
|
+
submit
|
|
557
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
558
|
+
.subscribe(() => this._formSubmitRev.update((n) => n + 1));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// ── Public imperative API ──
|
|
562
|
+
/** Moves focus to the hours field. */
|
|
563
|
+
focus() {
|
|
564
|
+
this.hourInputRef().nativeElement.focus();
|
|
565
|
+
}
|
|
566
|
+
/** Clears the value and emits `timeChange` with `source: 'clear'`. */
|
|
567
|
+
clear() {
|
|
568
|
+
this.commit(null, 'clear');
|
|
569
|
+
}
|
|
570
|
+
// ── Field interactions ──
|
|
571
|
+
/** @internal */
|
|
572
|
+
onFieldFocus(field) {
|
|
573
|
+
this.focusedField.set(field);
|
|
574
|
+
this.editing = true;
|
|
575
|
+
const el = this.getFieldEl(field);
|
|
576
|
+
if (el)
|
|
577
|
+
el.select();
|
|
578
|
+
}
|
|
579
|
+
/** @internal */
|
|
580
|
+
onFieldBlur(_field) {
|
|
581
|
+
this.editing = false;
|
|
582
|
+
this.normalizeEmptyFields();
|
|
583
|
+
}
|
|
584
|
+
/** @internal */
|
|
585
|
+
onBeforeInput(event, field) {
|
|
586
|
+
const evt = event;
|
|
587
|
+
const data = evt.data ?? '';
|
|
588
|
+
// Let browsers drive Backspace / Delete natively.
|
|
589
|
+
if (evt.inputType === 'deleteContentBackward' || evt.inputType === 'deleteContentForward') {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
// Replace default text insertion with our buffered digit logic.
|
|
593
|
+
if (evt.inputType === 'insertText' && /^\d$/.test(data)) {
|
|
594
|
+
evt.preventDefault();
|
|
595
|
+
this.typeDigit(field, data);
|
|
596
|
+
}
|
|
597
|
+
else if (evt.inputType === 'insertText') {
|
|
598
|
+
// Reject non-digit insertions.
|
|
599
|
+
evt.preventDefault();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/** @internal Handle native delete events (Backspace/Delete). */
|
|
603
|
+
onInputEvent(event, field) {
|
|
604
|
+
const evt = event;
|
|
605
|
+
const el = event.target;
|
|
606
|
+
if (evt.inputType === 'deleteContentBackward' || evt.inputType === 'deleteContentForward') {
|
|
607
|
+
this.editing = true;
|
|
608
|
+
const currentLen = this.getFieldText(field).length;
|
|
609
|
+
const nativeText = el.value.replace(/\D/g, '').slice(0, 2);
|
|
610
|
+
if (nativeText.length < currentLen || nativeText.length === 0) {
|
|
611
|
+
this.setFieldText(field, nativeText);
|
|
612
|
+
this.commitFromFields('input', field);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/** @internal */
|
|
617
|
+
onFieldKeydown(event, field) {
|
|
618
|
+
if (this.isDisabled() || this.readonlyInput())
|
|
619
|
+
return;
|
|
620
|
+
const key = event.key;
|
|
621
|
+
switch (key) {
|
|
622
|
+
case 'ArrowUp':
|
|
623
|
+
event.preventDefault();
|
|
624
|
+
this.stepField(field, 1, event.shiftKey);
|
|
625
|
+
return;
|
|
626
|
+
case 'ArrowDown':
|
|
627
|
+
event.preventDefault();
|
|
628
|
+
this.stepField(field, -1, event.shiftKey);
|
|
629
|
+
return;
|
|
630
|
+
case 'ArrowLeft':
|
|
631
|
+
if (this.isFieldCaretAtStart(field)) {
|
|
632
|
+
event.preventDefault();
|
|
633
|
+
this.moveFocus(field, -1);
|
|
634
|
+
}
|
|
635
|
+
return;
|
|
636
|
+
case 'ArrowRight':
|
|
637
|
+
if (this.isFieldCaretAtEnd(field)) {
|
|
638
|
+
event.preventDefault();
|
|
639
|
+
this.moveFocus(field, 1);
|
|
640
|
+
}
|
|
641
|
+
return;
|
|
642
|
+
case 'Home':
|
|
643
|
+
event.preventDefault();
|
|
644
|
+
this.setFieldText(field, padTwo(this.fieldMinFor(field)));
|
|
645
|
+
this.commitFromFields('input', field);
|
|
646
|
+
return;
|
|
647
|
+
case 'End':
|
|
648
|
+
event.preventDefault();
|
|
649
|
+
this.setFieldText(field, padTwo(this.fieldMaxFor(field)));
|
|
650
|
+
this.commitFromFields('input', field);
|
|
651
|
+
return;
|
|
652
|
+
case 'Delete':
|
|
653
|
+
case 'Backspace':
|
|
654
|
+
// Let native input drive these; `onInputEvent` syncs state.
|
|
655
|
+
return;
|
|
656
|
+
default:
|
|
657
|
+
if (key.length === 1 && !/\d/.test(key)) {
|
|
658
|
+
event.preventDefault();
|
|
659
|
+
}
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/** @internal */
|
|
664
|
+
setMeridiem(m) {
|
|
665
|
+
if (this.isDisabled() || this.readonlyInput())
|
|
666
|
+
return;
|
|
667
|
+
if (this.meridiem() === m)
|
|
668
|
+
return;
|
|
669
|
+
this.meridiem.set(m);
|
|
670
|
+
this.commitFromFields('meridiem', 'meridiem');
|
|
671
|
+
}
|
|
672
|
+
/** @internal */
|
|
673
|
+
onMeridiemKeydown(event, m) {
|
|
674
|
+
if (this.isDisabled() || this.readonlyInput())
|
|
675
|
+
return;
|
|
676
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === ' ' || event.key === 'Enter') {
|
|
677
|
+
event.preventDefault();
|
|
678
|
+
this.setMeridiem(m === 'AM' ? 'PM' : 'AM');
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/** @internal */
|
|
682
|
+
onStepperMouseDown(event) {
|
|
683
|
+
// Keep focus on the current field when clicking the stepper.
|
|
684
|
+
event.preventDefault();
|
|
685
|
+
}
|
|
686
|
+
/** @internal */
|
|
687
|
+
onStepperClick(direction) {
|
|
688
|
+
const field = this.focusedField() ?? 'hour';
|
|
689
|
+
if (field === 'meridiem') {
|
|
690
|
+
this.setMeridiem(this.meridiem() === 'AM' ? 'PM' : 'AM');
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this.stepField(field, direction, false);
|
|
694
|
+
// Restore focus to the field we stepped.
|
|
695
|
+
const el = this.getFieldEl(field);
|
|
696
|
+
el?.focus();
|
|
697
|
+
}
|
|
698
|
+
/** @internal */
|
|
699
|
+
onClearClick(event) {
|
|
700
|
+
event.stopPropagation();
|
|
701
|
+
event.preventDefault();
|
|
702
|
+
this.clear();
|
|
703
|
+
}
|
|
704
|
+
// ── Helpers ──
|
|
705
|
+
typeDigit(field, digit) {
|
|
706
|
+
if (field === 'meridiem')
|
|
707
|
+
return;
|
|
708
|
+
const max = this.fieldMaxFor(field);
|
|
709
|
+
const current = this.getFieldText(field);
|
|
710
|
+
const next = appendDigit(current, digit, max);
|
|
711
|
+
const advance = isTerminalDigit(current, digit, max) || next.length === 2;
|
|
712
|
+
this.setFieldText(field, next);
|
|
713
|
+
this.commitFromFields('input', field);
|
|
714
|
+
if (advance)
|
|
715
|
+
this.moveFocus(field, 1);
|
|
716
|
+
}
|
|
717
|
+
stepField(field, direction, shift) {
|
|
718
|
+
if (field === 'meridiem') {
|
|
719
|
+
this.setMeridiem(this.meridiem() === 'AM' ? 'PM' : 'AM');
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const step = this.stepFor(field) * (shift ? 2 : 1);
|
|
723
|
+
const min = this.fieldMinFor(field);
|
|
724
|
+
const max = this.fieldMaxFor(field);
|
|
725
|
+
const current = parseField(this.getFieldText(field)) ?? min;
|
|
726
|
+
const next = stepWithWrap(current, step, direction, min, max);
|
|
727
|
+
this.setFieldText(field, padTwo(next));
|
|
728
|
+
this.commitFromFields('stepper', field);
|
|
729
|
+
}
|
|
730
|
+
commitFromFields(source, field) {
|
|
731
|
+
const hourRaw = parseField(this.hourText());
|
|
732
|
+
const minuteRaw = parseField(this.minuteText());
|
|
733
|
+
const secondRaw = this.showSeconds() ? parseField(this.secondText()) : 0;
|
|
734
|
+
this.timeInput.emit({
|
|
735
|
+
field,
|
|
736
|
+
rawText: field === 'meridiem' ? this.meridiem() : this.getFieldText(field),
|
|
737
|
+
parsed: this.buildValueIfComplete(hourRaw, minuteRaw, secondRaw),
|
|
738
|
+
});
|
|
739
|
+
if (hourRaw === null || minuteRaw === null || (this.showSeconds() && secondRaw === null)) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const h24 = this.toCanonicalHour(hourRaw);
|
|
743
|
+
if (h24 === null)
|
|
744
|
+
return;
|
|
745
|
+
const base = this.resolveBaseDate();
|
|
746
|
+
const next = this.adapter.withTime(base, h24, minuteRaw, this.showSeconds() ? (secondRaw ?? 0) : 0);
|
|
747
|
+
if (!this.adapter.isValid(next))
|
|
748
|
+
return;
|
|
749
|
+
this.commit(next, source);
|
|
750
|
+
}
|
|
751
|
+
buildValueIfComplete(h, m, s) {
|
|
752
|
+
if (h === null || m === null)
|
|
753
|
+
return null;
|
|
754
|
+
if (this.showSeconds() && s === null)
|
|
755
|
+
return null;
|
|
756
|
+
const h24 = this.toCanonicalHour(h);
|
|
757
|
+
if (h24 === null)
|
|
758
|
+
return null;
|
|
759
|
+
const base = this.resolveBaseDate();
|
|
760
|
+
const built = this.adapter.withTime(base, h24, m, this.showSeconds() ? (s ?? 0) : 0);
|
|
761
|
+
return this.adapter.isValid(built) ? built : null;
|
|
762
|
+
}
|
|
763
|
+
toCanonicalHour(display) {
|
|
764
|
+
if (this.format() === '12h') {
|
|
765
|
+
if (display < 1 || display > 12)
|
|
766
|
+
return null;
|
|
767
|
+
return from12h(display, this.meridiem());
|
|
768
|
+
}
|
|
769
|
+
if (display < 0 || display > 23)
|
|
770
|
+
return null;
|
|
771
|
+
return display;
|
|
772
|
+
}
|
|
773
|
+
resolveBaseDate() {
|
|
774
|
+
const current = this.internalValue();
|
|
775
|
+
if (current !== null && this.adapter.isValid(current))
|
|
776
|
+
return current;
|
|
777
|
+
const reference = this.referenceDate();
|
|
778
|
+
if (reference !== null && this.adapter.isValid(reference))
|
|
779
|
+
return reference;
|
|
780
|
+
return this.adapter.today();
|
|
781
|
+
}
|
|
782
|
+
commit(next, source) {
|
|
783
|
+
const previous = this.internalValue();
|
|
784
|
+
this.internalValue.set(next);
|
|
785
|
+
this.value.set(next);
|
|
786
|
+
this.rangeError.set(next !== null ? !this.isInRange(next) : false);
|
|
787
|
+
if (source !== 'programmatic') {
|
|
788
|
+
this.onChange(next);
|
|
789
|
+
this.onTouched();
|
|
790
|
+
if (next === null) {
|
|
791
|
+
this.liveAnnouncer.announce(this.intl.clearedAnnouncement, 'polite');
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
this.liveAnnouncer.announce(this.intl.selectedAnnouncement(this.formatAnnouncement(next)), 'polite');
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
this.timeChange.emit({ value: next, previousValue: previous, source });
|
|
798
|
+
}
|
|
799
|
+
formatAnnouncement(v) {
|
|
800
|
+
const h24 = this.adapter.getHours(v);
|
|
801
|
+
const m = this.adapter.getMinutes(v);
|
|
802
|
+
const s = this.adapter.getSeconds(v);
|
|
803
|
+
const secPart = this.showSeconds() ? `:${padTwo(s)}` : '';
|
|
804
|
+
if (this.format() === '12h') {
|
|
805
|
+
const meridiemLabel = h24 >= 12 ? this.intl.pmLabel : this.intl.amLabel;
|
|
806
|
+
return `${padTwo(to12h(h24))}:${padTwo(m)}${secPart} ${meridiemLabel}`;
|
|
807
|
+
}
|
|
808
|
+
return `${padTwo(h24)}:${padTwo(m)}${secPart}`;
|
|
809
|
+
}
|
|
810
|
+
isInRange(v) {
|
|
811
|
+
const min = this.minTime();
|
|
812
|
+
const max = this.maxTime();
|
|
813
|
+
if (!min && !max)
|
|
814
|
+
return true;
|
|
815
|
+
const vSecs = timeOfDaySeconds(this.adapter.getHours(v), this.adapter.getMinutes(v), this.adapter.getSeconds(v));
|
|
816
|
+
if (min && this.adapter.isValid(min)) {
|
|
817
|
+
const minSecs = timeOfDaySeconds(this.adapter.getHours(min), this.adapter.getMinutes(min), this.adapter.getSeconds(min));
|
|
818
|
+
if (vSecs < minSecs)
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
if (max && this.adapter.isValid(max)) {
|
|
822
|
+
const maxSecs = timeOfDaySeconds(this.adapter.getHours(max), this.adapter.getMinutes(max), this.adapter.getSeconds(max));
|
|
823
|
+
if (vSecs > maxSecs)
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
normalizeEmptyFields() {
|
|
829
|
+
// Zero-pad single-digit entries once the user leaves the component so the
|
|
830
|
+
// display always settles to "HH : MM".
|
|
831
|
+
for (const field of ['hour', 'minute', 'second']) {
|
|
832
|
+
if (field === 'second' && !this.showSeconds())
|
|
833
|
+
continue;
|
|
834
|
+
const raw = this.getFieldText(field);
|
|
835
|
+
if (raw.length === 1) {
|
|
836
|
+
this.setFieldText(field, padTwo(Number(raw)));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
moveFocus(field, direction) {
|
|
841
|
+
const order = this.showSeconds()
|
|
842
|
+
? ['hour', 'minute', 'second']
|
|
843
|
+
: ['hour', 'minute'];
|
|
844
|
+
const idx = order.indexOf(field);
|
|
845
|
+
if (idx === -1)
|
|
846
|
+
return;
|
|
847
|
+
const nextIdx = clamp(idx + direction, 0, order.length - 1);
|
|
848
|
+
if (nextIdx === idx)
|
|
849
|
+
return;
|
|
850
|
+
const el = this.getFieldEl(order[nextIdx]);
|
|
851
|
+
el?.focus();
|
|
852
|
+
}
|
|
853
|
+
isFieldCaretAtStart(field) {
|
|
854
|
+
const el = this.getFieldEl(field);
|
|
855
|
+
if (!el)
|
|
856
|
+
return true;
|
|
857
|
+
return (el.selectionStart ?? 0) === 0;
|
|
858
|
+
}
|
|
859
|
+
isFieldCaretAtEnd(field) {
|
|
860
|
+
const el = this.getFieldEl(field);
|
|
861
|
+
if (!el)
|
|
862
|
+
return true;
|
|
863
|
+
const end = el.selectionEnd ?? 0;
|
|
864
|
+
return end === el.value.length;
|
|
865
|
+
}
|
|
866
|
+
getFieldEl(field) {
|
|
867
|
+
switch (field) {
|
|
868
|
+
case 'hour':
|
|
869
|
+
return this.hourInputRef().nativeElement;
|
|
870
|
+
case 'minute':
|
|
871
|
+
return this.minuteInputRef().nativeElement;
|
|
872
|
+
case 'second':
|
|
873
|
+
return this.secondInputRef()?.nativeElement ?? null;
|
|
874
|
+
default:
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
getFieldText(field) {
|
|
879
|
+
switch (field) {
|
|
880
|
+
case 'hour':
|
|
881
|
+
return this.hourText();
|
|
882
|
+
case 'minute':
|
|
883
|
+
return this.minuteText();
|
|
884
|
+
case 'second':
|
|
885
|
+
return this.secondText();
|
|
886
|
+
default:
|
|
887
|
+
return '';
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
setFieldText(field, next) {
|
|
891
|
+
this.editing = true;
|
|
892
|
+
switch (field) {
|
|
893
|
+
case 'hour':
|
|
894
|
+
this.hourText.set(next);
|
|
895
|
+
return;
|
|
896
|
+
case 'minute':
|
|
897
|
+
this.minuteText.set(next);
|
|
898
|
+
return;
|
|
899
|
+
case 'second':
|
|
900
|
+
this.secondText.set(next);
|
|
901
|
+
return;
|
|
902
|
+
default:
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
stepFor(field) {
|
|
907
|
+
switch (field) {
|
|
908
|
+
case 'hour':
|
|
909
|
+
return Math.max(1, Math.floor(this.hourStep()));
|
|
910
|
+
case 'minute':
|
|
911
|
+
return Math.max(1, Math.floor(this.minuteStep()));
|
|
912
|
+
case 'second':
|
|
913
|
+
return Math.max(1, Math.floor(this.secondStep()));
|
|
914
|
+
default:
|
|
915
|
+
return 1;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
fieldMinFor(field) {
|
|
919
|
+
if (field === 'hour')
|
|
920
|
+
return fieldMin('hour', this.format());
|
|
921
|
+
return 0;
|
|
922
|
+
}
|
|
923
|
+
fieldMaxFor(field) {
|
|
924
|
+
if (field === 'hour')
|
|
925
|
+
return fieldMax('hour', this.format());
|
|
926
|
+
return 59;
|
|
927
|
+
}
|
|
928
|
+
// ── ControlValueAccessor ──
|
|
929
|
+
writeValue(value) {
|
|
930
|
+
if (value === null || value === undefined || value === '') {
|
|
931
|
+
const previous = this.internalValue();
|
|
932
|
+
this.internalValue.set(null);
|
|
933
|
+
this.value.set(null);
|
|
934
|
+
this.rangeError.set(false);
|
|
935
|
+
this.hourText.set('');
|
|
936
|
+
this.minuteText.set('');
|
|
937
|
+
this.secondText.set('');
|
|
938
|
+
this.timeChange.emit({ value: null, previousValue: previous, source: 'programmatic' });
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const coerced = this.adapter.deserialize(value);
|
|
942
|
+
if (coerced === null || !this.adapter.isValid(coerced)) {
|
|
943
|
+
this.internalValue.set(null);
|
|
944
|
+
this.value.set(null);
|
|
945
|
+
this.hourText.set('');
|
|
946
|
+
this.minuteText.set('');
|
|
947
|
+
this.secondText.set('');
|
|
948
|
+
this.rangeError.set(true);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const previous = this.internalValue();
|
|
952
|
+
this.internalValue.set(coerced);
|
|
953
|
+
this.value.set(coerced);
|
|
954
|
+
this.rangeError.set(!this.isInRange(coerced));
|
|
955
|
+
this.timeChange.emit({ value: coerced, previousValue: previous, source: 'programmatic' });
|
|
956
|
+
}
|
|
957
|
+
registerOnChange(fn) {
|
|
958
|
+
this.onChange = fn;
|
|
959
|
+
}
|
|
960
|
+
registerOnTouched(fn) {
|
|
961
|
+
this.onTouched = fn;
|
|
962
|
+
}
|
|
963
|
+
setDisabledState(isDisabled) {
|
|
964
|
+
this.cvaDisabled.set(isDisabled);
|
|
965
|
+
}
|
|
966
|
+
// ── FormFieldControl methods ──
|
|
967
|
+
/** @internal */
|
|
968
|
+
setDescribedByIds(ids) {
|
|
969
|
+
this.describedByIdsSignal.set([...ids]);
|
|
970
|
+
}
|
|
971
|
+
/** @internal */
|
|
972
|
+
onContainerClick(event) {
|
|
973
|
+
if (this.isDisabled())
|
|
974
|
+
return;
|
|
975
|
+
const target = event.target;
|
|
976
|
+
// Clicks that land on an actual field or button should settle focus naturally.
|
|
977
|
+
if (target && (target.tagName === 'INPUT' || target.tagName === 'BUTTON'))
|
|
978
|
+
return;
|
|
979
|
+
this.hourInputRef().nativeElement.focus();
|
|
980
|
+
}
|
|
981
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
982
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: TimePickerComponent, isStandalone: true, selector: "tw-time-picker", inputs: { idInput: { classPropertyName: "idInput", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requiredInput: { classPropertyName: "requiredInput", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, hourStep: { classPropertyName: "hourStep", publicName: "hourStep", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, secondStep: { classPropertyName: "secondStep", publicName: "secondStep", isSignal: true, isRequired: false, transformFunction: null }, minTime: { classPropertyName: "minTime", publicName: "minTime", isSignal: true, isRequired: false, transformFunction: null }, maxTime: { classPropertyName: "maxTime", publicName: "maxTime", isSignal: true, isRequired: false, transformFunction: null }, referenceDate: { classPropertyName: "referenceDate", publicName: "referenceDate", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, showSteppers: { classPropertyName: "showSteppers", publicName: "showSteppers", isSignal: true, isRequired: false, transformFunction: null }, showClear: { classPropertyName: "showClear", publicName: "showClear", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, userAriaDescribedByInput: { classPropertyName: "userAriaDescribedByInput", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", timeInput: "timeInput", timeChange: "timeChange" }, host: { properties: { "class": "rootClasses()", "id": "hostId", "attr.aria-disabled": "isDisabled() || null", "attr.aria-invalid": "errorState() || null", "attr.data-variant": "resolvedVariant()" } }, providers: [
|
|
983
|
+
{
|
|
984
|
+
provide: TW_FORM_FIELD_CONTROL,
|
|
985
|
+
useExisting: forwardRef(() => TimePickerComponent),
|
|
986
|
+
},
|
|
987
|
+
], viewQueries: [{ propertyName: "hourInputRef", first: true, predicate: ["hourInput"], descendants: true, isSignal: true }, { propertyName: "minuteInputRef", first: true, predicate: ["minuteInput"], descendants: true, isSignal: true }, { propertyName: "secondInputRef", first: true, predicate: ["secondInput"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
|
|
988
|
+
<div [class]="fieldGroupClasses()" role="group" [attr.aria-label]="groupAriaLabel()">
|
|
989
|
+
<input
|
|
990
|
+
#hourInput
|
|
991
|
+
type="text"
|
|
992
|
+
inputmode="numeric"
|
|
993
|
+
autocomplete="off"
|
|
994
|
+
maxlength="2"
|
|
995
|
+
[class]="fieldClasses()"
|
|
996
|
+
[value]="hourDisplay()"
|
|
997
|
+
[placeholder]="placeholder() ?? '--'"
|
|
998
|
+
[disabled]="isDisabled()"
|
|
999
|
+
[attr.readonly]="readonlyInput() ? 'true' : null"
|
|
1000
|
+
[attr.aria-label]="intl.hoursLabel"
|
|
1001
|
+
[attr.role]="'spinbutton'"
|
|
1002
|
+
[attr.aria-valuemin]="hourMin()"
|
|
1003
|
+
[attr.aria-valuemax]="hourMax()"
|
|
1004
|
+
[attr.aria-valuenow]="hourValueNow()"
|
|
1005
|
+
[attr.aria-valuetext]="hourValueText()"
|
|
1006
|
+
[attr.aria-invalid]="errorState() || null"
|
|
1007
|
+
[attr.aria-disabled]="isDisabled() || null"
|
|
1008
|
+
(keydown)="onFieldKeydown($event, 'hour')"
|
|
1009
|
+
(beforeinput)="onBeforeInput($event, 'hour')"
|
|
1010
|
+
(input)="onInputEvent($event, 'hour')"
|
|
1011
|
+
(focus)="onFieldFocus('hour')"
|
|
1012
|
+
(blur)="onFieldBlur('hour')"
|
|
1013
|
+
/>
|
|
1014
|
+
|
|
1015
|
+
<span [class]="separatorClasses()" aria-hidden="true">:</span>
|
|
1016
|
+
|
|
1017
|
+
<input
|
|
1018
|
+
#minuteInput
|
|
1019
|
+
type="text"
|
|
1020
|
+
inputmode="numeric"
|
|
1021
|
+
autocomplete="off"
|
|
1022
|
+
maxlength="2"
|
|
1023
|
+
[class]="fieldClasses()"
|
|
1024
|
+
[value]="minuteText()"
|
|
1025
|
+
[placeholder]="placeholder() ?? '--'"
|
|
1026
|
+
[disabled]="isDisabled()"
|
|
1027
|
+
[attr.readonly]="readonlyInput() ? 'true' : null"
|
|
1028
|
+
[attr.aria-label]="intl.minutesLabel"
|
|
1029
|
+
[attr.role]="'spinbutton'"
|
|
1030
|
+
[attr.aria-valuemin]="0"
|
|
1031
|
+
[attr.aria-valuemax]="59"
|
|
1032
|
+
[attr.aria-valuenow]="minuteValueNow()"
|
|
1033
|
+
[attr.aria-valuetext]="minuteValueText()"
|
|
1034
|
+
[attr.aria-invalid]="errorState() || null"
|
|
1035
|
+
[attr.aria-disabled]="isDisabled() || null"
|
|
1036
|
+
(keydown)="onFieldKeydown($event, 'minute')"
|
|
1037
|
+
(beforeinput)="onBeforeInput($event, 'minute')"
|
|
1038
|
+
(input)="onInputEvent($event, 'minute')"
|
|
1039
|
+
(focus)="onFieldFocus('minute')"
|
|
1040
|
+
(blur)="onFieldBlur('minute')"
|
|
1041
|
+
/>
|
|
1042
|
+
|
|
1043
|
+
@if (showSeconds()) {
|
|
1044
|
+
<span [class]="separatorClasses()" aria-hidden="true">:</span>
|
|
1045
|
+
|
|
1046
|
+
<input
|
|
1047
|
+
#secondInput
|
|
1048
|
+
type="text"
|
|
1049
|
+
inputmode="numeric"
|
|
1050
|
+
autocomplete="off"
|
|
1051
|
+
maxlength="2"
|
|
1052
|
+
[class]="fieldClasses()"
|
|
1053
|
+
[value]="secondText()"
|
|
1054
|
+
[placeholder]="placeholder() ?? '--'"
|
|
1055
|
+
[disabled]="isDisabled()"
|
|
1056
|
+
[attr.readonly]="readonlyInput() ? 'true' : null"
|
|
1057
|
+
[attr.aria-label]="intl.secondsLabel"
|
|
1058
|
+
[attr.role]="'spinbutton'"
|
|
1059
|
+
[attr.aria-valuemin]="0"
|
|
1060
|
+
[attr.aria-valuemax]="59"
|
|
1061
|
+
[attr.aria-valuenow]="secondValueNow()"
|
|
1062
|
+
[attr.aria-valuetext]="secondValueText()"
|
|
1063
|
+
[attr.aria-invalid]="errorState() || null"
|
|
1064
|
+
[attr.aria-disabled]="isDisabled() || null"
|
|
1065
|
+
(keydown)="onFieldKeydown($event, 'second')"
|
|
1066
|
+
(beforeinput)="onBeforeInput($event, 'second')"
|
|
1067
|
+
(input)="onInputEvent($event, 'second')"
|
|
1068
|
+
(focus)="onFieldFocus('second')"
|
|
1069
|
+
(blur)="onFieldBlur('second')"
|
|
1070
|
+
/>
|
|
1071
|
+
}
|
|
1072
|
+
</div>
|
|
1073
|
+
|
|
1074
|
+
@if (showSteppers()) {
|
|
1075
|
+
<div [class]="stepperGroupClasses()">
|
|
1076
|
+
<button
|
|
1077
|
+
type="button"
|
|
1078
|
+
tabindex="-1"
|
|
1079
|
+
[class]="stepperClasses()"
|
|
1080
|
+
[attr.aria-label]="intl.increaseLabel(focusedFieldLabel())"
|
|
1081
|
+
[disabled]="isDisabled() || readonlyInput()"
|
|
1082
|
+
(mousedown)="onStepperMouseDown($event)"
|
|
1083
|
+
(click)="onStepperClick(1)"
|
|
1084
|
+
>
|
|
1085
|
+
<ng-content select="[slot=stepper-up]">
|
|
1086
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" [class]="stepperIconClasses()">
|
|
1087
|
+
<path fill-rule="evenodd" d="M14.77 12.79a.75.75 0 0 1-1.06-.02L10 8.94l-3.71 3.83a.75.75 0 1 1-1.08-1.04l4.25-4.39a.75.75 0 0 1 1.08 0l4.25 4.39a.75.75 0 0 1-.02 1.06Z" clip-rule="evenodd" />
|
|
1088
|
+
</svg>
|
|
1089
|
+
</ng-content>
|
|
1090
|
+
</button>
|
|
1091
|
+
<button
|
|
1092
|
+
type="button"
|
|
1093
|
+
tabindex="-1"
|
|
1094
|
+
[class]="stepperClasses()"
|
|
1095
|
+
[attr.aria-label]="intl.decreaseLabel(focusedFieldLabel())"
|
|
1096
|
+
[disabled]="isDisabled() || readonlyInput()"
|
|
1097
|
+
(mousedown)="onStepperMouseDown($event)"
|
|
1098
|
+
(click)="onStepperClick(-1)"
|
|
1099
|
+
>
|
|
1100
|
+
<ng-content select="[slot=stepper-down]">
|
|
1101
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" [class]="stepperIconClasses()">
|
|
1102
|
+
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.06l3.71-3.83a.75.75 0 1 1 1.08 1.04l-4.25 4.39a.75.75 0 0 1-1.08 0L5.21 8.27a.75.75 0 0 1 .02-1.06Z" clip-rule="evenodd" />
|
|
1103
|
+
</svg>
|
|
1104
|
+
</ng-content>
|
|
1105
|
+
</button>
|
|
1106
|
+
</div>
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
@if (format() === '12h') {
|
|
1110
|
+
<div [class]="meridiemClasses()" role="radiogroup" [attr.aria-label]="intl.meridiemGroupLabel">
|
|
1111
|
+
<button
|
|
1112
|
+
type="button"
|
|
1113
|
+
role="radio"
|
|
1114
|
+
[class]="meridiemButtonClasses('AM')"
|
|
1115
|
+
[attr.aria-checked]="meridiem() === 'AM'"
|
|
1116
|
+
[attr.aria-label]="intl.amLabel"
|
|
1117
|
+
[disabled]="isDisabled() || readonlyInput()"
|
|
1118
|
+
(click)="setMeridiem('AM')"
|
|
1119
|
+
(keydown)="onMeridiemKeydown($event, 'AM')"
|
|
1120
|
+
>{{ intl.amLabel }}</button>
|
|
1121
|
+
<button
|
|
1122
|
+
type="button"
|
|
1123
|
+
role="radio"
|
|
1124
|
+
[class]="meridiemButtonClasses('PM')"
|
|
1125
|
+
[attr.aria-checked]="meridiem() === 'PM'"
|
|
1126
|
+
[attr.aria-label]="intl.pmLabel"
|
|
1127
|
+
[disabled]="isDisabled() || readonlyInput()"
|
|
1128
|
+
(click)="setMeridiem('PM')"
|
|
1129
|
+
(keydown)="onMeridiemKeydown($event, 'PM')"
|
|
1130
|
+
>{{ intl.pmLabel }}</button>
|
|
1131
|
+
</div>
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
@if (showClear() && !isEmpty() && !isDisabled() && !readonlyInput()) {
|
|
1135
|
+
<button
|
|
1136
|
+
type="button"
|
|
1137
|
+
tabindex="-1"
|
|
1138
|
+
[class]="clearButtonClasses()"
|
|
1139
|
+
[attr.aria-label]="clearLabel()"
|
|
1140
|
+
(click)="onClearClick($event)"
|
|
1141
|
+
>
|
|
1142
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-3">
|
|
1143
|
+
<path
|
|
1144
|
+
fill-rule="evenodd"
|
|
1145
|
+
d="M10 8.586 4.707 3.293a1 1 0 0 0-1.414 1.414L8.586 10l-5.293 5.293a1 1 0 1 0 1.414 1.414L10 11.414l5.293 5.293a1 1 0 0 0 1.414-1.414L11.414 10l5.293-5.293a1 1 0 0 0-1.414-1.414L10 8.586Z"
|
|
1146
|
+
clip-rule="evenodd"
|
|
1147
|
+
/>
|
|
1148
|
+
</svg>
|
|
1149
|
+
</button>
|
|
1150
|
+
}
|
|
1151
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1152
|
+
}
|
|
1153
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimePickerComponent, decorators: [{
|
|
1154
|
+
type: Component,
|
|
1155
|
+
args: [{
|
|
1156
|
+
selector: 'tw-time-picker',
|
|
1157
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1158
|
+
providers: [
|
|
1159
|
+
{
|
|
1160
|
+
provide: TW_FORM_FIELD_CONTROL,
|
|
1161
|
+
useExisting: forwardRef(() => TimePickerComponent),
|
|
1162
|
+
},
|
|
1163
|
+
],
|
|
1164
|
+
template: `
|
|
1165
|
+
<div [class]="fieldGroupClasses()" role="group" [attr.aria-label]="groupAriaLabel()">
|
|
1166
|
+
<input
|
|
1167
|
+
#hourInput
|
|
1168
|
+
type="text"
|
|
1169
|
+
inputmode="numeric"
|
|
1170
|
+
autocomplete="off"
|
|
1171
|
+
maxlength="2"
|
|
1172
|
+
[class]="fieldClasses()"
|
|
1173
|
+
[value]="hourDisplay()"
|
|
1174
|
+
[placeholder]="placeholder() ?? '--'"
|
|
1175
|
+
[disabled]="isDisabled()"
|
|
1176
|
+
[attr.readonly]="readonlyInput() ? 'true' : null"
|
|
1177
|
+
[attr.aria-label]="intl.hoursLabel"
|
|
1178
|
+
[attr.role]="'spinbutton'"
|
|
1179
|
+
[attr.aria-valuemin]="hourMin()"
|
|
1180
|
+
[attr.aria-valuemax]="hourMax()"
|
|
1181
|
+
[attr.aria-valuenow]="hourValueNow()"
|
|
1182
|
+
[attr.aria-valuetext]="hourValueText()"
|
|
1183
|
+
[attr.aria-invalid]="errorState() || null"
|
|
1184
|
+
[attr.aria-disabled]="isDisabled() || null"
|
|
1185
|
+
(keydown)="onFieldKeydown($event, 'hour')"
|
|
1186
|
+
(beforeinput)="onBeforeInput($event, 'hour')"
|
|
1187
|
+
(input)="onInputEvent($event, 'hour')"
|
|
1188
|
+
(focus)="onFieldFocus('hour')"
|
|
1189
|
+
(blur)="onFieldBlur('hour')"
|
|
1190
|
+
/>
|
|
1191
|
+
|
|
1192
|
+
<span [class]="separatorClasses()" aria-hidden="true">:</span>
|
|
1193
|
+
|
|
1194
|
+
<input
|
|
1195
|
+
#minuteInput
|
|
1196
|
+
type="text"
|
|
1197
|
+
inputmode="numeric"
|
|
1198
|
+
autocomplete="off"
|
|
1199
|
+
maxlength="2"
|
|
1200
|
+
[class]="fieldClasses()"
|
|
1201
|
+
[value]="minuteText()"
|
|
1202
|
+
[placeholder]="placeholder() ?? '--'"
|
|
1203
|
+
[disabled]="isDisabled()"
|
|
1204
|
+
[attr.readonly]="readonlyInput() ? 'true' : null"
|
|
1205
|
+
[attr.aria-label]="intl.minutesLabel"
|
|
1206
|
+
[attr.role]="'spinbutton'"
|
|
1207
|
+
[attr.aria-valuemin]="0"
|
|
1208
|
+
[attr.aria-valuemax]="59"
|
|
1209
|
+
[attr.aria-valuenow]="minuteValueNow()"
|
|
1210
|
+
[attr.aria-valuetext]="minuteValueText()"
|
|
1211
|
+
[attr.aria-invalid]="errorState() || null"
|
|
1212
|
+
[attr.aria-disabled]="isDisabled() || null"
|
|
1213
|
+
(keydown)="onFieldKeydown($event, 'minute')"
|
|
1214
|
+
(beforeinput)="onBeforeInput($event, 'minute')"
|
|
1215
|
+
(input)="onInputEvent($event, 'minute')"
|
|
1216
|
+
(focus)="onFieldFocus('minute')"
|
|
1217
|
+
(blur)="onFieldBlur('minute')"
|
|
1218
|
+
/>
|
|
1219
|
+
|
|
1220
|
+
@if (showSeconds()) {
|
|
1221
|
+
<span [class]="separatorClasses()" aria-hidden="true">:</span>
|
|
1222
|
+
|
|
1223
|
+
<input
|
|
1224
|
+
#secondInput
|
|
1225
|
+
type="text"
|
|
1226
|
+
inputmode="numeric"
|
|
1227
|
+
autocomplete="off"
|
|
1228
|
+
maxlength="2"
|
|
1229
|
+
[class]="fieldClasses()"
|
|
1230
|
+
[value]="secondText()"
|
|
1231
|
+
[placeholder]="placeholder() ?? '--'"
|
|
1232
|
+
[disabled]="isDisabled()"
|
|
1233
|
+
[attr.readonly]="readonlyInput() ? 'true' : null"
|
|
1234
|
+
[attr.aria-label]="intl.secondsLabel"
|
|
1235
|
+
[attr.role]="'spinbutton'"
|
|
1236
|
+
[attr.aria-valuemin]="0"
|
|
1237
|
+
[attr.aria-valuemax]="59"
|
|
1238
|
+
[attr.aria-valuenow]="secondValueNow()"
|
|
1239
|
+
[attr.aria-valuetext]="secondValueText()"
|
|
1240
|
+
[attr.aria-invalid]="errorState() || null"
|
|
1241
|
+
[attr.aria-disabled]="isDisabled() || null"
|
|
1242
|
+
(keydown)="onFieldKeydown($event, 'second')"
|
|
1243
|
+
(beforeinput)="onBeforeInput($event, 'second')"
|
|
1244
|
+
(input)="onInputEvent($event, 'second')"
|
|
1245
|
+
(focus)="onFieldFocus('second')"
|
|
1246
|
+
(blur)="onFieldBlur('second')"
|
|
1247
|
+
/>
|
|
1248
|
+
}
|
|
1249
|
+
</div>
|
|
1250
|
+
|
|
1251
|
+
@if (showSteppers()) {
|
|
1252
|
+
<div [class]="stepperGroupClasses()">
|
|
1253
|
+
<button
|
|
1254
|
+
type="button"
|
|
1255
|
+
tabindex="-1"
|
|
1256
|
+
[class]="stepperClasses()"
|
|
1257
|
+
[attr.aria-label]="intl.increaseLabel(focusedFieldLabel())"
|
|
1258
|
+
[disabled]="isDisabled() || readonlyInput()"
|
|
1259
|
+
(mousedown)="onStepperMouseDown($event)"
|
|
1260
|
+
(click)="onStepperClick(1)"
|
|
1261
|
+
>
|
|
1262
|
+
<ng-content select="[slot=stepper-up]">
|
|
1263
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" [class]="stepperIconClasses()">
|
|
1264
|
+
<path fill-rule="evenodd" d="M14.77 12.79a.75.75 0 0 1-1.06-.02L10 8.94l-3.71 3.83a.75.75 0 1 1-1.08-1.04l4.25-4.39a.75.75 0 0 1 1.08 0l4.25 4.39a.75.75 0 0 1-.02 1.06Z" clip-rule="evenodd" />
|
|
1265
|
+
</svg>
|
|
1266
|
+
</ng-content>
|
|
1267
|
+
</button>
|
|
1268
|
+
<button
|
|
1269
|
+
type="button"
|
|
1270
|
+
tabindex="-1"
|
|
1271
|
+
[class]="stepperClasses()"
|
|
1272
|
+
[attr.aria-label]="intl.decreaseLabel(focusedFieldLabel())"
|
|
1273
|
+
[disabled]="isDisabled() || readonlyInput()"
|
|
1274
|
+
(mousedown)="onStepperMouseDown($event)"
|
|
1275
|
+
(click)="onStepperClick(-1)"
|
|
1276
|
+
>
|
|
1277
|
+
<ng-content select="[slot=stepper-down]">
|
|
1278
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" [class]="stepperIconClasses()">
|
|
1279
|
+
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.06l3.71-3.83a.75.75 0 1 1 1.08 1.04l-4.25 4.39a.75.75 0 0 1-1.08 0L5.21 8.27a.75.75 0 0 1 .02-1.06Z" clip-rule="evenodd" />
|
|
1280
|
+
</svg>
|
|
1281
|
+
</ng-content>
|
|
1282
|
+
</button>
|
|
1283
|
+
</div>
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
@if (format() === '12h') {
|
|
1287
|
+
<div [class]="meridiemClasses()" role="radiogroup" [attr.aria-label]="intl.meridiemGroupLabel">
|
|
1288
|
+
<button
|
|
1289
|
+
type="button"
|
|
1290
|
+
role="radio"
|
|
1291
|
+
[class]="meridiemButtonClasses('AM')"
|
|
1292
|
+
[attr.aria-checked]="meridiem() === 'AM'"
|
|
1293
|
+
[attr.aria-label]="intl.amLabel"
|
|
1294
|
+
[disabled]="isDisabled() || readonlyInput()"
|
|
1295
|
+
(click)="setMeridiem('AM')"
|
|
1296
|
+
(keydown)="onMeridiemKeydown($event, 'AM')"
|
|
1297
|
+
>{{ intl.amLabel }}</button>
|
|
1298
|
+
<button
|
|
1299
|
+
type="button"
|
|
1300
|
+
role="radio"
|
|
1301
|
+
[class]="meridiemButtonClasses('PM')"
|
|
1302
|
+
[attr.aria-checked]="meridiem() === 'PM'"
|
|
1303
|
+
[attr.aria-label]="intl.pmLabel"
|
|
1304
|
+
[disabled]="isDisabled() || readonlyInput()"
|
|
1305
|
+
(click)="setMeridiem('PM')"
|
|
1306
|
+
(keydown)="onMeridiemKeydown($event, 'PM')"
|
|
1307
|
+
>{{ intl.pmLabel }}</button>
|
|
1308
|
+
</div>
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
@if (showClear() && !isEmpty() && !isDisabled() && !readonlyInput()) {
|
|
1312
|
+
<button
|
|
1313
|
+
type="button"
|
|
1314
|
+
tabindex="-1"
|
|
1315
|
+
[class]="clearButtonClasses()"
|
|
1316
|
+
[attr.aria-label]="clearLabel()"
|
|
1317
|
+
(click)="onClearClick($event)"
|
|
1318
|
+
>
|
|
1319
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-3">
|
|
1320
|
+
<path
|
|
1321
|
+
fill-rule="evenodd"
|
|
1322
|
+
d="M10 8.586 4.707 3.293a1 1 0 0 0-1.414 1.414L8.586 10l-5.293 5.293a1 1 0 1 0 1.414 1.414L10 11.414l5.293 5.293a1 1 0 0 0 1.414-1.414L11.414 10l5.293-5.293a1 1 0 0 0-1.414-1.414L10 8.586Z"
|
|
1323
|
+
clip-rule="evenodd"
|
|
1324
|
+
/>
|
|
1325
|
+
</svg>
|
|
1326
|
+
</button>
|
|
1327
|
+
}
|
|
1328
|
+
`,
|
|
1329
|
+
host: {
|
|
1330
|
+
'[class]': 'rootClasses()',
|
|
1331
|
+
'[id]': 'hostId',
|
|
1332
|
+
'[attr.aria-disabled]': 'isDisabled() || null',
|
|
1333
|
+
'[attr.aria-invalid]': 'errorState() || null',
|
|
1334
|
+
'[attr.data-variant]': 'resolvedVariant()',
|
|
1335
|
+
},
|
|
1336
|
+
}]
|
|
1337
|
+
}], ctorParameters: () => [], propDecorators: { idInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], showSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeconds", required: false }] }], hourStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "hourStep", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], secondStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondStep", required: false }] }], minTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "minTime", required: false }] }], maxTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxTime", required: false }] }], referenceDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "referenceDate", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], showSteppers: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSteppers", required: false }] }], showClear: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClear", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], userAriaDescribedByInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], timeInput: [{ type: i0.Output, args: ["timeInput"] }], timeChange: [{ type: i0.Output, args: ["timeChange"] }], hourInputRef: [{ type: i0.ViewChild, args: ['hourInput', { isSignal: true }] }], minuteInputRef: [{ type: i0.ViewChild, args: ['minuteInput', { isSignal: true }] }], secondInputRef: [{ type: i0.ViewChild, args: ['secondInput', { isSignal: true }] }] } });
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Generated bundle index. Do not edit.
|
|
1341
|
+
*/
|
|
1342
|
+
|
|
1343
|
+
export { TimePickerComponent, TimePickerIntl, provideTimePickerIntl };
|
|
1344
|
+
//# sourceMappingURL=cdevhub-ngx-tw-time-picker.mjs.map
|