@momentum-design/components 0.130.8 → 0.131.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.
@@ -0,0 +1,1016 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { html, nothing } from 'lit';
11
+ import { property, query, state } from 'lit/decorators.js';
12
+ import { ifDefined } from 'lit/directives/if-defined.js';
13
+ import { DataAriaLabelMixin } from '../../utils/mixins/DataAriaLabelMixin';
14
+ import { FormInternalsMixin } from '../../utils/mixins/FormInternalsMixin';
15
+ import { KEYS } from '../../utils/keys';
16
+ import FormfieldWrapper from '../formfieldwrapper/formfieldwrapper.component';
17
+ import { POPOVER_PLACEMENT, TRIGGER, DEFAULTS as POPOVER_DEFAULTS } from '../popover/popover.constants';
18
+ import { ARROW_ICON, DEFAULTS, TIME_FORMAT, TRIGGER_ID, LISTBOX_ID } from './timepicker.constants';
19
+ import styles from './timepicker.styles';
20
+ /**
21
+ * mdc-timepicker is a component that allows users to select a specific time
22
+ * or enter a time manually. It supports both 12-hour and 24-hour formats.
23
+ *
24
+ * The component consists of:
25
+ * - label - describes the time picker field
26
+ * - input field - made up of 2-3 spinbuttons (hours, minutes, optional AM/PM period)
27
+ * - dropdown arrow button - opens a flyout menu with predefined time intervals
28
+ * - helper text - displayed below the input field
29
+ *
30
+ * Users can input values by:
31
+ * - Manually entering numbers/characters in spinbuttons
32
+ * - Navigating using arrow keys to increment/decrement values
33
+ * - Selecting a predefined time from the dropdown menu
34
+ *
35
+ * @tagname mdc-timepicker
36
+ *
37
+ * @dependency mdc-button
38
+ * @dependency mdc-icon
39
+ * @dependency mdc-option
40
+ * @dependency mdc-popover
41
+ * @dependency mdc-text
42
+ * @dependency mdc-toggletip
43
+ *
44
+ * @event input - (React: onInput) This event is dispatched when the time value changes.
45
+ * @event change - (React: onChange) This event is dispatched when the time value is committed.
46
+ * @event focus - (React: onFocus) This event is dispatched when the timepicker receives focus.
47
+ * @event blur - (React: onBlur) This event is dispatched when the timepicker loses focus.
48
+ *
49
+ * @slot label - Slot for the label element.
50
+ * @slot toggletip - Slot for the toggletip info icon button.
51
+ * @slot help-icon - Slot for the helper/validation icon.
52
+ * @slot help-text - Slot for the helper/validation text.
53
+ *
54
+ * @cssproperty --mdc-timepicker-background-color - Background color of the timepicker input.
55
+ * @cssproperty --mdc-timepicker-border-color - Border color of the timepicker input.
56
+ * @cssproperty --mdc-timepicker-text-color - Text color of the timepicker input.
57
+ * @cssproperty --mdc-timepicker-width - Width of the timepicker component.
58
+ *
59
+ * @csspart label - The label element.
60
+ * @csspart label-text - The container for the label and required indicator elements.
61
+ * @csspart required-indicator - The required indicator element.
62
+ * @csspart info-icon-btn - The info icon button element.
63
+ * @csspart label-toggletip - The toggletip element.
64
+ * @csspart help-text - The helper/validation text element.
65
+ * @csspart helper-icon - The helper/validation icon element.
66
+ * @csspart help-text-container - The container for helper/validation elements.
67
+ * @csspart container - The outer container for the input and popover.
68
+ * @csspart base-container - The input container with border and background.
69
+ * @csspart spinbutton-group - The container for spinbutton elements.
70
+ * @csspart spinbutton - A spinbutton input element.
71
+ * @csspart separator - The colon separator between spinbuttons.
72
+ * @csspart period - The AM/PM period spinbutton.
73
+ * @csspart icon-container - The dropdown arrow button container.
74
+ * @csspart native-input - The hidden native input for form participation.
75
+ * @csspart listbox - The dropdown list container.
76
+ */
77
+ class TimePicker extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)) {
78
+ constructor() {
79
+ super(...arguments);
80
+ /**
81
+ * The time format to use for display.
82
+ * - `'12h'`: 12-hour format with AM/PM period
83
+ * - `'24h'`: 24-hour format without period
84
+ * @default '12h'
85
+ */
86
+ this.timeFormat = DEFAULTS.TIME_FORMAT;
87
+ /**
88
+ * The interval in minutes between time options in the dropdown menu.
89
+ * @default 30
90
+ */
91
+ this.interval = DEFAULTS.INTERVAL;
92
+ /**
93
+ * The placement of the popover dropdown.
94
+ * @default 'bottom-start'
95
+ */
96
+ this.placement = POPOVER_PLACEMENT.BOTTOM_START;
97
+ /**
98
+ * The strategy for positioning the popover.
99
+ * @default 'absolute'
100
+ */
101
+ this.strategy = POPOVER_DEFAULTS.STRATEGY;
102
+ /**
103
+ * Determines whether the dropdown should flip its position when it hits the boundary.
104
+ * @default false
105
+ */
106
+ this.disableFlip = DEFAULTS.DISABLE_FLIP;
107
+ /**
108
+ * Accessible label for the hours spinbutton.
109
+ * Consumers must provide a translated string.
110
+ */
111
+ this.localeHoursLabel = '';
112
+ /**
113
+ * Accessible label for the minutes spinbutton.
114
+ * Consumers must provide a translated string.
115
+ */
116
+ this.localeMinutesLabel = '';
117
+ /**
118
+ * Accessible label for the period (AM/PM) spinbutton.
119
+ * Consumers must provide a translated string.
120
+ */
121
+ this.localePeriodLabel = '';
122
+ /**
123
+ * Placeholder text for the hours spinbutton.
124
+ * Consumers must provide a translated string.
125
+ */
126
+ this.localeHoursPlaceholder = '';
127
+ /**
128
+ * Placeholder text for the minutes spinbutton.
129
+ * Consumers must provide a translated string.
130
+ */
131
+ this.localeMinutesPlaceholder = '';
132
+ /**
133
+ * Placeholder text for the period spinbutton.
134
+ * Consumers must provide a translated string.
135
+ */
136
+ this.localePeriodPlaceholder = '';
137
+ /**
138
+ * Label for the AM period.
139
+ * Consumers must provide a translated string.
140
+ */
141
+ this.localeAmLabel = '';
142
+ /**
143
+ * Label for the PM period.
144
+ * Consumers must provide a translated string.
145
+ */
146
+ this.localePmLabel = '';
147
+ /**
148
+ * Accessible label for the dropdown toggle button.
149
+ * Consumers must provide a translated string.
150
+ */
151
+ this.localeShowTimePickerLabel = '';
152
+ /**
153
+ * Accessible label for the time options listbox.
154
+ * Consumers must provide a translated string.
155
+ */
156
+ this.localeTimeOptionsLabel = '';
157
+ /**
158
+ * Accessible description for spinbutton inputs (instruction text).
159
+ * Consumers must provide a translated string.
160
+ */
161
+ this.localeSpinbuttonDescription = '';
162
+ /** @internal */
163
+ this.displayPopover = false;
164
+ /** @internal */
165
+ this.internalHours = '';
166
+ /** @internal */
167
+ this.internalMinutes = '';
168
+ /** @internal */
169
+ this.internalPeriod = 'AM';
170
+ /** @internal */
171
+ this.focusedOptionIndex = -1;
172
+ /** @internal */
173
+ this.pendingDigits = '';
174
+ }
175
+ connectedCallback() {
176
+ super.connectedCallback();
177
+ this.updateComplete
178
+ .then(() => {
179
+ this.parseValueToInternal();
180
+ this.syncFormValue();
181
+ })
182
+ .catch(error => {
183
+ if (this.onerror) {
184
+ this.onerror(error);
185
+ }
186
+ });
187
+ }
188
+ disconnectedCallback() {
189
+ super.disconnectedCallback();
190
+ if (this.pendingDigitTimeout) {
191
+ clearTimeout(this.pendingDigitTimeout);
192
+ }
193
+ }
194
+ willUpdate(changedProperties) {
195
+ super.willUpdate(changedProperties);
196
+ if (changedProperties.has('value') && !this.displayPopover) {
197
+ this.parseValueToInternal();
198
+ this.syncFormValue();
199
+ }
200
+ if (changedProperties.has('disabled') ||
201
+ changedProperties.has('softDisabled') ||
202
+ changedProperties.has('readonly')) {
203
+ if (this.disabled || this.softDisabled || this.readonly) {
204
+ this.displayPopover = false;
205
+ }
206
+ }
207
+ }
208
+ updated(changedProperties) {
209
+ super.updated(changedProperties);
210
+ if (changedProperties.has('displayPopover') && this.displayPopover) {
211
+ this.focusMenuItemOnOpen();
212
+ }
213
+ }
214
+ /**
215
+ * When the menu opens, focus the selected item or the first item.
216
+ * @internal
217
+ */
218
+ focusMenuItemOnOpen() {
219
+ const options = this.getTimeOptions();
220
+ const currentValue = this.internalToValue();
221
+ const selectedIndex = options.findIndex(opt => opt.value === currentValue);
222
+ this.focusedOptionIndex = selectedIndex >= 0 ? selectedIndex : 0;
223
+ this.updateComplete
224
+ .then(() => {
225
+ this.focusCurrentMenuItem();
226
+ })
227
+ .catch(() => { });
228
+ }
229
+ /**
230
+ * Focuses the menu item at the current focusedOptionIndex.
231
+ * @internal
232
+ */
233
+ focusCurrentMenuItem() {
234
+ var _a;
235
+ const listbox = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(`#${LISTBOX_ID}`);
236
+ if (!listbox)
237
+ return;
238
+ const items = listbox.querySelectorAll('mdc-option');
239
+ if (items[this.focusedOptionIndex]) {
240
+ items[this.focusedOptionIndex].focus();
241
+ }
242
+ }
243
+ /** @internal */
244
+ formResetCallback() {
245
+ this.value = '';
246
+ this.internalHours = '';
247
+ this.internalMinutes = '';
248
+ this.internalPeriod = 'AM';
249
+ this.syncFormValue();
250
+ this.requestUpdate();
251
+ }
252
+ /** @internal */
253
+ formStateRestoreCallback(state) {
254
+ this.value = state;
255
+ this.parseValueToInternal();
256
+ }
257
+ /**
258
+ * Parses the value (24h HH:MM) into internal hours, minutes, and period.
259
+ * @internal
260
+ */
261
+ parseValueToInternal() {
262
+ if (!this.value) {
263
+ this.internalHours = '';
264
+ this.internalMinutes = '';
265
+ this.internalPeriod = 'AM';
266
+ return;
267
+ }
268
+ const match = this.value.match(/^(\d{1,2}):(\d{2})$/);
269
+ if (!match)
270
+ return;
271
+ const hours = parseInt(match[1], 10);
272
+ const minutes = parseInt(match[2], 10);
273
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59)
274
+ return;
275
+ this.internalMinutes = String(minutes).padStart(2, '0');
276
+ if (this.timeFormat === TIME_FORMAT.TWELVE_HOUR) {
277
+ if (hours === 0) {
278
+ this.internalPeriod = 'AM';
279
+ this.internalHours = '12';
280
+ }
281
+ else if (hours < 12) {
282
+ this.internalPeriod = 'AM';
283
+ this.internalHours = String(hours).padStart(2, '0');
284
+ }
285
+ else if (hours === 12) {
286
+ this.internalPeriod = 'PM';
287
+ this.internalHours = '12';
288
+ }
289
+ else {
290
+ this.internalPeriod = 'PM';
291
+ this.internalHours = String(hours - 12).padStart(2, '0');
292
+ }
293
+ }
294
+ else {
295
+ this.internalHours = String(hours).padStart(2, '0');
296
+ }
297
+ }
298
+ /**
299
+ * Converts internal hours, minutes, and period to 24h HH:MM value.
300
+ * @internal
301
+ */
302
+ internalToValue() {
303
+ if (!this.internalHours || !this.internalMinutes)
304
+ return '';
305
+ let hours = parseInt(this.internalHours, 10);
306
+ if (this.timeFormat === TIME_FORMAT.TWELVE_HOUR) {
307
+ if (this.internalPeriod === 'AM') {
308
+ hours = hours === 12 ? 0 : hours;
309
+ }
310
+ else {
311
+ hours = hours === 12 ? 12 : hours + 12;
312
+ }
313
+ }
314
+ return `${String(hours).padStart(2, '0')}:${this.internalMinutes}`;
315
+ }
316
+ /**
317
+ * Syncs the form value with the current internal state.
318
+ * @internal
319
+ */
320
+ syncFormValue() {
321
+ const val = this.internalToValue();
322
+ this.internals.setFormValue(val || this.value);
323
+ }
324
+ /**
325
+ * Updates the value from internal state and fires events.
326
+ * @internal
327
+ */
328
+ commitValue() {
329
+ const newValue = this.internalToValue();
330
+ if (newValue && newValue !== this.value) {
331
+ this.value = newValue;
332
+ this.syncFormValue();
333
+ this.dispatchEvent(new CustomEvent('input', {
334
+ detail: { value: this.value },
335
+ composed: true,
336
+ bubbles: true,
337
+ }));
338
+ this.dispatchEvent(new CustomEvent('change', {
339
+ detail: { value: this.value },
340
+ composed: true,
341
+ bubbles: true,
342
+ }));
343
+ }
344
+ }
345
+ /**
346
+ * Generates time interval options for the dropdown.
347
+ * @internal
348
+ */
349
+ getTimeOptions() {
350
+ const options = [];
351
+ const interval = Math.max(1, Math.min(this.interval, 60));
352
+ for (let totalMinutes = 0; totalMinutes < 24 * 60; totalMinutes += interval) {
353
+ const h = Math.floor(totalMinutes / 60);
354
+ const m = totalMinutes % 60;
355
+ const value24 = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
356
+ let label;
357
+ if (this.timeFormat === TIME_FORMAT.TWELVE_HOUR) {
358
+ const period = h < 12 ? this.localeAmLabel : this.localePmLabel;
359
+ let displayHour = h % 12;
360
+ if (displayHour === 0)
361
+ displayHour = 12;
362
+ const displayMin = m === 0 ? '00' : String(m);
363
+ label = `${displayHour}:${displayMin} ${period}`;
364
+ }
365
+ else {
366
+ label = `${h}:${String(m).padStart(2, '0')}`;
367
+ }
368
+ // Filter by min/max if provided
369
+ const withinMin = !this.min || value24 >= this.min;
370
+ const withinMax = !this.max || value24 <= this.max;
371
+ if (withinMin && withinMax) {
372
+ options.push({ label, value: value24 });
373
+ }
374
+ }
375
+ return options;
376
+ }
377
+ /**
378
+ * Handles clicking on a time option in the dropdown.
379
+ * @internal
380
+ */
381
+ handleOptionClick(optionValue) {
382
+ var _a;
383
+ this.value = optionValue;
384
+ this.parseValueToInternal();
385
+ this.displayPopover = false;
386
+ this.syncFormValue();
387
+ this.dispatchEvent(new CustomEvent('input', {
388
+ detail: { value: this.value },
389
+ composed: true,
390
+ bubbles: true,
391
+ }));
392
+ this.dispatchEvent(new CustomEvent('change', {
393
+ detail: { value: this.value },
394
+ composed: true,
395
+ bubbles: true,
396
+ }));
397
+ // Return focus to the dropdown button
398
+ (_a = this.dropdownButton) === null || _a === void 0 ? void 0 : _a.focus();
399
+ }
400
+ /**
401
+ * Handles clicking the dropdown arrow button.
402
+ * @internal
403
+ */
404
+ handleDropdownClick(event) {
405
+ if (this.disabled || this.softDisabled || this.readonly)
406
+ return;
407
+ this.displayPopover = !this.displayPopover;
408
+ event.stopPropagation();
409
+ }
410
+ /**
411
+ * Handles clicking on the spinbutton area (not the dropdown button).
412
+ * Focuses the nearest spinbutton.
413
+ * @internal
414
+ */
415
+ handleSpinbuttonAreaClick(event) {
416
+ var _a, _b;
417
+ if (this.disabled || this.softDisabled || this.readonly)
418
+ return;
419
+ const target = event.target;
420
+ // If clicking on a spinbutton itself, let it handle focus
421
+ if (target.getAttribute('role') === 'spinbutton')
422
+ return;
423
+ // Otherwise focus the hours spinbutton
424
+ (_a = this.hoursInput) === null || _a === void 0 ? void 0 : _a.focus();
425
+ (_b = this.hoursInput) === null || _b === void 0 ? void 0 : _b.select();
426
+ }
427
+ /**
428
+ * Handles keydown on the base container (when popover is closed).
429
+ * @internal
430
+ */
431
+ handleBaseKeydown(event) {
432
+ var _a;
433
+ if (this.disabled || this.softDisabled || this.readonly)
434
+ return;
435
+ if (event.key === KEYS.ESCAPE && this.displayPopover) {
436
+ this.displayPopover = false;
437
+ (_a = this.dropdownButton) === null || _a === void 0 ? void 0 : _a.focus();
438
+ event.preventDefault();
439
+ event.stopPropagation();
440
+ }
441
+ }
442
+ /**
443
+ * Handles keydown on the popover/listbox (when open).
444
+ * Supports ArrowDown/ArrowUp to navigate, Enter to select, Escape to close.
445
+ * @internal
446
+ */
447
+ handleListboxKeydown(event) {
448
+ var _a;
449
+ const options = this.getTimeOptions();
450
+ const optionCount = options.length;
451
+ if (optionCount === 0)
452
+ return;
453
+ switch (event.key) {
454
+ case KEYS.ARROW_DOWN: {
455
+ event.preventDefault();
456
+ event.stopPropagation();
457
+ this.focusedOptionIndex = (this.focusedOptionIndex + 1) % optionCount;
458
+ this.focusCurrentMenuItem();
459
+ break;
460
+ }
461
+ case KEYS.ARROW_UP: {
462
+ event.preventDefault();
463
+ event.stopPropagation();
464
+ this.focusedOptionIndex = (this.focusedOptionIndex - 1 + optionCount) % optionCount;
465
+ this.focusCurrentMenuItem();
466
+ break;
467
+ }
468
+ case KEYS.ENTER: {
469
+ event.preventDefault();
470
+ event.stopPropagation();
471
+ if (this.focusedOptionIndex >= 0 && this.focusedOptionIndex < optionCount) {
472
+ this.handleOptionClick(options[this.focusedOptionIndex].value);
473
+ }
474
+ break;
475
+ }
476
+ case KEYS.ESCAPE: {
477
+ event.preventDefault();
478
+ event.stopPropagation();
479
+ this.displayPopover = false;
480
+ (_a = this.dropdownButton) === null || _a === void 0 ? void 0 : _a.focus();
481
+ break;
482
+ }
483
+ default:
484
+ break;
485
+ }
486
+ }
487
+ /**
488
+ * Handles keydown on the hours spinbutton.
489
+ * @internal
490
+ */
491
+ handleHoursKeydown(event) {
492
+ this.handleSpinbuttonKeydown(event, 'hours');
493
+ }
494
+ /**
495
+ * Handles keydown on the minutes spinbutton.
496
+ * @internal
497
+ */
498
+ handleMinutesKeydown(event) {
499
+ this.handleSpinbuttonKeydown(event, 'minutes');
500
+ }
501
+ /**
502
+ * Handles keydown on the period spinbutton.
503
+ * @internal
504
+ */
505
+ handlePeriodKeydown(event) {
506
+ var _a, _b;
507
+ switch (event.key) {
508
+ case KEYS.ARROW_UP:
509
+ case KEYS.ARROW_DOWN:
510
+ event.preventDefault();
511
+ this.internalPeriod = this.internalPeriod === 'AM' ? 'PM' : 'AM';
512
+ this.commitValue();
513
+ this.requestUpdate();
514
+ break;
515
+ case KEYS.TAB:
516
+ // Allow default tab behavior
517
+ break;
518
+ case KEYS.ARROW_LEFT:
519
+ event.preventDefault();
520
+ (_a = this.minutesInput) === null || _a === void 0 ? void 0 : _a.focus();
521
+ (_b = this.minutesInput) === null || _b === void 0 ? void 0 : _b.select();
522
+ break;
523
+ default: {
524
+ event.preventDefault();
525
+ const amChar = this.localeAmLabel.charAt(0).toLowerCase();
526
+ const pmChar = this.localePmLabel.charAt(0).toLowerCase();
527
+ const pressed = event.key.toLowerCase();
528
+ if (pressed === amChar) {
529
+ this.internalPeriod = 'AM';
530
+ this.commitValue();
531
+ this.requestUpdate();
532
+ }
533
+ else if (pressed === pmChar) {
534
+ this.internalPeriod = 'PM';
535
+ this.commitValue();
536
+ this.requestUpdate();
537
+ }
538
+ break;
539
+ }
540
+ }
541
+ }
542
+ /**
543
+ * Generic spinbutton keydown handler for hours and minutes.
544
+ * @internal
545
+ */
546
+ handleSpinbuttonKeydown(event, field) {
547
+ var _a, _b, _c, _d, _e, _f;
548
+ const is12h = this.timeFormat === TIME_FORMAT.TWELVE_HOUR;
549
+ let minVal;
550
+ let maxVal;
551
+ if (field === 'hours') {
552
+ minVal = is12h ? DEFAULTS.MIN_HOUR_12 : DEFAULTS.MIN_HOUR_24;
553
+ maxVal = is12h ? DEFAULTS.MAX_HOUR_12 : DEFAULTS.MAX_HOUR_24;
554
+ }
555
+ else {
556
+ minVal = DEFAULTS.MIN_MINUTE;
557
+ maxVal = DEFAULTS.MAX_MINUTE;
558
+ }
559
+ const currentStr = field === 'hours' ? this.internalHours : this.internalMinutes;
560
+ let current = currentStr ? parseInt(currentStr, 10) : minVal;
561
+ switch (event.key) {
562
+ case KEYS.ARROW_UP: {
563
+ event.preventDefault();
564
+ current = current >= maxVal ? minVal : current + 1;
565
+ this.setSpinbuttonValue(field, current);
566
+ this.commitValue();
567
+ break;
568
+ }
569
+ case KEYS.ARROW_DOWN: {
570
+ event.preventDefault();
571
+ current = current <= minVal ? maxVal : current - 1;
572
+ this.setSpinbuttonValue(field, current);
573
+ this.commitValue();
574
+ break;
575
+ }
576
+ case KEYS.ARROW_RIGHT: {
577
+ event.preventDefault();
578
+ if (field === 'hours') {
579
+ (_a = this.minutesInput) === null || _a === void 0 ? void 0 : _a.focus();
580
+ (_b = this.minutesInput) === null || _b === void 0 ? void 0 : _b.select();
581
+ }
582
+ else if (is12h) {
583
+ (_c = this.periodInput) === null || _c === void 0 ? void 0 : _c.focus();
584
+ (_d = this.periodInput) === null || _d === void 0 ? void 0 : _d.select();
585
+ }
586
+ break;
587
+ }
588
+ case KEYS.ARROW_LEFT: {
589
+ event.preventDefault();
590
+ if (field === 'minutes') {
591
+ (_e = this.hoursInput) === null || _e === void 0 ? void 0 : _e.focus();
592
+ (_f = this.hoursInput) === null || _f === void 0 ? void 0 : _f.select();
593
+ }
594
+ break;
595
+ }
596
+ case KEYS.TAB: {
597
+ // Allow default tab behavior for navigation
598
+ break;
599
+ }
600
+ default: {
601
+ // Handle digit input
602
+ if (/^\d$/.test(event.key)) {
603
+ event.preventDefault();
604
+ this.handleDigitInput(event.key, field, minVal, maxVal);
605
+ }
606
+ else {
607
+ event.preventDefault();
608
+ }
609
+ break;
610
+ }
611
+ }
612
+ }
613
+ /**
614
+ * Handles digit input for spinbuttons with auto-advance logic.
615
+ * @internal
616
+ */
617
+ handleDigitInput(digit, field, minVal, maxVal) {
618
+ if (this.pendingDigitTimeout) {
619
+ clearTimeout(this.pendingDigitTimeout);
620
+ }
621
+ this.pendingDigits += digit;
622
+ if (this.pendingDigits.length >= 2) {
623
+ // Two digits entered - commit and advance
624
+ let val = parseInt(this.pendingDigits, 10);
625
+ if (val > maxVal)
626
+ val = maxVal;
627
+ if (val < minVal)
628
+ val = minVal;
629
+ this.setSpinbuttonValue(field, val);
630
+ this.pendingDigits = '';
631
+ this.commitValue();
632
+ this.advanceToNextField(field);
633
+ }
634
+ else {
635
+ // Single digit - check if we can determine the value needs a second digit
636
+ const firstDigit = parseInt(this.pendingDigits, 10);
637
+ const maxFirstDigit = Math.floor(maxVal / 10);
638
+ if (firstDigit > maxFirstDigit) {
639
+ // Single digit already exceeds max first digit - auto-pad and advance
640
+ let val = firstDigit;
641
+ if (val > maxVal)
642
+ val = maxVal;
643
+ if (val < minVal)
644
+ val = minVal;
645
+ this.setSpinbuttonValue(field, val);
646
+ this.pendingDigits = '';
647
+ this.commitValue();
648
+ this.advanceToNextField(field);
649
+ }
650
+ else {
651
+ // Wait for second digit with timeout
652
+ this.setSpinbuttonValue(field, firstDigit);
653
+ this.pendingDigitTimeout = setTimeout(() => {
654
+ // Auto-pad with leading zero
655
+ let val = firstDigit;
656
+ if (val < minVal)
657
+ val = minVal;
658
+ this.setSpinbuttonValue(field, val);
659
+ this.pendingDigits = '';
660
+ this.commitValue();
661
+ this.advanceToNextField(field);
662
+ }, 1000);
663
+ }
664
+ }
665
+ }
666
+ /**
667
+ * Sets the value of a spinbutton field.
668
+ * @internal
669
+ */
670
+ setSpinbuttonValue(field, val) {
671
+ const padded = String(val).padStart(2, '0');
672
+ if (field === 'hours') {
673
+ this.internalHours = padded;
674
+ }
675
+ else {
676
+ this.internalMinutes = padded;
677
+ }
678
+ this.requestUpdate();
679
+ }
680
+ /**
681
+ * Advances focus to the next spinbutton field.
682
+ * @internal
683
+ */
684
+ advanceToNextField(currentField) {
685
+ if (currentField === 'hours') {
686
+ this.updateComplete
687
+ .then(() => {
688
+ var _a, _b;
689
+ (_a = this.minutesInput) === null || _a === void 0 ? void 0 : _a.focus();
690
+ (_b = this.minutesInput) === null || _b === void 0 ? void 0 : _b.select();
691
+ })
692
+ .catch(() => { });
693
+ }
694
+ else if (this.timeFormat === TIME_FORMAT.TWELVE_HOUR) {
695
+ this.updateComplete
696
+ .then(() => {
697
+ var _a, _b;
698
+ (_a = this.periodInput) === null || _a === void 0 ? void 0 : _a.focus();
699
+ (_b = this.periodInput) === null || _b === void 0 ? void 0 : _b.select();
700
+ })
701
+ .catch(() => { });
702
+ }
703
+ }
704
+ /**
705
+ * Handles focus on a spinbutton - selects all text.
706
+ * @internal
707
+ */
708
+ handleSpinbuttonFocus(event) {
709
+ const target = event.target;
710
+ target.select();
711
+ // Clear pending digits when switching fields
712
+ this.pendingDigits = '';
713
+ if (this.pendingDigitTimeout) {
714
+ clearTimeout(this.pendingDigitTimeout);
715
+ }
716
+ }
717
+ /**
718
+ * Gets the display text for the current period using locale labels.
719
+ * @internal
720
+ */
721
+ get displayPeriod() {
722
+ return this.internalPeriod === 'AM' ? this.localeAmLabel : this.localePmLabel;
723
+ }
724
+ /**
725
+ * Gets the placeholder text for the hours spinbutton.
726
+ * @internal
727
+ */
728
+ get hoursPlaceholder() {
729
+ return this.localeHoursPlaceholder;
730
+ }
731
+ /**
732
+ * Gets the placeholder text for the minutes spinbutton.
733
+ * @internal
734
+ */
735
+ get minutesPlaceholder() {
736
+ return this.localeMinutesPlaceholder;
737
+ }
738
+ /**
739
+ * Gets the placeholder text for the period spinbutton.
740
+ * @internal
741
+ */
742
+ get periodPlaceholder() {
743
+ return this.localePeriodPlaceholder;
744
+ }
745
+ /**
746
+ * Renders the dropdown time options list using mdc-option components.
747
+ * @internal
748
+ */
749
+ renderTimeOptions() {
750
+ const options = this.getTimeOptions();
751
+ const currentValue = this.internalToValue();
752
+ return options.map(option => html `
753
+ <mdc-option
754
+ label="${option.label}"
755
+ ?selected="${option.value === currentValue}"
756
+ aria-selected="${option.value === currentValue ? 'true' : 'false'}"
757
+ @click="${() => this.handleOptionClick(option.value)}"
758
+ ></mdc-option>
759
+ `);
760
+ }
761
+ render() {
762
+ const is12h = this.timeFormat === TIME_FORMAT.TWELVE_HOUR;
763
+ const hoursMin = is12h ? DEFAULTS.MIN_HOUR_12 : DEFAULTS.MIN_HOUR_24;
764
+ const hoursMax = is12h ? DEFAULTS.MAX_HOUR_12 : DEFAULTS.MAX_HOUR_24;
765
+ return html `
766
+ ${this.renderLabel()}
767
+ <div part="container">
768
+ <div
769
+ id="${TRIGGER_ID}"
770
+ part="base-container"
771
+ class="mdc-focus-ring"
772
+ @click="${this.handleSpinbuttonAreaClick}"
773
+ @keydown="${this.handleBaseKeydown}"
774
+ >
775
+ <div part="spinbutton-group">
776
+ <input
777
+ id="hours-spinbutton"
778
+ part="spinbutton"
779
+ role="spinbutton"
780
+ aria-label="${this.localeHoursLabel}"
781
+ aria-valuemin="${hoursMin}"
782
+ aria-valuemax="${hoursMax}"
783
+ aria-valuenow="${this.internalHours ? parseInt(this.internalHours, 10) : ''}"
784
+ aria-description="${this.localeSpinbuttonDescription}"
785
+ .value="${this.internalHours}"
786
+ placeholder="${this.hoursPlaceholder}"
787
+ ?disabled="${this.disabled}"
788
+ ?readonly="${this.readonly}"
789
+ tabindex="${this.disabled ? '-1' : '0'}"
790
+ @keydown="${this.handleHoursKeydown}"
791
+ @focus="${this.handleSpinbuttonFocus}"
792
+ />
793
+ <span part="separator">:</span>
794
+ <input
795
+ id="minutes-spinbutton"
796
+ part="spinbutton"
797
+ role="spinbutton"
798
+ aria-label="${this.localeMinutesLabel}"
799
+ aria-valuemin="${DEFAULTS.MIN_MINUTE}"
800
+ aria-valuemax="${DEFAULTS.MAX_MINUTE}"
801
+ aria-valuenow="${this.internalMinutes ? parseInt(this.internalMinutes, 10) : ''}"
802
+ aria-description="${this.localeSpinbuttonDescription}"
803
+ .value="${this.internalMinutes}"
804
+ placeholder="${this.minutesPlaceholder}"
805
+ ?disabled="${this.disabled}"
806
+ ?readonly="${this.readonly}"
807
+ tabindex="${this.disabled ? '-1' : '0'}"
808
+ @keydown="${this.handleMinutesKeydown}"
809
+ @focus="${this.handleSpinbuttonFocus}"
810
+ />
811
+ ${is12h
812
+ ? html `
813
+ <input
814
+ id="period-spinbutton"
815
+ part="period"
816
+ role="spinbutton"
817
+ aria-label="${this.localePeriodLabel}"
818
+ aria-valuetext="${this.displayPeriod}"
819
+ aria-description="${this.localeSpinbuttonDescription}"
820
+ .value="${this.displayPeriod || ''}"
821
+ placeholder="${this.periodPlaceholder}"
822
+ ?disabled="${this.disabled}"
823
+ ?readonly="${this.readonly}"
824
+ tabindex="${this.disabled ? '-1' : '0'}"
825
+ @keydown="${this.handlePeriodKeydown}"
826
+ @focus="${this.handleSpinbuttonFocus}"
827
+ />
828
+ `
829
+ : nothing}
830
+ </div>
831
+ <mdc-button
832
+ part="icon-container"
833
+ class="own-focus-ring"
834
+ variant="tertiary"
835
+ prefix-icon="${this.displayPopover ? ARROW_ICON.ARROW_UP : ARROW_ICON.ARROW_DOWN}"
836
+ aria-label="${this.localeShowTimePickerLabel}"
837
+ aria-expanded="${this.displayPopover ? 'true' : 'false'}"
838
+ aria-haspopup="true"
839
+ ?disabled="${this.disabled}"
840
+ size="20"
841
+ @click="${this.handleDropdownClick}"
842
+ ></mdc-button>
843
+ </div>
844
+ <input
845
+ id="${this.inputId}"
846
+ part="native-input"
847
+ name="${this.name}"
848
+ type="text"
849
+ ?disabled="${this.disabled}"
850
+ ?required="${this.required}"
851
+ ?readonly="${this.readonly}"
852
+ tabindex="-1"
853
+ aria-hidden="true"
854
+ aria-disabled="${ifDefined(this.disabled || this.softDisabled)}"
855
+ />
856
+ <mdc-popover
857
+ trigger="${TRIGGER.MANUAL}"
858
+ triggerid="${TRIGGER_ID}"
859
+ interactive
860
+ ?visible="${this.displayPopover}"
861
+ role=""
862
+ backdrop
863
+ backdrop-append-to="${ifDefined(this.backdropAppendTo)}"
864
+ append-to="${ifDefined(this.appendTo)}"
865
+ hide-on-outside-click
866
+ hide-on-escape
867
+ focus-back-to-trigger
868
+ focus-trap
869
+ disable-aria-expanded
870
+ size
871
+ ?disable-flip="${this.disableFlip}"
872
+ placement="${this.placement}"
873
+ strategy="${ifDefined(this.strategy)}"
874
+ @closebyescape="${(event) => {
875
+ if (event.target === event.currentTarget) {
876
+ this.displayPopover = false;
877
+ }
878
+ }}"
879
+ @closebyoutsideclick="${() => {
880
+ this.displayPopover = false;
881
+ }}"
882
+ exportparts="popover-content"
883
+ >
884
+ <div
885
+ id="${LISTBOX_ID}"
886
+ part="listbox"
887
+ role="listbox"
888
+ aria-label="${this.localeTimeOptionsLabel}"
889
+ @keydown="${this.handleListboxKeydown}"
890
+ >
891
+ ${this.renderTimeOptions()}
892
+ </div>
893
+ </mdc-popover>
894
+ </div>
895
+ ${this.helpText ? this.renderHelperText() : nothing}
896
+ `;
897
+ }
898
+ }
899
+ TimePicker.styles = [...FormfieldWrapper.styles, ...styles];
900
+ __decorate([
901
+ property({ type: String, reflect: true, attribute: 'time-format' }),
902
+ __metadata("design:type", String)
903
+ ], TimePicker.prototype, "timeFormat", void 0);
904
+ __decorate([
905
+ property({ type: Number, reflect: true }),
906
+ __metadata("design:type", Number)
907
+ ], TimePicker.prototype, "interval", void 0);
908
+ __decorate([
909
+ property({ type: String, reflect: true }),
910
+ __metadata("design:type", String)
911
+ ], TimePicker.prototype, "placement", void 0);
912
+ __decorate([
913
+ property({ type: String, reflect: true }),
914
+ __metadata("design:type", String)
915
+ ], TimePicker.prototype, "strategy", void 0);
916
+ __decorate([
917
+ property({ type: Boolean, reflect: true, attribute: 'disable-flip' }),
918
+ __metadata("design:type", Boolean)
919
+ ], TimePicker.prototype, "disableFlip", void 0);
920
+ __decorate([
921
+ property({ type: String, reflect: true, attribute: 'append-to' }),
922
+ __metadata("design:type", String)
923
+ ], TimePicker.prototype, "appendTo", void 0);
924
+ __decorate([
925
+ property({ type: String, reflect: true, attribute: 'backdrop-append-to' }),
926
+ __metadata("design:type", String)
927
+ ], TimePicker.prototype, "backdropAppendTo", void 0);
928
+ __decorate([
929
+ property({ type: String, reflect: true }),
930
+ __metadata("design:type", String)
931
+ ], TimePicker.prototype, "min", void 0);
932
+ __decorate([
933
+ property({ type: String, reflect: true }),
934
+ __metadata("design:type", String)
935
+ ], TimePicker.prototype, "max", void 0);
936
+ __decorate([
937
+ property({ type: String, attribute: 'locale-hours-label' }),
938
+ __metadata("design:type", Object)
939
+ ], TimePicker.prototype, "localeHoursLabel", void 0);
940
+ __decorate([
941
+ property({ type: String, attribute: 'locale-minutes-label' }),
942
+ __metadata("design:type", Object)
943
+ ], TimePicker.prototype, "localeMinutesLabel", void 0);
944
+ __decorate([
945
+ property({ type: String, attribute: 'locale-period-label' }),
946
+ __metadata("design:type", Object)
947
+ ], TimePicker.prototype, "localePeriodLabel", void 0);
948
+ __decorate([
949
+ property({ type: String, attribute: 'locale-hours-placeholder' }),
950
+ __metadata("design:type", Object)
951
+ ], TimePicker.prototype, "localeHoursPlaceholder", void 0);
952
+ __decorate([
953
+ property({ type: String, attribute: 'locale-minutes-placeholder' }),
954
+ __metadata("design:type", Object)
955
+ ], TimePicker.prototype, "localeMinutesPlaceholder", void 0);
956
+ __decorate([
957
+ property({ type: String, attribute: 'locale-period-placeholder' }),
958
+ __metadata("design:type", Object)
959
+ ], TimePicker.prototype, "localePeriodPlaceholder", void 0);
960
+ __decorate([
961
+ property({ type: String, attribute: 'locale-am-label' }),
962
+ __metadata("design:type", Object)
963
+ ], TimePicker.prototype, "localeAmLabel", void 0);
964
+ __decorate([
965
+ property({ type: String, attribute: 'locale-pm-label' }),
966
+ __metadata("design:type", Object)
967
+ ], TimePicker.prototype, "localePmLabel", void 0);
968
+ __decorate([
969
+ property({ type: String, attribute: 'locale-show-time-picker-label' }),
970
+ __metadata("design:type", Object)
971
+ ], TimePicker.prototype, "localeShowTimePickerLabel", void 0);
972
+ __decorate([
973
+ property({ type: String, attribute: 'locale-time-options-label' }),
974
+ __metadata("design:type", Object)
975
+ ], TimePicker.prototype, "localeTimeOptionsLabel", void 0);
976
+ __decorate([
977
+ property({ type: String, attribute: 'locale-spinbutton-description' }),
978
+ __metadata("design:type", Object)
979
+ ], TimePicker.prototype, "localeSpinbuttonDescription", void 0);
980
+ __decorate([
981
+ query('mdc-button[part="icon-container"]'),
982
+ __metadata("design:type", HTMLElement)
983
+ ], TimePicker.prototype, "dropdownButton", void 0);
984
+ __decorate([
985
+ query('#hours-spinbutton'),
986
+ __metadata("design:type", HTMLInputElement)
987
+ ], TimePicker.prototype, "hoursInput", void 0);
988
+ __decorate([
989
+ query('#minutes-spinbutton'),
990
+ __metadata("design:type", HTMLInputElement)
991
+ ], TimePicker.prototype, "minutesInput", void 0);
992
+ __decorate([
993
+ query('#period-spinbutton'),
994
+ __metadata("design:type", HTMLInputElement)
995
+ ], TimePicker.prototype, "periodInput", void 0);
996
+ __decorate([
997
+ state(),
998
+ __metadata("design:type", Object)
999
+ ], TimePicker.prototype, "displayPopover", void 0);
1000
+ __decorate([
1001
+ state(),
1002
+ __metadata("design:type", Object)
1003
+ ], TimePicker.prototype, "internalHours", void 0);
1004
+ __decorate([
1005
+ state(),
1006
+ __metadata("design:type", Object)
1007
+ ], TimePicker.prototype, "internalMinutes", void 0);
1008
+ __decorate([
1009
+ state(),
1010
+ __metadata("design:type", String)
1011
+ ], TimePicker.prototype, "internalPeriod", void 0);
1012
+ __decorate([
1013
+ state(),
1014
+ __metadata("design:type", Object)
1015
+ ], TimePicker.prototype, "focusedOptionIndex", void 0);
1016
+ export default TimePicker;