tyrell-components 1.0.0-TC52 → 1.0.0-TC53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/css/tyrell-theme.css +25 -2
  2. package/css/tyrell.css +4 -0
  3. package/dist/tyrell-colors-static.css +1 -316
  4. package/dist/tyrell-theme.css +1 -1443
  5. package/dist/tyrell.css +1 -1390
  6. package/dist/tyrell.js +1 -1
  7. package/lib/base/ty-component.js +196 -196
  8. package/lib/components/button.d.ts +1 -1
  9. package/lib/components/button.js +260 -259
  10. package/lib/components/calendar-month.d.ts +2 -2
  11. package/lib/components/calendar-month.js +426 -416
  12. package/lib/components/calendar-navigation.js +238 -238
  13. package/lib/components/calendar.js +605 -602
  14. package/lib/components/checkbox.d.ts +1 -1
  15. package/lib/components/checkbox.js +273 -272
  16. package/lib/components/copy.d.ts +1 -1
  17. package/lib/components/copy.js +279 -278
  18. package/lib/components/date-picker.d.ts +1 -1
  19. package/lib/components/date-picker.js +1103 -1108
  20. package/lib/components/file-upload.js +404 -404
  21. package/lib/components/icon.js +141 -141
  22. package/lib/components/input.d.ts +1 -1
  23. package/lib/components/input.js +593 -596
  24. package/lib/components/modal.js +319 -319
  25. package/lib/components/option.js +167 -167
  26. package/lib/components/popup.js +233 -233
  27. package/lib/components/radio.d.ts +2 -2
  28. package/lib/components/radio.js +407 -406
  29. package/lib/components/resize-observer.js +67 -67
  30. package/lib/components/scroll-container.js +294 -294
  31. package/lib/components/select.d.ts +1 -1
  32. package/lib/components/select.js +1524 -1532
  33. package/lib/components/selected-tags.js +184 -184
  34. package/lib/components/step.js +62 -62
  35. package/lib/components/switch.d.ts +1 -1
  36. package/lib/components/switch.js +214 -213
  37. package/lib/components/tab.js +62 -62
  38. package/lib/components/tabs.js +507 -507
  39. package/lib/components/tag.d.ts +1 -1
  40. package/lib/components/tag.js +236 -235
  41. package/lib/components/textarea.d.ts +1 -1
  42. package/lib/components/textarea.js +457 -456
  43. package/lib/components/tooltip.js +283 -283
  44. package/lib/components/wizard.js +384 -384
  45. package/lib/icons/heroicons/micro.js +392 -392
  46. package/lib/icons/heroicons/mini.js +382 -382
  47. package/lib/icons/heroicons/outline.js +337 -337
  48. package/lib/icons/heroicons/solid.js +407 -407
  49. package/lib/icons/lucide.js +22843 -22843
  50. package/lib/icons/material/filled.js +9653 -9653
  51. package/lib/icons/material/outlined.js +10271 -10271
  52. package/lib/icons/material/round.js +10495 -10495
  53. package/lib/icons/material/sharp.js +9905 -9905
  54. package/lib/icons/material/two-tone.js +12792 -12792
  55. package/lib/index.js +21 -21
  56. package/lib/styles/button.js +176 -288
  57. package/lib/styles/calendar-month.js +123 -183
  58. package/lib/styles/calendar-navigation.js +60 -79
  59. package/lib/styles/calendar.js +8 -9
  60. package/lib/styles/checkbox.js +45 -72
  61. package/lib/styles/copy.js +67 -91
  62. package/lib/styles/custom-scrollbar.js +80 -100
  63. package/lib/styles/date-picker.js +261 -331
  64. package/lib/styles/field-size.d.ts +6 -0
  65. package/lib/styles/field-size.js +91 -0
  66. package/lib/styles/file-upload.js +139 -169
  67. package/lib/styles/icon.js +92 -156
  68. package/lib/styles/input.js +125 -241
  69. package/lib/styles/modal.js +67 -102
  70. package/lib/styles/option.d.ts +1 -1
  71. package/lib/styles/option.js +47 -67
  72. package/lib/styles/popup.js +34 -65
  73. package/lib/styles/radio.js +69 -96
  74. package/lib/styles/resize-observer.js +2 -5
  75. package/lib/styles/scroll-container.js +100 -130
  76. package/lib/styles/select-base.js +525 -778
  77. package/lib/styles/select.js +140 -228
  78. package/lib/styles/step.js +22 -29
  79. package/lib/styles/switch.js +34 -55
  80. package/lib/styles/tab.js +22 -29
  81. package/lib/styles/tabs.js +171 -252
  82. package/lib/styles/tag.js +135 -188
  83. package/lib/styles/textarea.d.ts +1 -1
  84. package/lib/styles/textarea.js +205 -271
  85. package/lib/styles/tooltip.js +3 -3
  86. package/lib/styles/wizard.js +170 -226
  87. package/lib/types/common.d.ts +2 -0
  88. package/lib/types/common.js +6 -5
  89. package/lib/utils/calendar-utils.js +100 -100
  90. package/lib/utils/custom-scrollbar.js +272 -272
  91. package/lib/utils/flavor-sheet.js +14 -14
  92. package/lib/utils/icon-registry.js +127 -127
  93. package/lib/utils/loader-registry.js +3 -3
  94. package/lib/utils/locale.js +34 -34
  95. package/lib/utils/mobile.js +1 -1
  96. package/lib/utils/number-format.js +54 -54
  97. package/lib/utils/positioning.js +212 -212
  98. package/lib/utils/property-capture.js +36 -36
  99. package/lib/utils/property-manager.js +125 -125
  100. package/lib/utils/resize-observer.js +23 -23
  101. package/lib/utils/scroll-lock.js +57 -57
  102. package/lib/utils/styles.js +17 -17
  103. package/lib/version.d.ts +1 -1
  104. package/lib/version.js +1 -1
  105. package/package.json +4 -4
@@ -6,1149 +6,1144 @@ import { computeAnchoredPosition, placementToAnchored } from '../utils/positioni
6
6
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
7
7
  import { isMobileTouch } from '../utils/mobile.js';
8
8
  import { TyComponent } from '../base/ty-component.js';
9
+ import { DEFAULT_SIZE } from '../types/common.js';
9
10
  const CALENDAR_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='16' height='16' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='4' width='18' height='18' rx='2' ry='2'></rect><line x1='16' y1='2' x2='16' y2='6'></line><line x1='8' y1='2' x2='8' y2='6'></line><line x1='3' y1='10' x2='21' y2='10'></line></svg>`;
10
11
  const CLEAR_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='14' height='14' xmlns='http://www.w3.org/2000/svg'><line x1='18' y1='6' x2='6' y2='18'></line><line x1='6' y1='6' x2='18' y2='18'></line></svg>`;
11
12
  const SCHEDULE_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='16' height='16' xmlns='http://www.w3.org/2000/svg'><circle cx='12' cy='12' r='10'></circle><polyline points='12,6 12,12 16,14'></polyline></svg>`;
12
13
  const elementIds = new WeakMap();
13
14
  let elementIdCounter = 0;
14
15
  function getElementHash(element) {
15
- let id = elementIds.get(element);
16
- if (id === undefined) {
17
- id = ++elementIdCounter;
18
- elementIds.set(element, id);
19
- }
20
- return id;
16
+ let id = elementIds.get(element);
17
+ if (id === undefined) {
18
+ id = ++elementIdCounter;
19
+ elementIds.set(element, id);
20
+ }
21
+ return id;
21
22
  }
22
23
  function parseValue(value, withTime) {
23
- if (!value)
24
- return null;
25
- let dateObj = null;
26
- if (typeof value === 'string') {
27
- if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
28
- const [yearStr, monthStr, dayStr] = value.split('-');
29
- const year = parseInt(yearStr, 10);
30
- const month = parseInt(monthStr, 10) - 1;
31
- const day = parseInt(dayStr, 10);
32
- dateObj = new Date(year, month, day, 0, 0, 0, 0);
33
- }
34
- else {
35
- dateObj = new Date(value);
36
- }
37
- }
38
- else if (typeof value === 'number') {
39
- dateObj = new Date(value);
40
- }
41
- if (!dateObj || isNaN(dateObj.getTime())) {
42
- return null;
43
- }
44
- const components = {
45
- year: dateObj.getFullYear(),
46
- month: dateObj.getMonth() + 1,
47
- day: dateObj.getDate(),
48
- hour: withTime ? dateObj.getHours() : 0,
49
- minute: withTime ? dateObj.getMinutes() : 0,
50
- };
51
- return components;
24
+ if (!value)
25
+ return null;
26
+ let dateObj = null;
27
+ if (typeof value === 'string') {
28
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
29
+ const [yearStr, monthStr, dayStr] = value.split('-');
30
+ const year = parseInt(yearStr, 10);
31
+ const month = parseInt(monthStr, 10) - 1;
32
+ const day = parseInt(dayStr, 10);
33
+ dateObj = new Date(year, month, day, 0, 0, 0, 0);
34
+ }
35
+ else {
36
+ dateObj = new Date(value);
37
+ }
38
+ }
39
+ else if (typeof value === 'number') {
40
+ dateObj = new Date(value);
41
+ }
42
+ if (!dateObj || isNaN(dateObj.getTime())) {
43
+ return null;
44
+ }
45
+ const components = {
46
+ year: dateObj.getFullYear(),
47
+ month: dateObj.getMonth() + 1,
48
+ day: dateObj.getDate(),
49
+ hour: withTime ? dateObj.getHours() : 0,
50
+ minute: withTime ? dateObj.getMinutes() : 0,
51
+ };
52
+ return components;
52
53
  }
53
54
  function componentsToDateObject(components) {
54
- const { year, month, day, hour, minute } = components;
55
- if (!year || !month || !day)
56
- return null;
57
- return new Date(year, month - 1, day, hour || 0, minute || 0, 0, 0);
55
+ const { year, month, day, hour, minute } = components;
56
+ if (!year || !month || !day)
57
+ return null;
58
+ return new Date(year, month - 1, day, hour || 0, minute || 0, 0, 0);
58
59
  }
59
60
  function componentsToOutputValue(components) {
60
- if (!components.year || !components.month || !components.day) {
61
- return null;
62
- }
63
- const dateObj = componentsToDateObject(components);
64
- return dateObj ? dateObj.toISOString() : null;
61
+ if (!components.year || !components.month || !components.day) {
62
+ return null;
63
+ }
64
+ const dateObj = componentsToDateObject(components);
65
+ return dateObj ? dateObj.toISOString() : null;
65
66
  }
66
67
  function componentsToLocalValue(components, withTime) {
67
- if (!components.year || !components.month || !components.day) {
68
- return null;
69
- }
70
- const year = components.year.toString().padStart(4, '0');
71
- const month = components.month.toString().padStart(2, '0');
72
- const day = components.day.toString().padStart(2, '0');
73
- if (withTime) {
74
- const hour = (components.hour || 0).toString().padStart(2, '0');
75
- const minute = (components.minute || 0).toString().padStart(2, '0');
76
- return `${year}-${month}-${day}T${hour}:${minute}`;
77
- }
78
- else {
79
- return `${year}-${month}-${day}`;
80
- }
68
+ if (!components.year || !components.month || !components.day) {
69
+ return null;
70
+ }
71
+ const year = components.year.toString().padStart(4, '0');
72
+ const month = components.month.toString().padStart(2, '0');
73
+ const day = components.day.toString().padStart(2, '0');
74
+ if (withTime) {
75
+ const hour = (components.hour || 0).toString().padStart(2, '0');
76
+ const minute = (components.minute || 0).toString().padStart(2, '0');
77
+ return `${year}-${month}-${day}T${hour}:${minute}`;
78
+ }
79
+ else {
80
+ return `${year}-${month}-${day}`;
81
+ }
81
82
  }
82
83
  function formatDisplayValue(components, formatType, locale, withTime) {
83
- if (!components.year || !components.month || !components.day) {
84
- return null;
85
- }
86
- const dateObj = componentsToDateObject(components);
87
- if (!dateObj)
88
- return null;
89
- const options = {
90
- dateStyle: formatType || 'long',
91
- };
92
- if (withTime) {
93
- options.timeStyle = 'short';
94
- }
95
- const formatter = new Intl.DateTimeFormat(locale, options);
96
- return formatter.format(dateObj);
84
+ if (!components.year || !components.month || !components.day) {
85
+ return null;
86
+ }
87
+ const dateObj = componentsToDateObject(components);
88
+ if (!dateObj)
89
+ return null;
90
+ const options = {
91
+ dateStyle: formatType || 'long',
92
+ };
93
+ if (withTime) {
94
+ options.timeStyle = 'short';
95
+ }
96
+ const formatter = new Intl.DateTimeFormat(locale, options);
97
+ return formatter.format(dateObj);
97
98
  }
98
99
  function parseTimeComponents(rawDigits) {
99
- if (!rawDigits || rawDigits.length !== 4)
100
- return null;
101
- const hourStr = rawDigits.substring(0, 2);
102
- const minuteStr = rawDigits.substring(2, 4);
103
- const hour = parseInt(hourStr, 10);
104
- const minute = parseInt(minuteStr, 10);
105
- if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
106
- return null;
107
- }
108
- return { hour, minute };
100
+ if (!rawDigits || rawDigits.length !== 4)
101
+ return null;
102
+ const hourStr = rawDigits.substring(0, 2);
103
+ const minuteStr = rawDigits.substring(2, 4);
104
+ const hour = parseInt(hourStr, 10);
105
+ const minute = parseInt(minuteStr, 10);
106
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
107
+ return null;
108
+ }
109
+ return { hour, minute };
109
110
  }
110
111
  function validateTimeDigit(digit, position, currentDigits) {
111
- switch (position) {
112
- case 0:
113
- return digit <= 2;
114
- case 1: {
115
- const firstHour = parseInt(currentDigits[0], 10);
116
- return firstHour === 2 ? digit <= 3 : true;
117
- }
118
- case 3:
119
- return digit <= 5;
120
- case 4:
121
- return true;
122
- default:
123
- return false;
124
- }
112
+ switch (position) {
113
+ case 0:
114
+ return digit <= 2;
115
+ case 1: {
116
+ const firstHour = parseInt(currentDigits[0], 10);
117
+ return firstHour === 2 ? digit <= 3 : true;
118
+ }
119
+ case 3:
120
+ return digit <= 5;
121
+ case 4:
122
+ return true;
123
+ default:
124
+ return false;
125
+ }
125
126
  }
126
127
  function formatTimeDisplay(hour, minute) {
127
- const hourStr = hour.toString().padStart(2, '0');
128
- const minuteStr = minute.toString().padStart(2, '0');
129
- return `${hourStr}:${minuteStr}`;
128
+ const hourStr = hour.toString().padStart(2, '0');
129
+ const minuteStr = minute.toString().padStart(2, '0');
130
+ return `${hourStr}:${minuteStr}`;
130
131
  }
131
132
  function findNextEditablePosition(currentPos) {
132
- switch (currentPos) {
133
- case 0: return 1;
134
- case 1: return 3;
135
- case 3: return 4;
136
- case 4: return 5;
137
- case 5: return 5;
138
- default: return currentPos;
139
- }
133
+ switch (currentPos) {
134
+ case 0: return 1;
135
+ case 1: return 3;
136
+ case 3: return 4;
137
+ case 4: return 5;
138
+ case 5: return 5;
139
+ default: return currentPos;
140
+ }
140
141
  }
141
142
  function findPrevEditablePosition(currentPos) {
142
- switch (currentPos) {
143
- case 5: return 4;
144
- case 4: return 3;
145
- case 3: return 1;
146
- case 1: return 0;
147
- case 0: return 0;
148
- default: return currentPos;
149
- }
143
+ switch (currentPos) {
144
+ case 5: return 4;
145
+ case 4: return 3;
146
+ case 3: return 1;
147
+ case 1: return 0;
148
+ case 0: return 0;
149
+ default: return currentPos;
150
+ }
150
151
  }
151
152
  function positionToRawDigitIndex(internalPos) {
152
- switch (internalPos) {
153
- case 0: return 0;
154
- case 1: return 1;
155
- case 3: return 2;
156
- case 4: return 3;
157
- default: return 0;
158
- }
153
+ switch (internalPos) {
154
+ case 0: return 0;
155
+ case 1: return 1;
156
+ case 3: return 2;
157
+ case 4: return 3;
158
+ default: return 0;
159
+ }
159
160
  }
160
161
  class TimeInput {
161
- constructor(element, datePickerElement, hour = 0, minute = 0) {
162
- this.element = element;
163
- this.datePickerElement = datePickerElement;
164
- const display = formatTimeDisplay(hour, minute);
165
- const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
166
- this.state = {
167
- hour,
168
- minute,
169
- caretPosition: 0,
170
- displayValue: display,
171
- rawDigits,
172
- };
173
- this.element.value = display;
174
- this.setupEventListeners();
175
- }
176
- setupEventListeners() {
177
- this.element.addEventListener('keydown', (e) => this.handleKeyDown(e));
178
- this.element.addEventListener('input', (e) => this.handleInput(e));
179
- this.element.addEventListener('click', () => this.handleClick());
180
- this.element.addEventListener('focus', () => this.handleFocus());
181
- }
182
- handleKeyDown(event) {
183
- const key = event.key;
184
- switch (key) {
185
- case 'ArrowRight':
186
- this.handleArrowRight(event);
187
- break;
188
- case 'ArrowLeft':
189
- this.handleArrowLeft(event);
190
- break;
191
- case 'Backspace':
192
- this.handleBackspace(event);
193
- break;
194
- case 'Delete':
195
- this.handleDelete(event);
196
- break;
197
- case 'Home':
198
- event.preventDefault();
199
- this.updateState({ caretPosition: 0 });
200
- break;
201
- case 'End':
202
- event.preventDefault();
203
- this.updateState({ caretPosition: 5 });
204
- break;
205
- case 'Tab':
206
- break;
207
- default:
208
- if (/^\d$/.test(key)) {
209
- this.handleDigitInput(event, parseInt(key, 10));
210
- }
211
- break;
212
- }
213
- }
214
- handleInput(event) {
215
- event.preventDefault();
216
- }
217
- handleClick() {
218
- this.updateState({ caretPosition: 0 });
219
- }
220
- handleFocus() {
221
- this.updateState({ caretPosition: 0 });
222
- }
223
- handleArrowRight(event) {
224
- event.preventDefault();
225
- const nextPos = findNextEditablePosition(this.state.caretPosition);
226
- this.updateState({ caretPosition: nextPos });
227
- }
228
- handleArrowLeft(event) {
229
- event.preventDefault();
230
- const prevPos = findPrevEditablePosition(this.state.caretPosition);
231
- this.updateState({ caretPosition: prevPos });
232
- }
233
- handleDigitInput(event, digit) {
234
- event.preventDefault();
235
- const currentPos = this.state.caretPosition;
236
- if (![0, 1, 3, 4].includes(currentPos))
237
- return;
238
- if (!validateTimeDigit(digit, currentPos, this.state.rawDigits))
239
- return;
240
- const newState = this.replaceDigitAtPosition(currentPos, digit);
241
- if (!newState)
242
- return;
243
- const nextPos = findNextEditablePosition(currentPos);
244
- this.updateState({ ...newState, caretPosition: nextPos });
245
- this.notifyTimeChange();
246
- }
247
- handleBackspace(event) {
248
- event.preventDefault();
249
- const currentPos = this.state.caretPosition;
250
- if (currentPos === 0)
251
- return;
252
- const targetPos = currentPos === 1 ? 0 :
253
- currentPos === 3 ? 1 :
254
- currentPos === 4 ? 3 :
255
- currentPos === 5 ? 4 : 0;
256
- const newState = this.zeroDigitAtPosition(targetPos);
257
- if (!newState)
258
- return;
259
- this.updateState({ ...newState, caretPosition: targetPos });
260
- this.notifyTimeChange();
261
- }
262
- handleDelete(event) {
263
- event.preventDefault();
264
- const currentPos = this.state.caretPosition;
265
- if (![0, 1, 3, 4].includes(currentPos))
266
- return;
267
- const newState = this.zeroDigitAtPosition(currentPos);
268
- if (!newState)
269
- return;
270
- this.updateState({ ...newState, caretPosition: currentPos });
271
- this.notifyTimeChange();
272
- }
273
- replaceDigitAtPosition(position, newDigit) {
274
- const rawIndex = positionToRawDigitIndex(position);
275
- const digitsArray = this.state.rawDigits.split('');
276
- digitsArray[rawIndex] = newDigit.toString();
277
- const newDigits = digitsArray.join('');
278
- const parsed = parseTimeComponents(newDigits);
279
- if (!parsed)
280
- return null;
281
- return {
282
- rawDigits: newDigits,
283
- hour: parsed.hour,
284
- minute: parsed.minute,
285
- displayValue: formatTimeDisplay(parsed.hour, parsed.minute),
286
- };
287
- }
288
- zeroDigitAtPosition(position) {
289
- return this.replaceDigitAtPosition(position, 0);
290
- }
291
- updateState(updates) {
292
- this.state = { ...this.state, ...updates };
293
- this.element.value = this.state.displayValue;
294
- const caretPos = this.state.caretPosition;
295
- const actualPos = caretPos === 0 ? 0 :
296
- caretPos === 1 ? 1 :
297
- caretPos === 2 ? 3 :
298
- caretPos === 3 ? 3 :
299
- caretPos === 4 ? 4 :
300
- caretPos === 5 ? 5 : caretPos;
301
- this.element.setSelectionRange(actualPos, actualPos);
302
- }
303
- notifyTimeChange() {
304
- this.datePickerElement.handleTimeInputChange(this.state.hour, this.state.minute);
305
- }
306
- getTime() {
307
- return {
308
- hour: this.state.hour,
309
- minute: this.state.minute,
310
- };
311
- }
312
- setTime(hour, minute) {
313
- const display = formatTimeDisplay(hour, minute);
314
- const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
315
- this.state = {
316
- ...this.state,
317
- hour,
318
- minute,
319
- displayValue: display,
320
- rawDigits,
321
- };
322
- this.element.value = display;
323
- }
162
+ constructor(element, datePickerElement, hour = 0, minute = 0) {
163
+ this.element = element;
164
+ this.datePickerElement = datePickerElement;
165
+ const display = formatTimeDisplay(hour, minute);
166
+ const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
167
+ this.state = {
168
+ hour,
169
+ minute,
170
+ caretPosition: 0,
171
+ displayValue: display,
172
+ rawDigits,
173
+ };
174
+ this.element.value = display;
175
+ this.setupEventListeners();
176
+ }
177
+ setupEventListeners() {
178
+ this.element.addEventListener('keydown', (e) => this.handleKeyDown(e));
179
+ this.element.addEventListener('input', (e) => this.handleInput(e));
180
+ this.element.addEventListener('click', () => this.handleClick());
181
+ this.element.addEventListener('focus', () => this.handleFocus());
182
+ }
183
+ handleKeyDown(event) {
184
+ const key = event.key;
185
+ switch (key) {
186
+ case 'ArrowRight':
187
+ this.handleArrowRight(event);
188
+ break;
189
+ case 'ArrowLeft':
190
+ this.handleArrowLeft(event);
191
+ break;
192
+ case 'Backspace':
193
+ this.handleBackspace(event);
194
+ break;
195
+ case 'Delete':
196
+ this.handleDelete(event);
197
+ break;
198
+ case 'Home':
199
+ event.preventDefault();
200
+ this.updateState({ caretPosition: 0 });
201
+ break;
202
+ case 'End':
203
+ event.preventDefault();
204
+ this.updateState({ caretPosition: 5 });
205
+ break;
206
+ case 'Tab':
207
+ break;
208
+ default:
209
+ if (/^\d$/.test(key)) {
210
+ this.handleDigitInput(event, parseInt(key, 10));
211
+ }
212
+ break;
213
+ }
214
+ }
215
+ handleInput(event) {
216
+ event.preventDefault();
217
+ }
218
+ handleClick() {
219
+ this.updateState({ caretPosition: 0 });
220
+ }
221
+ handleFocus() {
222
+ this.updateState({ caretPosition: 0 });
223
+ }
224
+ handleArrowRight(event) {
225
+ event.preventDefault();
226
+ const nextPos = findNextEditablePosition(this.state.caretPosition);
227
+ this.updateState({ caretPosition: nextPos });
228
+ }
229
+ handleArrowLeft(event) {
230
+ event.preventDefault();
231
+ const prevPos = findPrevEditablePosition(this.state.caretPosition);
232
+ this.updateState({ caretPosition: prevPos });
233
+ }
234
+ handleDigitInput(event, digit) {
235
+ event.preventDefault();
236
+ const currentPos = this.state.caretPosition;
237
+ if (![0, 1, 3, 4].includes(currentPos))
238
+ return;
239
+ if (!validateTimeDigit(digit, currentPos, this.state.rawDigits))
240
+ return;
241
+ const newState = this.replaceDigitAtPosition(currentPos, digit);
242
+ if (!newState)
243
+ return;
244
+ const nextPos = findNextEditablePosition(currentPos);
245
+ this.updateState({ ...newState, caretPosition: nextPos });
246
+ this.notifyTimeChange();
247
+ }
248
+ handleBackspace(event) {
249
+ event.preventDefault();
250
+ const currentPos = this.state.caretPosition;
251
+ if (currentPos === 0)
252
+ return;
253
+ const targetPos = currentPos === 1 ? 0 :
254
+ currentPos === 3 ? 1 :
255
+ currentPos === 4 ? 3 :
256
+ currentPos === 5 ? 4 : 0;
257
+ const newState = this.zeroDigitAtPosition(targetPos);
258
+ if (!newState)
259
+ return;
260
+ this.updateState({ ...newState, caretPosition: targetPos });
261
+ this.notifyTimeChange();
262
+ }
263
+ handleDelete(event) {
264
+ event.preventDefault();
265
+ const currentPos = this.state.caretPosition;
266
+ if (![0, 1, 3, 4].includes(currentPos))
267
+ return;
268
+ const newState = this.zeroDigitAtPosition(currentPos);
269
+ if (!newState)
270
+ return;
271
+ this.updateState({ ...newState, caretPosition: currentPos });
272
+ this.notifyTimeChange();
273
+ }
274
+ replaceDigitAtPosition(position, newDigit) {
275
+ const rawIndex = positionToRawDigitIndex(position);
276
+ const digitsArray = this.state.rawDigits.split('');
277
+ digitsArray[rawIndex] = newDigit.toString();
278
+ const newDigits = digitsArray.join('');
279
+ const parsed = parseTimeComponents(newDigits);
280
+ if (!parsed)
281
+ return null;
282
+ return {
283
+ rawDigits: newDigits,
284
+ hour: parsed.hour,
285
+ minute: parsed.minute,
286
+ displayValue: formatTimeDisplay(parsed.hour, parsed.minute),
287
+ };
288
+ }
289
+ zeroDigitAtPosition(position) {
290
+ return this.replaceDigitAtPosition(position, 0);
291
+ }
292
+ updateState(updates) {
293
+ this.state = { ...this.state, ...updates };
294
+ this.element.value = this.state.displayValue;
295
+ const caretPos = this.state.caretPosition;
296
+ const actualPos = caretPos === 0 ? 0 :
297
+ caretPos === 1 ? 1 :
298
+ caretPos === 2 ? 3 :
299
+ caretPos === 3 ? 3 :
300
+ caretPos === 4 ? 4 :
301
+ caretPos === 5 ? 5 : caretPos;
302
+ this.element.setSelectionRange(actualPos, actualPos);
303
+ }
304
+ notifyTimeChange() {
305
+ this.datePickerElement.handleTimeInputChange(this.state.hour, this.state.minute);
306
+ }
307
+ getTime() {
308
+ return {
309
+ hour: this.state.hour,
310
+ minute: this.state.minute,
311
+ };
312
+ }
313
+ setTime(hour, minute) {
314
+ const display = formatTimeDisplay(hour, minute);
315
+ const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
316
+ this.state = {
317
+ ...this.state,
318
+ hour,
319
+ minute,
320
+ displayValue: display,
321
+ rawDigits,
322
+ };
323
+ this.element.value = display;
324
+ }
324
325
  }
325
326
  export class TyDatePicker extends TyComponent {
326
- constructor() {
327
- super();
328
- this._state = {
329
- withTime: false,
330
- open: false,
331
- };
332
- this._closeTimestamp = 0;
333
- this._customFlavorSheet = null;
334
- const shadow = this.shadowRoot;
335
- ensureStyles(shadow, { css: datePickerStyles, id: 'ty-date-picker' });
336
- }
337
- onConnect() {
338
- this.initializeState();
339
- this.render();
340
- this._localeObserver = observeLocaleChanges(this, () => {
341
- this.render();
342
- });
343
- this._syncCustomFlavor();
344
- }
345
- _syncCustomFlavor() {
346
- const flavor = this.flavor === 'default' ? 'neutral' : this.flavor;
347
- this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, flavor, ({ base }) => datePickerCustomFlavorCss(base));
348
- }
349
- onDisconnect() {
350
- if (this._localeObserver) {
351
- this._localeObserver();
352
- this._localeObserver = undefined;
353
- }
354
- this.cleanup();
355
- }
356
- onPropertiesChanged(changes) {
357
- for (const { name, newValue } of changes) {
358
- switch (name) {
359
- case 'flavor':
360
- this._syncCustomFlavor();
361
- break;
362
- case 'size':
363
- case 'label':
364
- case 'placeholder':
365
- case 'required':
366
- case 'disabled':
367
- case 'clearable':
368
- case 'format':
369
- case 'locale':
370
- break;
371
- case 'value': {
372
- const newComponents = parseValue(newValue, this._state.withTime);
373
- if (newComponents === null) {
374
- const hasDate = this._state.year !== undefined ||
375
- this._state.month !== undefined ||
376
- this._state.day !== undefined;
377
- if (hasDate) {
378
- this._state = {
379
- withTime: this._state.withTime,
380
- open: this._state.open,
381
- };
382
- }
383
- break;
384
- }
385
- const currentComponents = {
386
- year: this._state.year,
387
- month: this._state.month,
388
- day: this._state.day,
389
- hour: this._state.hour,
390
- minute: this._state.minute,
391
- };
392
- const changed = newComponents?.year !== currentComponents.year ||
393
- newComponents?.month !== currentComponents.month ||
394
- newComponents?.day !== currentComponents.day ||
395
- newComponents?.hour !== currentComponents.hour ||
396
- newComponents?.minute !== currentComponents.minute;
397
- if (changed) {
398
- this._state = {
399
- ...this._state,
400
- ...newComponents,
401
- };
402
- }
403
- break;
404
- }
405
- case 'with-time': {
406
- const newWithTime = newValue;
407
- const oldWithTime = this._state.withTime;
408
- if (newWithTime !== oldWithTime) {
409
- this._state.withTime = newWithTime;
410
- const hasDate = this._state.year !== undefined &&
411
- this._state.month !== undefined &&
412
- this._state.day !== undefined;
413
- if (hasDate) {
414
- this.syncFormValue();
415
- }
416
- }
417
- break;
418
- }
419
- }
420
- }
421
- }
422
- getFormValue() {
423
- return componentsToOutputValue(this.getComponents());
424
- }
425
- getComponents() {
426
- return {
427
- year: this._state.year,
428
- month: this._state.month,
429
- day: this._state.day,
430
- hour: this._state.hour,
431
- minute: this._state.minute,
432
- };
433
- }
434
- hasDate() {
435
- return !!(this._state.year && this._state.month && this._state.day);
436
- }
437
- getFormattedValue(components) {
438
- const c = components || this.getComponents();
439
- if (!c.year || !c.month || !c.day)
440
- return null;
441
- return formatDisplayValue(c, this.getProperty('format') || 'long', getEffectiveLocale(this, this.getProperty('locale')), this._state.withTime);
442
- }
443
- initializeState() {
444
- const valueAttr = this.getProperty('value');
445
- const withTime = this.getProperty('with-time');
446
- const components = parseValue(valueAttr, withTime);
447
- if (components === null) {
448
- this._state = {
449
- withTime,
450
- open: false,
451
- };
452
- }
453
- else {
454
- this._state = {
455
- ...components,
456
- withTime,
457
- open: false,
458
- };
459
- }
460
- this.syncFormValue();
461
- }
462
- updateState(updates, forceSync = false) {
463
- this._state = { ...this._state, ...updates };
464
- const shouldSync = forceSync || !this._state.open || !this._state.withTime;
465
- if (shouldSync) {
466
- this.syncFormValue();
467
- }
468
- }
469
- syncFormValue() {
470
- const utcValue = this.getFormValue();
471
- const currentValue = this.getProperty('value');
472
- if (utcValue !== currentValue) {
473
- if (utcValue) {
474
- this.setProperty('value', utcValue);
475
- }
476
- else {
477
- this.setProperty('value', null);
478
- }
479
- }
480
- }
481
- handleTimeInputChange(hour, minute) {
482
- if (!this.hasDate())
483
- return;
484
- const components = { ...this.getComponents(), hour, minute };
485
- this.updateState(components, true);
486
- this.emitChangeEvent(components, 'time-change');
487
- }
488
- emitChangeEvent(components, source) {
489
- const utcValue = components ? componentsToOutputValue(components) : null;
490
- const localValue = components ? componentsToLocalValue(components, this._state.withTime) : null;
491
- const milliseconds = components ? componentsToDateObject(components)?.getTime() ?? null : null;
492
- const formatted = components ? this.getFormattedValue(components) : null;
493
- const detail = {
494
- value: utcValue,
495
- localValue,
496
- milliseconds,
497
- formatted,
498
- source,
499
- };
500
- const event = new CustomEvent('change', {
501
- detail,
502
- bubbles: true,
503
- cancelable: true,
504
- });
505
- this.dispatchEvent(event);
506
- }
507
- get size() { return this.getProperty('size'); }
508
- set size(v) { this.setProperty('size', v); }
509
- get placement() { return this.getProperty('placement'); }
510
- set placement(v) { this.setProperty('placement', v); }
511
- get flavor() { return this.getProperty('flavor'); }
512
- set flavor(v) { this.setProperty('flavor', v); }
513
- get label() { return this.getProperty('label'); }
514
- set label(v) { this.setProperty('label', v); }
515
- get placeholder() { return this.getProperty('placeholder'); }
516
- set placeholder(v) { this.setProperty('placeholder', v); }
517
- get name() { return this.getProperty('name'); }
518
- set name(v) { this.setProperty('name', v); }
519
- get format() { return this.getProperty('format'); }
520
- set format(v) { this.setProperty('format', v); }
521
- get locale() { return this.getProperty('locale'); }
522
- set locale(v) { this.setProperty('locale', v); }
523
- get required() { return this.getProperty('required'); }
524
- set required(v) { this.setProperty('required', v); }
525
- get disabled() { return this.getProperty('disabled'); }
526
- set disabled(v) { this.setProperty('disabled', v); }
527
- get clearable() { return this.getProperty('clearable'); }
528
- set clearable(v) { this.setProperty('clearable', v); }
529
- get min() { return this.getProperty('min'); }
530
- set min(v) { this.setProperty('min', v ?? ''); }
531
- get max() { return this.getProperty('max'); }
532
- set max(v) { this.setProperty('max', v ?? ''); }
533
- get withTime() { return this.getProperty('with-time'); }
534
- set withTime(v) { this.setProperty('with-time', v); }
535
- get value() {
536
- return this.getProperty('value') || null;
537
- }
538
- set value(val) {
539
- if (val === null || val === undefined || val === '') {
540
- this.setProperty('value', null);
541
- }
542
- else {
543
- const strValue = val instanceof Date ? val.toISOString() : val;
544
- this.setProperty('value', strValue);
545
- }
546
- }
547
- clear() {
548
- this.clearValue();
549
- }
550
- buildStubClasses() {
551
- const classes = ['date-picker-stub'];
552
- const size = this.getProperty('size') || 'md';
553
- classes.push(size);
554
- if (this.getProperty('disabled'))
555
- classes.push('disabled');
556
- if (this.getProperty('required'))
557
- classes.push('required');
558
- if (this._state.open)
559
- classes.push('open');
560
- return classes.join(' ');
561
- }
562
- renderStub() {
563
- const stub = document.createElement('div');
564
- stub.className = this.buildStubClasses();
565
- const isDisabled = this.getProperty('disabled');
566
- if (isDisabled) {
567
- stub.setAttribute('disabled', 'true');
568
- }
569
- stub.setAttribute('role', 'button');
570
- stub.setAttribute('aria-haspopup', 'dialog');
571
- stub.setAttribute('aria-expanded', String(this._state.open));
572
- stub.setAttribute('tabindex', isDisabled ? '-1' : '0');
573
- if (isDisabled)
574
- stub.setAttribute('aria-disabled', 'true');
575
- if (this.getProperty('label')) {
576
- stub.setAttribute('aria-labelledby', `date-picker-label-${getElementHash(this)}`);
577
- }
578
- stub.addEventListener('keydown', (e) => {
579
- const ke = e;
580
- if (this._state.open)
581
- return;
582
- if (ke.key === 'Enter' || ke.key === ' ') {
583
- ke.preventDefault();
584
- this.handleStubClick(e);
585
- }
586
- });
587
- const startSlot = document.createElement('slot');
588
- startSlot.name = 'start';
589
- const displayText = document.createElement('span');
590
- displayText.className = 'stub-text';
591
- const formattedValue = this.getFormattedValue();
592
- const placeholder = this.getProperty('placeholder') || 'Select date...';
593
- displayText.textContent = formattedValue || placeholder;
594
- if (!formattedValue) {
595
- displayText.classList.add('placeholder');
596
- }
597
- const iconContainer = document.createElement('div');
598
- iconContainer.className = 'stub-icons';
599
- const clearable = this.getProperty('clearable');
600
- if (clearable && formattedValue && !isDisabled) {
601
- const clearButton = document.createElement('button');
602
- clearButton.className = 'stub-clear';
603
- clearButton.type = 'button';
604
- clearButton.innerHTML = CLEAR_ICON_SVG;
605
- clearButton.addEventListener('click', (e) => this.handleClearClick(e));
606
- iconContainer.appendChild(clearButton);
607
- }
608
- const calendarIcon = document.createElement('span');
609
- calendarIcon.className = 'stub-arrow';
610
- calendarIcon.innerHTML = CALENDAR_ICON_SVG;
611
- iconContainer.appendChild(calendarIcon);
612
- stub.addEventListener('click', (e) => this.handleStubClick(e));
613
- stub.appendChild(startSlot);
614
- stub.appendChild(displayText);
615
- stub.appendChild(iconContainer);
616
- return stub;
617
- }
618
- createTimeInputElement() {
619
- const input = document.createElement('input');
620
- input.type = 'text';
621
- input.className = 'time-input';
622
- input.placeholder = 'HH:mm';
623
- input.autocomplete = 'off';
624
- input.maxLength = 5;
625
- return input;
626
- }
627
- renderTimeSection() {
628
- const timeSection = document.createElement('div');
629
- timeSection.className = 'time-section';
630
- const timeLabel = document.createElement('label');
631
- timeLabel.className = 'time-label';
632
- timeLabel.textContent = 'Time:';
633
- const timeInputElement = this.createTimeInputElement();
634
- const hour = this._state.hour || 0;
635
- const minute = this._state.minute || 0;
636
- this._timeInput = new TimeInput(timeInputElement, this, hour, minute);
637
- const timeIcon = document.createElement('span');
638
- timeIcon.className = 'time-icon';
639
- timeIcon.innerHTML = SCHEDULE_ICON_SVG;
640
- timeSection.appendChild(timeLabel);
641
- timeSection.appendChild(timeInputElement);
642
- timeSection.appendChild(timeIcon);
643
- return timeSection;
644
- }
645
- renderCalendarDropdown() {
646
- const dialog = document.createElement('dialog');
647
- dialog.className = 'calendar-dialog';
648
- const contentWrapper = document.createElement('div');
649
- contentWrapper.className = 'calendar-content';
650
- const calendar = document.createElement('ty-calendar');
651
- if (this._state.year && this._state.month && this._state.day) {
652
- calendar.setAttribute('year', this._state.year.toString());
653
- calendar.setAttribute('month', this._state.month.toString());
654
- calendar.setAttribute('day', this._state.day.toString());
655
- }
656
- const locale = getEffectiveLocale(this, this.getProperty('locale'));
657
- if (locale) {
658
- calendar.setAttribute('locale', locale);
659
- }
660
- const min = this.getProperty('min');
661
- const max = this.getProperty('max');
662
- if (min)
663
- calendar.setAttribute('min', min);
664
- if (max)
665
- calendar.setAttribute('max', max);
666
- const flavor = this.getProperty('flavor');
667
- if (flavor && flavor !== 'default')
668
- calendar.setAttribute('flavor', flavor);
669
- calendar.addEventListener('change', (e) => this.handleCalendarChange(e));
670
- contentWrapper.appendChild(calendar);
671
- if (this._state.withTime) {
672
- contentWrapper.appendChild(this.renderTimeSection());
673
- }
674
- dialog.appendChild(contentWrapper);
675
- dialog.addEventListener('close', () => {
676
- this.updateState({ open: false });
677
- });
678
- return dialog;
679
- }
680
- renderNativeInput() {
681
- const wrapper = document.createElement('div');
682
- wrapper.className = 'dropdown-wrapper';
683
- const stub = document.createElement('div');
684
- stub.className = this.buildStubClasses();
685
- const isDisabled = this.getProperty('disabled');
686
- if (isDisabled) {
687
- stub.setAttribute('disabled', 'true');
688
- }
689
- const input = document.createElement('input');
690
- input.className = 'native-date-input';
691
- input.type = this._state.withTime ? 'datetime-local' : 'date';
692
- if (isDisabled)
693
- input.disabled = true;
694
- if (this.getProperty('required'))
695
- input.required = true;
696
- const minBound = this.getProperty('min');
697
- const maxBound = this.getProperty('max');
698
- if (minBound)
699
- input.min = this._state.withTime ? `${minBound}T00:00` : minBound;
700
- if (maxBound)
701
- input.max = this._state.withTime ? `${maxBound}T23:59` : maxBound;
702
- const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
703
- if (localValue) {
704
- input.value = localValue;
705
- }
706
- const placeholder = this.getProperty('placeholder') || 'Select date...';
707
- const formattedValue = this.getFormattedValue();
708
- const placeholderEl = document.createElement('span');
709
- placeholderEl.className = 'stub-text' + (formattedValue ? '' : ' placeholder');
710
- placeholderEl.textContent = formattedValue || placeholder;
711
- input.addEventListener('change', () => {
712
- if (!input.value) {
713
- this.clearValue();
714
- return;
715
- }
716
- const parsed = parseValue(input.value, this._state.withTime);
717
- if (parsed) {
718
- this.updateState(parsed, true);
719
- this.emitChangeEvent(parsed, 'selection');
720
- }
721
- });
722
- const startSlot = document.createElement('slot');
723
- startSlot.name = 'start';
724
- stub.appendChild(startSlot);
725
- stub.appendChild(input);
726
- stub.appendChild(placeholderEl);
727
- const iconContainer = document.createElement('div');
728
- iconContainer.className = 'stub-icons';
729
- const clearable = this.getProperty('clearable');
730
- if (clearable && localValue && !isDisabled) {
731
- const clearButton = document.createElement('button');
732
- clearButton.className = 'stub-clear';
733
- clearButton.type = 'button';
734
- clearButton.innerHTML = CLEAR_ICON_SVG;
735
- clearButton.addEventListener('click', (e) => this.handleClearClick(e));
736
- iconContainer.appendChild(clearButton);
737
- }
738
- const calendarIcon = document.createElement('span');
739
- calendarIcon.className = 'stub-arrow';
740
- calendarIcon.innerHTML = CALENDAR_ICON_SVG;
741
- iconContainer.appendChild(calendarIcon);
742
- stub.appendChild(iconContainer);
743
- wrapper.appendChild(stub);
744
- return wrapper;
745
- }
746
- calculateCalendarPosition() {
747
- if (!this.shadowRoot)
748
- return;
749
- if (isMobileTouch())
750
- return;
751
- const stub = this.shadowRoot.querySelector('.date-picker-stub');
752
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
753
- if (!stub || !dialog)
754
- return;
755
- const stubRect = stub.getBoundingClientRect();
756
- const dialogRect = dialog.getBoundingClientRect();
757
- const estimatedHeight = dialogRect.height || 400;
758
- const popupWidth = dialogRect.width || 320;
759
- const wrapPadding = 8;
760
- const anchored = placementToAnchored(this.getProperty('placement'));
761
- const pos = computeAnchoredPosition({
762
- anchorRect: stubRect,
763
- popupWidth,
764
- popupHeight: estimatedHeight,
765
- gap: 0,
766
- align: anchored.align,
767
- side: anchored.side,
768
- });
769
- const positionBelow = pos.below;
770
- const x = pos.x - wrapPadding;
771
- const y = positionBelow ? pos.topY : pos.bottomY;
772
- this.style.setProperty('--calendar-x', `${x}px`);
773
- this.style.setProperty('--calendar-y', `${y}px`);
774
- this.style.setProperty('--calendar-offset-x', '0px');
775
- this.style.setProperty('--calendar-offset-y', '0px');
776
- if (positionBelow) {
777
- dialog.classList.add('position-below');
778
- dialog.classList.remove('position-above');
779
- }
780
- else {
781
- dialog.classList.add('position-above');
782
- dialog.classList.remove('position-below');
783
- }
784
- }
785
- renderContainer() {
786
- const container = document.createElement('div');
787
- container.className = 'dropdown-container';
788
- const label = this.getProperty('label');
789
- if (label) {
790
- const labelEl = document.createElement('label');
791
- labelEl.className = 'ty-field-label';
792
- labelEl.id = `date-picker-label-${getElementHash(this)}`;
793
- labelEl.innerHTML = label + (this.getProperty('required') ? '<span class="required-icon">*</span>' : '');
794
- container.appendChild(labelEl);
795
- }
796
- return container;
797
- }
798
- render() {
799
- if (!this.shadowRoot)
800
- return;
801
- ensureStyles(this.shadowRoot, { css: datePickerStyles, id: 'ty-date-picker' });
802
- if (isMobileTouch()) {
803
- const existingNativeInput = this.shadowRoot.querySelector('.native-date-input');
804
- if (existingNativeInput) {
805
- this.updateDisplay();
806
- return;
807
- }
808
- this.shadowRoot.innerHTML = '';
809
- const container = this.renderContainer();
810
- container.appendChild(this.renderNativeInput());
811
- this.shadowRoot.appendChild(container);
812
- return;
813
- }
814
- const existingDialog = this.shadowRoot.querySelector('.calendar-dialog');
815
- const isDialogOpen = existingDialog && existingDialog.open;
816
- if (isDialogOpen && this._state.open) {
817
- this.updateDisplay();
818
- this.calculateCalendarPosition();
819
- return;
820
- }
821
- this.shadowRoot.innerHTML = '';
822
- const container = this.renderContainer();
823
- const wrapper = document.createElement('div');
824
- wrapper.className = 'dropdown-wrapper';
825
- wrapper.appendChild(this.renderStub());
826
- wrapper.appendChild(this.renderCalendarDropdown());
827
- container.appendChild(wrapper);
828
- this.shadowRoot.appendChild(container);
829
- this.setupEventListeners();
830
- }
831
- updateDisplay() {
832
- if (!this.shadowRoot)
833
- return;
834
- const formattedValue = this.getFormattedValue();
835
- const placeholder = this.getProperty('placeholder') || 'Select date...';
836
- const hasValue = this.hasDate();
837
- const clearable = this.getProperty('clearable');
838
- const isDisabled = this.getProperty('disabled');
839
- const stubText = this.shadowRoot.querySelector('.stub-text');
840
- if (stubText) {
841
- stubText.textContent = formattedValue || placeholder;
842
- stubText.classList.toggle('placeholder', !formattedValue);
843
- }
844
- const clearButton = this.shadowRoot.querySelector('.stub-clear');
845
- if (clearButton) {
846
- clearButton.style.display = (clearable && hasValue && !isDisabled) ? '' : 'none';
847
- }
848
- if (isMobileTouch()) {
849
- const nativeInput = this.shadowRoot.querySelector('.native-date-input');
850
- if (nativeInput) {
851
- const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
852
- nativeInput.value = localValue || '';
853
- }
854
- return;
855
- }
856
- const calendar = this.shadowRoot.querySelector('ty-calendar');
857
- if (calendar && hasValue) {
858
- calendar.setAttribute('year', this._state.year.toString());
859
- calendar.setAttribute('month', this._state.month.toString());
860
- calendar.setAttribute('day', this._state.day.toString());
861
- }
862
- if (calendar) {
863
- const flavor = this.getProperty('flavor');
864
- if (flavor && flavor !== 'default')
865
- calendar.setAttribute('flavor', flavor);
866
- else
867
- calendar.removeAttribute('flavor');
868
- }
869
- if (this._timeInput && this._state.hour !== undefined && this._state.minute !== undefined) {
870
- this._timeInput.setTime(this._state.hour, this._state.minute);
871
- }
872
- }
873
- setupEventListeners() {
874
- if (this._clickListener) {
875
- document.removeEventListener('click', this._clickListener);
876
- }
877
- if (this._keydownListener) {
878
- document.removeEventListener('keydown', this._keydownListener);
879
- }
880
- this._clickListener = (e) => this.handleOutsideClick(e);
881
- this._keydownListener = (e) => this.handleEscapeKey(e);
882
- document.addEventListener('click', this._clickListener);
883
- document.addEventListener('keydown', this._keydownListener);
884
- const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
885
- if (dialog) {
886
- this._dialogClickListener = (e) => this.handleDialogClick(e);
887
- dialog.addEventListener('click', this._dialogClickListener);
888
- }
889
- }
890
- handleStubClick(event) {
891
- event.preventDefault();
892
- const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
893
- if (this._state.open && (!dialog || !dialog.open)) {
894
- this.updateState({ open: false }, true);
895
- unlockScroll(`date-picker-${this.id || this.toString()}`);
896
- }
897
- const disabled = this.getProperty('disabled');
898
- if (!disabled && Date.now() - this._closeTimestamp > 300) {
899
- this.openDropdown();
900
- }
901
- }
902
- clearValue() {
903
- this.updateState({
904
- year: undefined,
905
- month: undefined,
906
- day: undefined,
907
- hour: undefined,
908
- minute: undefined,
909
- }, true);
910
- this.value = null;
911
- this.emitChangeEvent(null, 'clear');
912
- this.render();
913
- }
914
- handleClearClick(event) {
915
- event.preventDefault();
916
- event.stopPropagation();
917
- this.clearValue();
918
- }
919
- handleCalendarChange(event) {
920
- event.preventDefault();
921
- event.stopPropagation();
922
- const customEvent = event;
923
- const detail = customEvent.detail;
924
- const dayContext = detail.dayContext;
925
- if (!dayContext)
926
- return;
927
- const newComponents = {
928
- year: dayContext.year,
929
- month: dayContext.month,
930
- day: dayContext.dayInMonth,
931
- hour: this._state.hour,
932
- minute: this._state.minute,
933
- };
934
- this.updateState(newComponents, true);
935
- this.emitChangeEvent(newComponents, 'selection');
936
- if (!this._state.withTime) {
937
- this.closeDropdown();
938
- }
939
- else {
940
- requestAnimationFrame(() => {
941
- if (!this.shadowRoot)
942
- return;
943
- const timeInput = this.shadowRoot.querySelector('.time-input');
944
- if (timeInput) {
945
- timeInput.focus();
946
- }
947
- });
948
- }
949
- }
950
- handleDialogClick(event) {
951
- if (!this.shadowRoot)
952
- return;
953
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
954
- const content = this.shadowRoot.querySelector('.calendar-content');
955
- if (event.target === dialog && this._state.open && content && !content.contains(event.target)) {
956
- event.preventDefault();
957
- event.stopPropagation();
958
- this.closeDropdown();
959
- }
960
- }
961
- handleOutsideClick(event) {
962
- if (!this.shadowRoot)
963
- return;
964
- const target = event.target;
965
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
966
- if (this._state.open && dialog && !this.contains(target) && !dialog.contains(target)) {
967
- this.closeDropdown();
968
- }
969
- }
970
- handleEscapeKey(event) {
971
- const keyboardEvent = event;
972
- if (keyboardEvent.key === 'Escape' && this._state.open) {
973
- keyboardEvent.preventDefault();
974
- this.closeDropdown();
975
- }
976
- }
977
- openDropdown() {
978
- if (this._state.open)
979
- return;
980
- if (isMobileTouch())
981
- return;
982
- this.updateState({ open: true });
983
- this.dispatchEvent(new CustomEvent('open'));
984
- this.render();
985
- requestAnimationFrame(() => {
986
- if (!this.shadowRoot)
987
- return;
988
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
989
- if (dialog) {
990
- const pickerId = `date-picker-${this.id || this.toString()}`;
991
- lockScroll(pickerId);
992
- dialog.showModal();
993
- this.calculateCalendarPosition();
994
- dialog.classList.add('open');
995
- const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
996
- if (stubEl)
997
- stubEl.setAttribute('aria-expanded', 'true');
998
- const focusedElement = this.shadowRoot.activeElement;
999
- if (focusedElement) {
1000
- focusedElement.blur();
1001
- }
1002
- }
1003
- });
1004
- }
1005
- closeDropdown() {
1006
- if (!this._state.open)
1007
- return;
1008
- this._closeTimestamp = Date.now();
1009
- const pickerId = `date-picker-${this.id || this.toString()}`;
1010
- this.updateState({ open: false }, true);
1011
- unlockScroll(pickerId);
1012
- if (this.shadowRoot) {
1013
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1014
- if (dialog) {
1015
- dialog.classList.remove('position-above', 'position-below', 'open');
1016
- dialog.close();
1017
- }
1018
- const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
1019
- if (stubEl)
1020
- stubEl.setAttribute('aria-expanded', 'false');
1021
- }
1022
- this.dispatchEvent(new CustomEvent('close'));
1023
- this.render();
1024
- }
1025
- cleanup() {
1026
- if (this._clickListener) {
1027
- document.removeEventListener('click', this._clickListener);
1028
- }
1029
- if (this._keydownListener) {
1030
- document.removeEventListener('keydown', this._keydownListener);
1031
- }
1032
- if (this._dialogClickListener && this.shadowRoot) {
1033
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1034
- if (dialog) {
1035
- dialog.removeEventListener('click', this._dialogClickListener);
1036
- }
1037
- }
1038
- if (this._state.open) {
1039
- const pickerId = `date-picker-${this.id || this.toString()}`;
1040
- unlockScroll(pickerId);
1041
- }
1042
- this._clickListener = undefined;
1043
- this._keydownListener = undefined;
1044
- this._dialogClickListener = undefined;
1045
- this._timeInput = undefined;
1046
- }
327
+ constructor() {
328
+ super();
329
+ this._state = {
330
+ withTime: false,
331
+ open: false,
332
+ };
333
+ this._closeTimestamp = 0;
334
+ this._customFlavorSheet = null;
335
+ const shadow = this.shadowRoot;
336
+ ensureStyles(shadow, { css: datePickerStyles, id: 'ty-date-picker' });
337
+ }
338
+ onConnect() {
339
+ this.initializeState();
340
+ this.render();
341
+ this._localeObserver = observeLocaleChanges(this, () => {
342
+ this.render();
343
+ });
344
+ this._syncCustomFlavor();
345
+ }
346
+ _syncCustomFlavor() {
347
+ const flavor = this.flavor === 'default' ? 'neutral' : this.flavor;
348
+ this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, flavor, ({ base }) => datePickerCustomFlavorCss(base));
349
+ }
350
+ onDisconnect() {
351
+ if (this._localeObserver) {
352
+ this._localeObserver();
353
+ this._localeObserver = undefined;
354
+ }
355
+ this.cleanup();
356
+ }
357
+ onPropertiesChanged(changes) {
358
+ for (const { name, newValue } of changes) {
359
+ switch (name) {
360
+ case 'flavor':
361
+ this._syncCustomFlavor();
362
+ break;
363
+ case 'size':
364
+ case 'label':
365
+ case 'placeholder':
366
+ case 'required':
367
+ case 'disabled':
368
+ case 'clearable':
369
+ case 'format':
370
+ case 'locale':
371
+ break;
372
+ case 'value': {
373
+ const newComponents = parseValue(newValue, this._state.withTime);
374
+ if (newComponents === null) {
375
+ const hasDate = this._state.year !== undefined ||
376
+ this._state.month !== undefined ||
377
+ this._state.day !== undefined;
378
+ if (hasDate) {
379
+ this._state = {
380
+ withTime: this._state.withTime,
381
+ open: this._state.open,
382
+ };
383
+ }
384
+ break;
385
+ }
386
+ const currentComponents = {
387
+ year: this._state.year,
388
+ month: this._state.month,
389
+ day: this._state.day,
390
+ hour: this._state.hour,
391
+ minute: this._state.minute,
392
+ };
393
+ const changed = newComponents?.year !== currentComponents.year ||
394
+ newComponents?.month !== currentComponents.month ||
395
+ newComponents?.day !== currentComponents.day ||
396
+ newComponents?.hour !== currentComponents.hour ||
397
+ newComponents?.minute !== currentComponents.minute;
398
+ if (changed) {
399
+ this._state = {
400
+ ...this._state,
401
+ ...newComponents,
402
+ };
403
+ }
404
+ break;
405
+ }
406
+ case 'with-time': {
407
+ const newWithTime = newValue;
408
+ const oldWithTime = this._state.withTime;
409
+ if (newWithTime !== oldWithTime) {
410
+ this._state.withTime = newWithTime;
411
+ const hasDate = this._state.year !== undefined &&
412
+ this._state.month !== undefined &&
413
+ this._state.day !== undefined;
414
+ if (hasDate) {
415
+ this.syncFormValue();
416
+ }
417
+ }
418
+ break;
419
+ }
420
+ }
421
+ }
422
+ }
423
+ getFormValue() {
424
+ return componentsToOutputValue(this.getComponents());
425
+ }
426
+ getComponents() {
427
+ return {
428
+ year: this._state.year,
429
+ month: this._state.month,
430
+ day: this._state.day,
431
+ hour: this._state.hour,
432
+ minute: this._state.minute,
433
+ };
434
+ }
435
+ hasDate() {
436
+ return !!(this._state.year && this._state.month && this._state.day);
437
+ }
438
+ getFormattedValue(components) {
439
+ const c = components || this.getComponents();
440
+ if (!c.year || !c.month || !c.day)
441
+ return null;
442
+ return formatDisplayValue(c, this.getProperty('format') || 'long', getEffectiveLocale(this, this.getProperty('locale')), this._state.withTime);
443
+ }
444
+ initializeState() {
445
+ const valueAttr = this.getProperty('value');
446
+ const withTime = this.getProperty('with-time');
447
+ const components = parseValue(valueAttr, withTime);
448
+ if (components === null) {
449
+ this._state = {
450
+ withTime,
451
+ open: false,
452
+ };
453
+ }
454
+ else {
455
+ this._state = {
456
+ ...components,
457
+ withTime,
458
+ open: false,
459
+ };
460
+ }
461
+ this.syncFormValue();
462
+ }
463
+ updateState(updates, forceSync = false) {
464
+ this._state = { ...this._state, ...updates };
465
+ const shouldSync = forceSync || !this._state.open || !this._state.withTime;
466
+ if (shouldSync) {
467
+ this.syncFormValue();
468
+ }
469
+ }
470
+ syncFormValue() {
471
+ const utcValue = this.getFormValue();
472
+ const currentValue = this.getProperty('value');
473
+ if (utcValue !== currentValue) {
474
+ if (utcValue) {
475
+ this.setProperty('value', utcValue);
476
+ }
477
+ else {
478
+ this.setProperty('value', null);
479
+ }
480
+ }
481
+ }
482
+ handleTimeInputChange(hour, minute) {
483
+ if (!this.hasDate())
484
+ return;
485
+ const components = { ...this.getComponents(), hour, minute };
486
+ this.updateState(components, true);
487
+ this.emitChangeEvent(components, 'time-change');
488
+ }
489
+ emitChangeEvent(components, source) {
490
+ const utcValue = components ? componentsToOutputValue(components) : null;
491
+ const localValue = components ? componentsToLocalValue(components, this._state.withTime) : null;
492
+ const milliseconds = components ? componentsToDateObject(components)?.getTime() ?? null : null;
493
+ const formatted = components ? this.getFormattedValue(components) : null;
494
+ const detail = {
495
+ value: utcValue,
496
+ localValue,
497
+ milliseconds,
498
+ formatted,
499
+ source,
500
+ };
501
+ const event = new CustomEvent('change', {
502
+ detail,
503
+ bubbles: true,
504
+ cancelable: true,
505
+ });
506
+ this.dispatchEvent(event);
507
+ }
508
+ get size() { return this.getProperty('size'); }
509
+ set size(v) { this.setProperty('size', v); }
510
+ get placement() { return this.getProperty('placement'); }
511
+ set placement(v) { this.setProperty('placement', v); }
512
+ get flavor() { return this.getProperty('flavor'); }
513
+ set flavor(v) { this.setProperty('flavor', v); }
514
+ get label() { return this.getProperty('label'); }
515
+ set label(v) { this.setProperty('label', v); }
516
+ get placeholder() { return this.getProperty('placeholder'); }
517
+ set placeholder(v) { this.setProperty('placeholder', v); }
518
+ get name() { return this.getProperty('name'); }
519
+ set name(v) { this.setProperty('name', v); }
520
+ get format() { return this.getProperty('format'); }
521
+ set format(v) { this.setProperty('format', v); }
522
+ get locale() { return this.getProperty('locale'); }
523
+ set locale(v) { this.setProperty('locale', v); }
524
+ get required() { return this.getProperty('required'); }
525
+ set required(v) { this.setProperty('required', v); }
526
+ get disabled() { return this.getProperty('disabled'); }
527
+ set disabled(v) { this.setProperty('disabled', v); }
528
+ get clearable() { return this.getProperty('clearable'); }
529
+ set clearable(v) { this.setProperty('clearable', v); }
530
+ get min() { return this.getProperty('min'); }
531
+ set min(v) { this.setProperty('min', v ?? ''); }
532
+ get max() { return this.getProperty('max'); }
533
+ set max(v) { this.setProperty('max', v ?? ''); }
534
+ get withTime() { return this.getProperty('with-time'); }
535
+ set withTime(v) { this.setProperty('with-time', v); }
536
+ get value() {
537
+ return this.getProperty('value') || null;
538
+ }
539
+ set value(val) {
540
+ if (val === null || val === undefined || val === '') {
541
+ this.setProperty('value', null);
542
+ }
543
+ else {
544
+ const strValue = val instanceof Date ? val.toISOString() : val;
545
+ this.setProperty('value', strValue);
546
+ }
547
+ }
548
+ clear() {
549
+ this.clearValue();
550
+ }
551
+ buildStubClasses() {
552
+ const classes = ['date-picker-stub'];
553
+ if (this.getProperty('disabled'))
554
+ classes.push('disabled');
555
+ if (this.getProperty('required'))
556
+ classes.push('required');
557
+ if (this._state.open)
558
+ classes.push('open');
559
+ return classes.join(' ');
560
+ }
561
+ renderStub() {
562
+ const stub = document.createElement('div');
563
+ stub.className = this.buildStubClasses();
564
+ const isDisabled = this.getProperty('disabled');
565
+ if (isDisabled) {
566
+ stub.setAttribute('disabled', 'true');
567
+ }
568
+ stub.setAttribute('role', 'button');
569
+ stub.setAttribute('aria-haspopup', 'dialog');
570
+ stub.setAttribute('aria-expanded', String(this._state.open));
571
+ stub.setAttribute('tabindex', isDisabled ? '-1' : '0');
572
+ if (isDisabled)
573
+ stub.setAttribute('aria-disabled', 'true');
574
+ if (this.getProperty('label')) {
575
+ stub.setAttribute('aria-labelledby', `date-picker-label-${getElementHash(this)}`);
576
+ }
577
+ stub.addEventListener('keydown', (e) => {
578
+ const ke = e;
579
+ if (this._state.open)
580
+ return;
581
+ if (ke.key === 'Enter' || ke.key === ' ') {
582
+ ke.preventDefault();
583
+ this.handleStubClick(e);
584
+ }
585
+ });
586
+ const startSlot = document.createElement('slot');
587
+ startSlot.name = 'start';
588
+ const displayText = document.createElement('span');
589
+ displayText.className = 'stub-text';
590
+ const formattedValue = this.getFormattedValue();
591
+ const placeholder = this.getProperty('placeholder') || 'Select date...';
592
+ displayText.textContent = formattedValue || placeholder;
593
+ if (!formattedValue) {
594
+ displayText.classList.add('placeholder');
595
+ }
596
+ const iconContainer = document.createElement('div');
597
+ iconContainer.className = 'stub-icons';
598
+ const clearable = this.getProperty('clearable');
599
+ if (clearable && formattedValue && !isDisabled) {
600
+ const clearButton = document.createElement('button');
601
+ clearButton.className = 'stub-clear';
602
+ clearButton.type = 'button';
603
+ clearButton.innerHTML = CLEAR_ICON_SVG;
604
+ clearButton.addEventListener('click', (e) => this.handleClearClick(e));
605
+ iconContainer.appendChild(clearButton);
606
+ }
607
+ const calendarIcon = document.createElement('span');
608
+ calendarIcon.className = 'stub-arrow';
609
+ calendarIcon.innerHTML = CALENDAR_ICON_SVG;
610
+ iconContainer.appendChild(calendarIcon);
611
+ stub.addEventListener('click', (e) => this.handleStubClick(e));
612
+ stub.appendChild(startSlot);
613
+ stub.appendChild(displayText);
614
+ stub.appendChild(iconContainer);
615
+ return stub;
616
+ }
617
+ createTimeInputElement() {
618
+ const input = document.createElement('input');
619
+ input.type = 'text';
620
+ input.className = 'time-input';
621
+ input.placeholder = 'HH:mm';
622
+ input.autocomplete = 'off';
623
+ input.maxLength = 5;
624
+ return input;
625
+ }
626
+ renderTimeSection() {
627
+ const timeSection = document.createElement('div');
628
+ timeSection.className = 'time-section';
629
+ const timeLabel = document.createElement('label');
630
+ timeLabel.className = 'time-label';
631
+ timeLabel.textContent = 'Time:';
632
+ const timeInputElement = this.createTimeInputElement();
633
+ const hour = this._state.hour || 0;
634
+ const minute = this._state.minute || 0;
635
+ this._timeInput = new TimeInput(timeInputElement, this, hour, minute);
636
+ const timeIcon = document.createElement('span');
637
+ timeIcon.className = 'time-icon';
638
+ timeIcon.innerHTML = SCHEDULE_ICON_SVG;
639
+ timeSection.appendChild(timeLabel);
640
+ timeSection.appendChild(timeInputElement);
641
+ timeSection.appendChild(timeIcon);
642
+ return timeSection;
643
+ }
644
+ renderCalendarDropdown() {
645
+ const dialog = document.createElement('dialog');
646
+ dialog.className = 'calendar-dialog';
647
+ const contentWrapper = document.createElement('div');
648
+ contentWrapper.className = 'calendar-content';
649
+ const calendar = document.createElement('ty-calendar');
650
+ if (this._state.year && this._state.month && this._state.day) {
651
+ calendar.setAttribute('year', this._state.year.toString());
652
+ calendar.setAttribute('month', this._state.month.toString());
653
+ calendar.setAttribute('day', this._state.day.toString());
654
+ }
655
+ const locale = getEffectiveLocale(this, this.getProperty('locale'));
656
+ if (locale) {
657
+ calendar.setAttribute('locale', locale);
658
+ }
659
+ const min = this.getProperty('min');
660
+ const max = this.getProperty('max');
661
+ if (min)
662
+ calendar.setAttribute('min', min);
663
+ if (max)
664
+ calendar.setAttribute('max', max);
665
+ const flavor = this.getProperty('flavor');
666
+ if (flavor && flavor !== 'default')
667
+ calendar.setAttribute('flavor', flavor);
668
+ calendar.addEventListener('change', (e) => this.handleCalendarChange(e));
669
+ contentWrapper.appendChild(calendar);
670
+ if (this._state.withTime) {
671
+ contentWrapper.appendChild(this.renderTimeSection());
672
+ }
673
+ dialog.appendChild(contentWrapper);
674
+ dialog.addEventListener('close', () => {
675
+ this.updateState({ open: false });
676
+ });
677
+ return dialog;
678
+ }
679
+ renderNativeInput() {
680
+ const wrapper = document.createElement('div');
681
+ wrapper.className = 'dropdown-wrapper';
682
+ const stub = document.createElement('div');
683
+ stub.className = this.buildStubClasses();
684
+ const isDisabled = this.getProperty('disabled');
685
+ if (isDisabled) {
686
+ stub.setAttribute('disabled', 'true');
687
+ }
688
+ const input = document.createElement('input');
689
+ input.className = 'native-date-input';
690
+ input.type = this._state.withTime ? 'datetime-local' : 'date';
691
+ if (isDisabled)
692
+ input.disabled = true;
693
+ if (this.getProperty('required'))
694
+ input.required = true;
695
+ const minBound = this.getProperty('min');
696
+ const maxBound = this.getProperty('max');
697
+ if (minBound)
698
+ input.min = this._state.withTime ? `${minBound}T00:00` : minBound;
699
+ if (maxBound)
700
+ input.max = this._state.withTime ? `${maxBound}T23:59` : maxBound;
701
+ const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
702
+ if (localValue) {
703
+ input.value = localValue;
704
+ }
705
+ const placeholder = this.getProperty('placeholder') || 'Select date...';
706
+ const formattedValue = this.getFormattedValue();
707
+ const placeholderEl = document.createElement('span');
708
+ placeholderEl.className = 'stub-text' + (formattedValue ? '' : ' placeholder');
709
+ placeholderEl.textContent = formattedValue || placeholder;
710
+ input.addEventListener('change', () => {
711
+ if (!input.value) {
712
+ this.clearValue();
713
+ return;
714
+ }
715
+ const parsed = parseValue(input.value, this._state.withTime);
716
+ if (parsed) {
717
+ this.updateState(parsed, true);
718
+ this.emitChangeEvent(parsed, 'selection');
719
+ }
720
+ });
721
+ const startSlot = document.createElement('slot');
722
+ startSlot.name = 'start';
723
+ stub.appendChild(startSlot);
724
+ stub.appendChild(input);
725
+ stub.appendChild(placeholderEl);
726
+ const iconContainer = document.createElement('div');
727
+ iconContainer.className = 'stub-icons';
728
+ const clearable = this.getProperty('clearable');
729
+ if (clearable && localValue && !isDisabled) {
730
+ const clearButton = document.createElement('button');
731
+ clearButton.className = 'stub-clear';
732
+ clearButton.type = 'button';
733
+ clearButton.innerHTML = CLEAR_ICON_SVG;
734
+ clearButton.addEventListener('click', (e) => this.handleClearClick(e));
735
+ iconContainer.appendChild(clearButton);
736
+ }
737
+ const calendarIcon = document.createElement('span');
738
+ calendarIcon.className = 'stub-arrow';
739
+ calendarIcon.innerHTML = CALENDAR_ICON_SVG;
740
+ iconContainer.appendChild(calendarIcon);
741
+ stub.appendChild(iconContainer);
742
+ wrapper.appendChild(stub);
743
+ return wrapper;
744
+ }
745
+ calculateCalendarPosition() {
746
+ if (!this.shadowRoot)
747
+ return;
748
+ if (isMobileTouch())
749
+ return;
750
+ const stub = this.shadowRoot.querySelector('.date-picker-stub');
751
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
752
+ if (!stub || !dialog)
753
+ return;
754
+ const stubRect = stub.getBoundingClientRect();
755
+ const dialogRect = dialog.getBoundingClientRect();
756
+ const estimatedHeight = dialogRect.height || 400;
757
+ const popupWidth = dialogRect.width || 320;
758
+ const wrapPadding = 8;
759
+ const anchored = placementToAnchored(this.getProperty('placement'));
760
+ const pos = computeAnchoredPosition({
761
+ anchorRect: stubRect,
762
+ popupWidth,
763
+ popupHeight: estimatedHeight,
764
+ gap: 0,
765
+ align: anchored.align,
766
+ side: anchored.side,
767
+ });
768
+ const positionBelow = pos.below;
769
+ const x = pos.x - wrapPadding;
770
+ const y = positionBelow ? pos.topY : pos.bottomY;
771
+ this.style.setProperty('--calendar-x', `${x}px`);
772
+ this.style.setProperty('--calendar-y', `${y}px`);
773
+ this.style.setProperty('--calendar-offset-x', '0px');
774
+ this.style.setProperty('--calendar-offset-y', '0px');
775
+ if (positionBelow) {
776
+ dialog.classList.add('position-below');
777
+ dialog.classList.remove('position-above');
778
+ }
779
+ else {
780
+ dialog.classList.add('position-above');
781
+ dialog.classList.remove('position-below');
782
+ }
783
+ }
784
+ renderContainer() {
785
+ const container = document.createElement('div');
786
+ container.className = 'dropdown-container';
787
+ const label = this.getProperty('label');
788
+ if (label) {
789
+ const labelEl = document.createElement('label');
790
+ labelEl.className = 'ty-field-label';
791
+ labelEl.id = `date-picker-label-${getElementHash(this)}`;
792
+ labelEl.innerHTML = label + (this.getProperty('required') ? '<span class="required-icon">*</span>' : '');
793
+ container.appendChild(labelEl);
794
+ }
795
+ return container;
796
+ }
797
+ render() {
798
+ if (!this.shadowRoot)
799
+ return;
800
+ ensureStyles(this.shadowRoot, { css: datePickerStyles, id: 'ty-date-picker' });
801
+ if (isMobileTouch()) {
802
+ const existingNativeInput = this.shadowRoot.querySelector('.native-date-input');
803
+ if (existingNativeInput) {
804
+ this.updateDisplay();
805
+ return;
806
+ }
807
+ this.shadowRoot.innerHTML = '';
808
+ const container = this.renderContainer();
809
+ container.appendChild(this.renderNativeInput());
810
+ this.shadowRoot.appendChild(container);
811
+ return;
812
+ }
813
+ const existingDialog = this.shadowRoot.querySelector('.calendar-dialog');
814
+ const isDialogOpen = existingDialog && existingDialog.open;
815
+ if (isDialogOpen && this._state.open) {
816
+ this.updateDisplay();
817
+ this.calculateCalendarPosition();
818
+ return;
819
+ }
820
+ this.shadowRoot.innerHTML = '';
821
+ const container = this.renderContainer();
822
+ const wrapper = document.createElement('div');
823
+ wrapper.className = 'dropdown-wrapper';
824
+ wrapper.appendChild(this.renderStub());
825
+ wrapper.appendChild(this.renderCalendarDropdown());
826
+ container.appendChild(wrapper);
827
+ this.shadowRoot.appendChild(container);
828
+ this.setupEventListeners();
829
+ }
830
+ updateDisplay() {
831
+ if (!this.shadowRoot)
832
+ return;
833
+ const formattedValue = this.getFormattedValue();
834
+ const placeholder = this.getProperty('placeholder') || 'Select date...';
835
+ const hasValue = this.hasDate();
836
+ const clearable = this.getProperty('clearable');
837
+ const isDisabled = this.getProperty('disabled');
838
+ const stubText = this.shadowRoot.querySelector('.stub-text');
839
+ if (stubText) {
840
+ stubText.textContent = formattedValue || placeholder;
841
+ stubText.classList.toggle('placeholder', !formattedValue);
842
+ }
843
+ const clearButton = this.shadowRoot.querySelector('.stub-clear');
844
+ if (clearButton) {
845
+ clearButton.style.display = (clearable && hasValue && !isDisabled) ? '' : 'none';
846
+ }
847
+ if (isMobileTouch()) {
848
+ const nativeInput = this.shadowRoot.querySelector('.native-date-input');
849
+ if (nativeInput) {
850
+ const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
851
+ nativeInput.value = localValue || '';
852
+ }
853
+ return;
854
+ }
855
+ const calendar = this.shadowRoot.querySelector('ty-calendar');
856
+ if (calendar && hasValue) {
857
+ calendar.setAttribute('year', this._state.year.toString());
858
+ calendar.setAttribute('month', this._state.month.toString());
859
+ calendar.setAttribute('day', this._state.day.toString());
860
+ }
861
+ if (calendar) {
862
+ const flavor = this.getProperty('flavor');
863
+ if (flavor && flavor !== 'default')
864
+ calendar.setAttribute('flavor', flavor);
865
+ else
866
+ calendar.removeAttribute('flavor');
867
+ }
868
+ if (this._timeInput && this._state.hour !== undefined && this._state.minute !== undefined) {
869
+ this._timeInput.setTime(this._state.hour, this._state.minute);
870
+ }
871
+ }
872
+ setupEventListeners() {
873
+ if (this._clickListener) {
874
+ document.removeEventListener('click', this._clickListener);
875
+ }
876
+ if (this._keydownListener) {
877
+ document.removeEventListener('keydown', this._keydownListener);
878
+ }
879
+ this._clickListener = (e) => this.handleOutsideClick(e);
880
+ this._keydownListener = (e) => this.handleEscapeKey(e);
881
+ document.addEventListener('click', this._clickListener);
882
+ document.addEventListener('keydown', this._keydownListener);
883
+ const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
884
+ if (dialog) {
885
+ this._dialogClickListener = (e) => this.handleDialogClick(e);
886
+ dialog.addEventListener('click', this._dialogClickListener);
887
+ }
888
+ }
889
+ handleStubClick(event) {
890
+ event.preventDefault();
891
+ const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
892
+ if (this._state.open && (!dialog || !dialog.open)) {
893
+ this.updateState({ open: false }, true);
894
+ unlockScroll(`date-picker-${this.id || this.toString()}`);
895
+ }
896
+ const disabled = this.getProperty('disabled');
897
+ if (!disabled && Date.now() - this._closeTimestamp > 300) {
898
+ this.openDropdown();
899
+ }
900
+ }
901
+ clearValue() {
902
+ this.updateState({
903
+ year: undefined,
904
+ month: undefined,
905
+ day: undefined,
906
+ hour: undefined,
907
+ minute: undefined,
908
+ }, true);
909
+ this.value = null;
910
+ this.emitChangeEvent(null, 'clear');
911
+ this.render();
912
+ }
913
+ handleClearClick(event) {
914
+ event.preventDefault();
915
+ event.stopPropagation();
916
+ this.clearValue();
917
+ }
918
+ handleCalendarChange(event) {
919
+ event.preventDefault();
920
+ event.stopPropagation();
921
+ const customEvent = event;
922
+ const detail = customEvent.detail;
923
+ const dayContext = detail.dayContext;
924
+ if (!dayContext)
925
+ return;
926
+ const newComponents = {
927
+ year: dayContext.year,
928
+ month: dayContext.month,
929
+ day: dayContext.dayInMonth,
930
+ hour: this._state.hour,
931
+ minute: this._state.minute,
932
+ };
933
+ this.updateState(newComponents, true);
934
+ this.emitChangeEvent(newComponents, 'selection');
935
+ if (!this._state.withTime) {
936
+ this.closeDropdown();
937
+ }
938
+ else {
939
+ requestAnimationFrame(() => {
940
+ if (!this.shadowRoot)
941
+ return;
942
+ const timeInput = this.shadowRoot.querySelector('.time-input');
943
+ if (timeInput) {
944
+ timeInput.focus();
945
+ }
946
+ });
947
+ }
948
+ }
949
+ handleDialogClick(event) {
950
+ if (!this.shadowRoot)
951
+ return;
952
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
953
+ const content = this.shadowRoot.querySelector('.calendar-content');
954
+ if (event.target === dialog && this._state.open && content && !content.contains(event.target)) {
955
+ event.preventDefault();
956
+ event.stopPropagation();
957
+ this.closeDropdown();
958
+ }
959
+ }
960
+ handleOutsideClick(event) {
961
+ if (!this.shadowRoot)
962
+ return;
963
+ const target = event.target;
964
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
965
+ if (this._state.open && dialog && !this.contains(target) && !dialog.contains(target)) {
966
+ this.closeDropdown();
967
+ }
968
+ }
969
+ handleEscapeKey(event) {
970
+ const keyboardEvent = event;
971
+ if (keyboardEvent.key === 'Escape' && this._state.open) {
972
+ keyboardEvent.preventDefault();
973
+ this.closeDropdown();
974
+ }
975
+ }
976
+ openDropdown() {
977
+ if (this._state.open)
978
+ return;
979
+ if (isMobileTouch())
980
+ return;
981
+ this.updateState({ open: true });
982
+ this.dispatchEvent(new CustomEvent('open'));
983
+ this.render();
984
+ requestAnimationFrame(() => {
985
+ if (!this.shadowRoot)
986
+ return;
987
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
988
+ if (dialog) {
989
+ const pickerId = `date-picker-${this.id || this.toString()}`;
990
+ lockScroll(pickerId);
991
+ dialog.showModal();
992
+ this.calculateCalendarPosition();
993
+ dialog.classList.add('open');
994
+ const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
995
+ if (stubEl)
996
+ stubEl.setAttribute('aria-expanded', 'true');
997
+ const focusedElement = this.shadowRoot.activeElement;
998
+ if (focusedElement) {
999
+ focusedElement.blur();
1000
+ }
1001
+ }
1002
+ });
1003
+ }
1004
+ closeDropdown() {
1005
+ if (!this._state.open)
1006
+ return;
1007
+ this._closeTimestamp = Date.now();
1008
+ const pickerId = `date-picker-${this.id || this.toString()}`;
1009
+ this.updateState({ open: false }, true);
1010
+ unlockScroll(pickerId);
1011
+ if (this.shadowRoot) {
1012
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1013
+ if (dialog) {
1014
+ dialog.classList.remove('position-above', 'position-below', 'open');
1015
+ dialog.close();
1016
+ }
1017
+ const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
1018
+ if (stubEl)
1019
+ stubEl.setAttribute('aria-expanded', 'false');
1020
+ }
1021
+ this.dispatchEvent(new CustomEvent('close'));
1022
+ this.render();
1023
+ }
1024
+ cleanup() {
1025
+ if (this._clickListener) {
1026
+ document.removeEventListener('click', this._clickListener);
1027
+ }
1028
+ if (this._keydownListener) {
1029
+ document.removeEventListener('keydown', this._keydownListener);
1030
+ }
1031
+ if (this._dialogClickListener && this.shadowRoot) {
1032
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1033
+ if (dialog) {
1034
+ dialog.removeEventListener('click', this._dialogClickListener);
1035
+ }
1036
+ }
1037
+ if (this._state.open) {
1038
+ const pickerId = `date-picker-${this.id || this.toString()}`;
1039
+ unlockScroll(pickerId);
1040
+ }
1041
+ this._clickListener = undefined;
1042
+ this._keydownListener = undefined;
1043
+ this._dialogClickListener = undefined;
1044
+ this._timeInput = undefined;
1045
+ }
1047
1046
  }
1048
1047
  TyDatePicker.properties = {
1049
- placement: {
1050
- type: 'string',
1051
- visual: true,
1052
- default: '',
1053
- },
1054
- size: {
1055
- type: 'string',
1056
- visual: true,
1057
- default: 'md',
1058
- validate: (v) => ['sm', 'md', 'lg'].includes(v),
1059
- coerce: (v) => {
1060
- if (v === 'xs')
1061
- return 'sm';
1062
- if (v === 'xl')
1063
- return 'lg';
1064
- if (!['sm', 'md', 'lg'].includes(v)) {
1065
- console.warn(`[ty-date-picker] Invalid size '${v}'. Using 'md'.`);
1066
- return 'md';
1067
- }
1068
- return v;
1069
- }
1070
- },
1071
- flavor: {
1072
- type: 'string',
1073
- visual: true,
1074
- default: 'default'
1075
- },
1076
- label: {
1077
- type: 'string',
1078
- visual: true,
1079
- default: ''
1080
- },
1081
- placeholder: {
1082
- type: 'string',
1083
- visual: true,
1084
- default: 'Select date...'
1085
- },
1086
- name: {
1087
- type: 'string',
1088
- visual: false,
1089
- default: ''
1090
- },
1091
- min: {
1092
- type: 'string',
1093
- visual: true,
1094
- default: ''
1095
- },
1096
- max: {
1097
- type: 'string',
1098
- visual: true,
1099
- default: ''
1100
- },
1101
- required: {
1102
- type: 'boolean',
1103
- visual: true,
1104
- default: false
1105
- },
1106
- disabled: {
1107
- type: 'boolean',
1108
- visual: true,
1109
- default: false
1110
- },
1111
- clearable: {
1112
- type: 'boolean',
1113
- visual: true,
1114
- default: true,
1115
- aliases: {
1116
- 'not-clearable': false
1117
- }
1118
- },
1119
- format: {
1120
- type: 'string',
1121
- visual: true,
1122
- default: 'long',
1123
- validate: (v) => ['short', 'medium', 'long', 'full'].includes(v),
1124
- coerce: (v) => {
1125
- const validFormats = ['short', 'medium', 'long', 'full'];
1126
- if (!validFormats.includes(v)) {
1127
- console.warn(`[ty-date-picker] Invalid format '${v}'. Using 'long'.`);
1128
- return 'long';
1129
- }
1130
- return v;
1131
- }
1132
- },
1133
- locale: {
1134
- type: 'string',
1135
- visual: true,
1136
- default: ''
1137
- },
1138
- value: {
1139
- type: 'string',
1140
- visual: true,
1141
- formValue: true,
1142
- emitChange: false,
1143
- default: null
1144
- },
1145
- 'with-time': {
1146
- type: 'boolean',
1147
- visual: true,
1148
- default: false
1149
- }
1048
+ placement: {
1049
+ type: 'string',
1050
+ visual: true,
1051
+ default: '',
1052
+ },
1053
+ size: {
1054
+ type: 'string',
1055
+ visual: true,
1056
+ default: DEFAULT_SIZE,
1057
+ validate: (v) => ["xs", "sm", "md", "lg", "xl"].includes(v),
1058
+ coerce: (v) => {
1059
+ if (!["xs", "sm", "md", "lg", "xl"].includes(v)) {
1060
+ console.warn(`[ty-date-picker] Invalid size '${v}'. Using '${DEFAULT_SIZE}'.`);
1061
+ return DEFAULT_SIZE;
1062
+ }
1063
+ return v;
1064
+ }
1065
+ },
1066
+ flavor: {
1067
+ type: 'string',
1068
+ visual: true,
1069
+ default: 'default'
1070
+ },
1071
+ label: {
1072
+ type: 'string',
1073
+ visual: true,
1074
+ default: ''
1075
+ },
1076
+ placeholder: {
1077
+ type: 'string',
1078
+ visual: true,
1079
+ default: 'Select date...'
1080
+ },
1081
+ name: {
1082
+ type: 'string',
1083
+ visual: false,
1084
+ default: ''
1085
+ },
1086
+ min: {
1087
+ type: 'string',
1088
+ visual: true,
1089
+ default: ''
1090
+ },
1091
+ max: {
1092
+ type: 'string',
1093
+ visual: true,
1094
+ default: ''
1095
+ },
1096
+ required: {
1097
+ type: 'boolean',
1098
+ visual: true,
1099
+ default: false
1100
+ },
1101
+ disabled: {
1102
+ type: 'boolean',
1103
+ visual: true,
1104
+ default: false
1105
+ },
1106
+ clearable: {
1107
+ type: 'boolean',
1108
+ visual: true,
1109
+ default: true,
1110
+ aliases: {
1111
+ 'not-clearable': false
1112
+ }
1113
+ },
1114
+ format: {
1115
+ type: 'string',
1116
+ visual: true,
1117
+ default: 'long',
1118
+ validate: (v) => ['short', 'medium', 'long', 'full'].includes(v),
1119
+ coerce: (v) => {
1120
+ const validFormats = ['short', 'medium', 'long', 'full'];
1121
+ if (!validFormats.includes(v)) {
1122
+ console.warn(`[ty-date-picker] Invalid format '${v}'. Using 'long'.`);
1123
+ return 'long';
1124
+ }
1125
+ return v;
1126
+ }
1127
+ },
1128
+ locale: {
1129
+ type: 'string',
1130
+ visual: true,
1131
+ default: ''
1132
+ },
1133
+ value: {
1134
+ type: 'string',
1135
+ visual: true,
1136
+ formValue: true,
1137
+ emitChange: false,
1138
+ default: null
1139
+ },
1140
+ 'with-time': {
1141
+ type: 'boolean',
1142
+ visual: true,
1143
+ default: false
1144
+ }
1150
1145
  };
1151
1146
  TyDatePicker.formAssociated = true;
1152
1147
  if (!customElements.get('ty-date-picker')) {
1153
- customElements.define('ty-date-picker', TyDatePicker);
1148
+ customElements.define('ty-date-picker', TyDatePicker);
1154
1149
  }