tyrell-components 1.0.0-RC13 → 1.0.0-RC15

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 (358) hide show
  1. package/README.md +10 -2
  2. package/css/tyrell-theme.css +192 -180
  3. package/css/tyrell.css +23 -0
  4. package/dist/tyrell-colors-static.css +1 -316
  5. package/dist/tyrell-theme.css +1 -1454
  6. package/dist/tyrell.css +1 -1371
  7. package/dist/tyrell.js +1 -1
  8. package/lib/base/ty-component.d.ts +20 -10
  9. package/lib/base/ty-component.js +196 -319
  10. package/lib/components/button.d.ts +3 -17
  11. package/lib/components/button.js +261 -295
  12. package/lib/components/calendar-month.d.ts +4 -50
  13. package/lib/components/calendar-month.js +443 -582
  14. package/lib/components/calendar-navigation.d.ts +3 -48
  15. package/lib/components/calendar-navigation.js +239 -362
  16. package/lib/components/calendar.d.ts +8 -83
  17. package/lib/components/calendar.js +616 -829
  18. package/lib/components/checkbox.d.ts +5 -72
  19. package/lib/components/checkbox.js +273 -416
  20. package/lib/components/copy.d.ts +4 -57
  21. package/lib/components/copy.js +280 -397
  22. package/lib/components/date-picker.d.ts +21 -163
  23. package/lib/components/date-picker.js +1143 -1674
  24. package/lib/components/file-upload.d.ts +9 -15
  25. package/lib/components/file-upload.js +404 -426
  26. package/lib/components/icon.d.ts +3 -24
  27. package/lib/components/icon.js +141 -230
  28. package/lib/components/input.d.ts +8 -108
  29. package/lib/components/input.js +593 -784
  30. package/lib/components/modal.d.ts +17 -26
  31. package/lib/components/modal.js +340 -484
  32. package/lib/components/option.d.ts +2 -9
  33. package/lib/components/option.js +168 -216
  34. package/lib/components/popup.d.ts +2 -17
  35. package/lib/components/popup.js +249 -380
  36. package/lib/components/radio.d.ts +5 -25
  37. package/lib/components/radio.js +407 -461
  38. package/lib/components/resize-observer.d.ts +3 -30
  39. package/lib/components/resize-observer.js +69 -103
  40. package/lib/components/scroll-container.d.ts +2 -12
  41. package/lib/components/scroll-container.js +293 -338
  42. package/lib/components/select.d.ts +87 -159
  43. package/lib/components/select.js +1609 -2090
  44. package/lib/components/selected-tags.d.ts +5 -48
  45. package/lib/components/selected-tags.js +188 -276
  46. package/lib/components/step.d.ts +1 -6
  47. package/lib/components/step.js +62 -68
  48. package/lib/components/switch.d.ts +1 -12
  49. package/lib/components/switch.js +214 -247
  50. package/lib/components/tab.d.ts +1 -6
  51. package/lib/components/tab.js +62 -69
  52. package/lib/components/tabs.d.ts +6 -63
  53. package/lib/components/tabs.js +545 -777
  54. package/lib/components/tag.d.ts +3 -37
  55. package/lib/components/tag.js +236 -335
  56. package/lib/components/textarea.d.ts +7 -87
  57. package/lib/components/textarea.js +457 -627
  58. package/lib/components/tooltip.d.ts +3 -11
  59. package/lib/components/tooltip.js +307 -443
  60. package/lib/components/wizard.d.ts +6 -54
  61. package/lib/components/wizard.js +405 -618
  62. package/lib/icons/fontawesome/brands.d.ts +0 -1
  63. package/lib/icons/fontawesome/brands.js +0 -8
  64. package/lib/icons/fontawesome/regular.d.ts +0 -1
  65. package/lib/icons/fontawesome/regular.js +0 -8
  66. package/lib/icons/fontawesome/solid.d.ts +0 -1
  67. package/lib/icons/fontawesome/solid.js +0 -8
  68. package/lib/icons/heroicons/micro.d.ts +0 -1
  69. package/lib/icons/heroicons/micro.js +392 -400
  70. package/lib/icons/heroicons/mini.d.ts +0 -1
  71. package/lib/icons/heroicons/mini.js +382 -390
  72. package/lib/icons/heroicons/outline.d.ts +0 -1
  73. package/lib/icons/heroicons/outline.js +337 -345
  74. package/lib/icons/heroicons/solid.d.ts +0 -1
  75. package/lib/icons/heroicons/solid.js +407 -415
  76. package/lib/icons/lucide.d.ts +0 -1
  77. package/lib/icons/lucide.js +24479 -26351
  78. package/lib/icons/material/filled.d.ts +0 -1
  79. package/lib/icons/material/filled.js +9653 -9663
  80. package/lib/icons/material/outlined.d.ts +0 -1
  81. package/lib/icons/material/outlined.js +10271 -10281
  82. package/lib/icons/material/round.d.ts +0 -1
  83. package/lib/icons/material/round.js +10495 -10505
  84. package/lib/icons/material/sharp.d.ts +0 -1
  85. package/lib/icons/material/sharp.js +9905 -9915
  86. package/lib/icons/material/two-tone.d.ts +0 -1
  87. package/lib/icons/material/two-tone.js +12792 -12802
  88. package/lib/index.d.ts +0 -1
  89. package/lib/index.js +21 -41
  90. package/lib/styles/button.d.ts +2 -20
  91. package/lib/styles/button.js +202 -404
  92. package/lib/styles/calendar-month.d.ts +1 -5
  93. package/lib/styles/calendar-month.js +127 -244
  94. package/lib/styles/calendar-navigation.d.ts +2 -6
  95. package/lib/styles/calendar-navigation.js +64 -104
  96. package/lib/styles/calendar.d.ts +1 -2
  97. package/lib/styles/calendar.js +9 -22
  98. package/lib/styles/checkbox.d.ts +0 -1
  99. package/lib/styles/checkbox.js +45 -94
  100. package/lib/styles/copy.d.ts +0 -1
  101. package/lib/styles/copy.js +67 -106
  102. package/lib/styles/custom-scrollbar.d.ts +1 -2
  103. package/lib/styles/custom-scrollbar.js +80 -115
  104. package/lib/styles/date-picker.d.ts +1 -5
  105. package/lib/styles/date-picker.js +261 -335
  106. package/lib/styles/field-size.d.ts +6 -0
  107. package/lib/styles/field-size.js +91 -0
  108. package/lib/styles/file-upload.d.ts +1 -2
  109. package/lib/styles/file-upload.js +143 -188
  110. package/lib/styles/icon.d.ts +2 -6
  111. package/lib/styles/icon.js +92 -170
  112. package/lib/styles/input.d.ts +1 -6
  113. package/lib/styles/input.js +125 -290
  114. package/lib/styles/modal.d.ts +1 -2
  115. package/lib/styles/modal.js +71 -108
  116. package/lib/styles/option.d.ts +2 -6
  117. package/lib/styles/option.js +49 -64
  118. package/lib/styles/popup.d.ts +1 -2
  119. package/lib/styles/popup.js +42 -77
  120. package/lib/styles/radio.d.ts +0 -1
  121. package/lib/styles/radio.js +69 -110
  122. package/lib/styles/resize-observer.d.ts +1 -2
  123. package/lib/styles/resize-observer.js +2 -12
  124. package/lib/styles/scroll-container.d.ts +1 -2
  125. package/lib/styles/scroll-container.js +100 -149
  126. package/lib/styles/select-base.d.ts +0 -1
  127. package/lib/styles/select-base.js +597 -785
  128. package/lib/styles/select.d.ts +1 -2
  129. package/lib/styles/select.js +140 -236
  130. package/lib/styles/step.d.ts +1 -2
  131. package/lib/styles/step.js +22 -33
  132. package/lib/styles/switch.d.ts +0 -1
  133. package/lib/styles/switch.js +34 -73
  134. package/lib/styles/tab.d.ts +1 -2
  135. package/lib/styles/tab.js +22 -34
  136. package/lib/styles/tabs.d.ts +3 -11
  137. package/lib/styles/tabs.js +190 -222
  138. package/lib/styles/tag.d.ts +1 -5
  139. package/lib/styles/tag.js +136 -218
  140. package/lib/styles/textarea.d.ts +1 -2
  141. package/lib/styles/textarea.js +210 -319
  142. package/lib/styles/tooltip.d.ts +0 -1
  143. package/lib/styles/tooltip.js +3 -13
  144. package/lib/styles/wizard.d.ts +5 -21
  145. package/lib/styles/wizard.js +170 -306
  146. package/lib/types/common.d.ts +2 -2
  147. package/lib/types/common.js +6 -17
  148. package/lib/utils/calendar-utils.d.ts +4 -21
  149. package/lib/utils/calendar-utils.js +100 -273
  150. package/lib/utils/custom-scrollbar.d.ts +6 -20
  151. package/lib/utils/custom-scrollbar.js +273 -317
  152. package/lib/utils/flavor-sheet.d.ts +0 -1
  153. package/lib/utils/flavor-sheet.js +14 -37
  154. package/lib/utils/icon-registry.d.ts +0 -2
  155. package/lib/utils/icon-registry.js +137 -243
  156. package/lib/utils/icons.d.ts +0 -1
  157. package/lib/utils/icons.js +0 -9
  158. package/lib/utils/loader-registry.d.ts +0 -1
  159. package/lib/utils/loader-registry.js +3 -35
  160. package/lib/utils/locale.d.ts +2 -23
  161. package/lib/utils/locale.js +34 -120
  162. package/lib/utils/mobile.d.ts +0 -1
  163. package/lib/utils/mobile.js +1 -18
  164. package/lib/utils/number-format.d.ts +6 -3
  165. package/lib/utils/number-format.js +54 -110
  166. package/lib/utils/positioning.d.ts +50 -7
  167. package/lib/utils/positioning.js +224 -234
  168. package/lib/utils/property-capture.d.ts +5 -27
  169. package/lib/utils/property-capture.js +36 -144
  170. package/lib/utils/property-manager.d.ts +0 -1
  171. package/lib/utils/property-manager.js +126 -196
  172. package/lib/utils/resize-observer.d.ts +0 -1
  173. package/lib/utils/resize-observer.js +23 -59
  174. package/lib/utils/scroll-lock.d.ts +4 -11
  175. package/lib/utils/scroll-lock.js +65 -172
  176. package/lib/utils/styles.d.ts +0 -1
  177. package/lib/utils/styles.js +17 -41
  178. package/lib/version.d.ts +1 -2
  179. package/lib/version.js +1 -11
  180. package/package.json +4 -4
  181. package/lib/base/ty-component.d.ts.map +0 -1
  182. package/lib/base/ty-component.js.map +0 -1
  183. package/lib/components/button.d.ts.map +0 -1
  184. package/lib/components/button.js.map +0 -1
  185. package/lib/components/calendar-month.d.ts.map +0 -1
  186. package/lib/components/calendar-month.js.map +0 -1
  187. package/lib/components/calendar-navigation.d.ts.map +0 -1
  188. package/lib/components/calendar-navigation.js.map +0 -1
  189. package/lib/components/calendar.d.ts.map +0 -1
  190. package/lib/components/calendar.js.map +0 -1
  191. package/lib/components/checkbox.d.ts.map +0 -1
  192. package/lib/components/checkbox.js.map +0 -1
  193. package/lib/components/copy.d.ts.map +0 -1
  194. package/lib/components/copy.js.map +0 -1
  195. package/lib/components/date-picker.d.ts.map +0 -1
  196. package/lib/components/date-picker.js.map +0 -1
  197. package/lib/components/file-upload.d.ts.map +0 -1
  198. package/lib/components/file-upload.js.map +0 -1
  199. package/lib/components/icon.d.ts.map +0 -1
  200. package/lib/components/icon.js.map +0 -1
  201. package/lib/components/input.d.ts.map +0 -1
  202. package/lib/components/input.js.map +0 -1
  203. package/lib/components/modal.d.ts.map +0 -1
  204. package/lib/components/modal.js.map +0 -1
  205. package/lib/components/option.d.ts.map +0 -1
  206. package/lib/components/option.js.map +0 -1
  207. package/lib/components/popup.d.ts.map +0 -1
  208. package/lib/components/popup.js.map +0 -1
  209. package/lib/components/radio.d.ts.map +0 -1
  210. package/lib/components/radio.js.map +0 -1
  211. package/lib/components/resize-observer.d.ts.map +0 -1
  212. package/lib/components/resize-observer.js.map +0 -1
  213. package/lib/components/scroll-container.d.ts.map +0 -1
  214. package/lib/components/scroll-container.js.map +0 -1
  215. package/lib/components/select.d.ts.map +0 -1
  216. package/lib/components/select.js.map +0 -1
  217. package/lib/components/selected-tags.d.ts.map +0 -1
  218. package/lib/components/selected-tags.js.map +0 -1
  219. package/lib/components/step.d.ts.map +0 -1
  220. package/lib/components/step.js.map +0 -1
  221. package/lib/components/switch.d.ts.map +0 -1
  222. package/lib/components/switch.js.map +0 -1
  223. package/lib/components/tab.d.ts.map +0 -1
  224. package/lib/components/tab.js.map +0 -1
  225. package/lib/components/tabs.d.ts.map +0 -1
  226. package/lib/components/tabs.js.map +0 -1
  227. package/lib/components/tag.d.ts.map +0 -1
  228. package/lib/components/tag.js.map +0 -1
  229. package/lib/components/textarea.d.ts.map +0 -1
  230. package/lib/components/textarea.js.map +0 -1
  231. package/lib/components/tooltip.d.ts.map +0 -1
  232. package/lib/components/tooltip.js.map +0 -1
  233. package/lib/components/wizard.d.ts.map +0 -1
  234. package/lib/components/wizard.js.map +0 -1
  235. package/lib/icons/fontawesome/brands.d.ts.map +0 -1
  236. package/lib/icons/fontawesome/brands.js.map +0 -1
  237. package/lib/icons/fontawesome/regular.d.ts.map +0 -1
  238. package/lib/icons/fontawesome/regular.js.map +0 -1
  239. package/lib/icons/fontawesome/solid.d.ts.map +0 -1
  240. package/lib/icons/fontawesome/solid.js.map +0 -1
  241. package/lib/icons/heroicons/micro.d.ts.map +0 -1
  242. package/lib/icons/heroicons/micro.js.map +0 -1
  243. package/lib/icons/heroicons/mini.d.ts.map +0 -1
  244. package/lib/icons/heroicons/mini.js.map +0 -1
  245. package/lib/icons/heroicons/outline.d.ts.map +0 -1
  246. package/lib/icons/heroicons/outline.js.map +0 -1
  247. package/lib/icons/heroicons/solid.d.ts.map +0 -1
  248. package/lib/icons/heroicons/solid.js.map +0 -1
  249. package/lib/icons/lucide.d.ts.map +0 -1
  250. package/lib/icons/lucide.js.map +0 -1
  251. package/lib/icons/material/filled.d.ts.map +0 -1
  252. package/lib/icons/material/filled.js.map +0 -1
  253. package/lib/icons/material/outlined.d.ts.map +0 -1
  254. package/lib/icons/material/outlined.js.map +0 -1
  255. package/lib/icons/material/round.d.ts.map +0 -1
  256. package/lib/icons/material/round.js.map +0 -1
  257. package/lib/icons/material/sharp.d.ts.map +0 -1
  258. package/lib/icons/material/sharp.js.map +0 -1
  259. package/lib/icons/material/two-tone.d.ts.map +0 -1
  260. package/lib/icons/material/two-tone.js.map +0 -1
  261. package/lib/index.d.ts.map +0 -1
  262. package/lib/index.js.map +0 -1
  263. package/lib/styles/button.d.ts.map +0 -1
  264. package/lib/styles/button.js.map +0 -1
  265. package/lib/styles/calendar-month.d.ts.map +0 -1
  266. package/lib/styles/calendar-month.js.map +0 -1
  267. package/lib/styles/calendar-navigation.d.ts.map +0 -1
  268. package/lib/styles/calendar-navigation.js.map +0 -1
  269. package/lib/styles/calendar.d.ts.map +0 -1
  270. package/lib/styles/calendar.js.map +0 -1
  271. package/lib/styles/checkbox.d.ts.map +0 -1
  272. package/lib/styles/checkbox.js.map +0 -1
  273. package/lib/styles/copy.d.ts.map +0 -1
  274. package/lib/styles/copy.js.map +0 -1
  275. package/lib/styles/custom-scrollbar.d.ts.map +0 -1
  276. package/lib/styles/custom-scrollbar.js.map +0 -1
  277. package/lib/styles/date-picker.d.ts.map +0 -1
  278. package/lib/styles/date-picker.js.map +0 -1
  279. package/lib/styles/file-upload.d.ts.map +0 -1
  280. package/lib/styles/file-upload.js.map +0 -1
  281. package/lib/styles/icon.d.ts.map +0 -1
  282. package/lib/styles/icon.js.map +0 -1
  283. package/lib/styles/input.d.ts.map +0 -1
  284. package/lib/styles/input.js.map +0 -1
  285. package/lib/styles/modal.d.ts.map +0 -1
  286. package/lib/styles/modal.js.map +0 -1
  287. package/lib/styles/multiselect.d.ts +0 -8
  288. package/lib/styles/multiselect.d.ts.map +0 -1
  289. package/lib/styles/multiselect.js +0 -849
  290. package/lib/styles/multiselect.js.map +0 -1
  291. package/lib/styles/option.d.ts.map +0 -1
  292. package/lib/styles/option.js.map +0 -1
  293. package/lib/styles/popup.d.ts.map +0 -1
  294. package/lib/styles/popup.js.map +0 -1
  295. package/lib/styles/radio.d.ts.map +0 -1
  296. package/lib/styles/radio.js.map +0 -1
  297. package/lib/styles/resize-observer.d.ts.map +0 -1
  298. package/lib/styles/resize-observer.js.map +0 -1
  299. package/lib/styles/scroll-container.d.ts.map +0 -1
  300. package/lib/styles/scroll-container.js.map +0 -1
  301. package/lib/styles/select-base.d.ts.map +0 -1
  302. package/lib/styles/select-base.js.map +0 -1
  303. package/lib/styles/select.d.ts.map +0 -1
  304. package/lib/styles/select.js.map +0 -1
  305. package/lib/styles/step.d.ts.map +0 -1
  306. package/lib/styles/step.js.map +0 -1
  307. package/lib/styles/switch.d.ts.map +0 -1
  308. package/lib/styles/switch.js.map +0 -1
  309. package/lib/styles/tab.d.ts.map +0 -1
  310. package/lib/styles/tab.js.map +0 -1
  311. package/lib/styles/tabs.d.ts.map +0 -1
  312. package/lib/styles/tabs.js.map +0 -1
  313. package/lib/styles/tag.d.ts.map +0 -1
  314. package/lib/styles/tag.js.map +0 -1
  315. package/lib/styles/textarea.d.ts.map +0 -1
  316. package/lib/styles/textarea.js.map +0 -1
  317. package/lib/styles/tooltip.d.ts.map +0 -1
  318. package/lib/styles/tooltip.js.map +0 -1
  319. package/lib/styles/wizard.d.ts.map +0 -1
  320. package/lib/styles/wizard.js.map +0 -1
  321. package/lib/types/common.d.ts.map +0 -1
  322. package/lib/types/common.js.map +0 -1
  323. package/lib/utils/calendar-utils.d.ts.map +0 -1
  324. package/lib/utils/calendar-utils.js.map +0 -1
  325. package/lib/utils/custom-scrollbar.d.ts.map +0 -1
  326. package/lib/utils/custom-scrollbar.js.map +0 -1
  327. package/lib/utils/flavor-sheet.d.ts.map +0 -1
  328. package/lib/utils/flavor-sheet.js.map +0 -1
  329. package/lib/utils/icon-registry.d.ts.map +0 -1
  330. package/lib/utils/icon-registry.js.map +0 -1
  331. package/lib/utils/icons.d.ts.map +0 -1
  332. package/lib/utils/icons.js.map +0 -1
  333. package/lib/utils/loader-registry.d.ts.map +0 -1
  334. package/lib/utils/loader-registry.js.map +0 -1
  335. package/lib/utils/locale.d.ts.map +0 -1
  336. package/lib/utils/locale.js.map +0 -1
  337. package/lib/utils/mobile.d.ts.map +0 -1
  338. package/lib/utils/mobile.js.map +0 -1
  339. package/lib/utils/number-format.d.ts.map +0 -1
  340. package/lib/utils/number-format.js.map +0 -1
  341. package/lib/utils/parse-boolean.d.ts +0 -39
  342. package/lib/utils/parse-boolean.d.ts.map +0 -1
  343. package/lib/utils/parse-boolean.js +0 -58
  344. package/lib/utils/parse-boolean.js.map +0 -1
  345. package/lib/utils/positioning.d.ts.map +0 -1
  346. package/lib/utils/positioning.js.map +0 -1
  347. package/lib/utils/property-capture.d.ts.map +0 -1
  348. package/lib/utils/property-capture.js.map +0 -1
  349. package/lib/utils/property-manager.d.ts.map +0 -1
  350. package/lib/utils/property-manager.js.map +0 -1
  351. package/lib/utils/resize-observer.d.ts.map +0 -1
  352. package/lib/utils/resize-observer.js.map +0 -1
  353. package/lib/utils/scroll-lock.d.ts.map +0 -1
  354. package/lib/utils/scroll-lock.js.map +0 -1
  355. package/lib/utils/styles.d.ts.map +0 -1
  356. package/lib/utils/styles.js.map +0 -1
  357. package/lib/version.d.ts.map +0 -1
  358. package/lib/version.js.map +0 -1
@@ -1,1705 +1,1174 @@
1
- /**
2
- * TyDatePicker Web Component
3
- * PORTED FROM: cljs/ty/components/date_picker.cljs
4
- *
5
- * A date picker component with read-only input and calendar dropdown.
6
- * Supports date-only and date+time modes with smart time input.
7
- *
8
- * Architecture:
9
- * - Read-only input stub (displays formatted date)
10
- * - Calendar dropdown (modal dialog with ty-calendar)
11
- * - Optional time input (with smart digit navigation)
12
- * - Form participation via ElementInternals
13
- * - UTC output, local display
14
- *
15
- * Features:
16
- * - Date selection with calendar dropdown
17
- * - Optional time input with smart navigation
18
- * - Form integration (works with FormData)
19
- * - UTC value output for server communication
20
- * - Localized display formatting (Intl API)
21
- * - Clearable with clear button
22
- * - Keyboard navigation (Escape to close)
23
- * - Outside click handling
24
- *
25
- * @example
26
- * ```html
27
- * <!-- Basic date picker -->
28
- * <ty-date-picker
29
- * label="Select Date"
30
- * placeholder="Choose a date">
31
- * </ty-date-picker>
32
- *
33
- * <!-- With time -->
34
- * <ty-date-picker
35
- * with-time="true"
36
- * label="Select Date & Time">
37
- * </ty-date-picker>
38
- *
39
- * <!-- Form integration -->
40
- * <form>
41
- * <ty-date-picker name="booking-date"></ty-date-picker>
42
- * <button type="submit">Submit</button>
43
- * </form>
44
- *
45
- * <!-- Pre-filled value (UTC) -->
46
- * <ty-date-picker value="2024-09-21T08:30:00.000Z"></ty-date-picker>
47
- * ```
48
- */
49
1
  import { ensureStyles } from '../utils/styles.js';
50
2
  import { syncCustomFlavorSheet } from '../utils/flavor-sheet.js';
51
3
  import { datePickerStyles, datePickerCustomFlavorCss } from '../styles/date-picker.js';
52
4
  import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
53
- import { computeAnchoredPosition } from '../utils/positioning.js';
5
+ import { computeAnchoredPosition, placementToAnchored } from '../utils/positioning.js';
54
6
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
55
7
  import { isMobileTouch } from '../utils/mobile.js';
56
8
  import { TyComponent } from '../base/ty-component.js';
57
- // ============================================================================
58
- // Constants
59
- // ============================================================================
9
+ import { DEFAULT_SIZE } from '../types/common.js';
60
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>`;
61
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>`;
62
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>`;
63
- // Stable per-instance id (survives across full-rebuild re-renders, unlike a
64
- // fresh Math.random() per render) — used to wire the stub's aria-labelledby
65
- // to the label element it's paired with. Same pattern as select.ts's
66
- // getElementHash.
67
13
  const elementIds = new WeakMap();
68
14
  let elementIdCounter = 0;
69
15
  function getElementHash(element) {
70
- let id = elementIds.get(element);
71
- if (id === undefined) {
72
- id = ++elementIdCounter;
73
- elementIds.set(element, id);
74
- }
75
- return id;
76
- }
77
- // ============================================================================
78
- // Helper Functions - Date Parsing & Conversion
79
- // ============================================================================
80
- /**
81
- * Parse ANY input format into year/month/day/hour/minute components.
82
- * Accepts:
83
- * - UTC strings: '2024-09-21T08:30:00Z' or '2024-09-21T08:30:00.000Z'
84
- * - Datetime-local: '2024-09-21T10:30'
85
- * - Date only: '2024-09-21'
86
- * - Timestamps: milliseconds since epoch
87
- * - With timezone: '2024-09-21T10:30:00+02:00'
88
- *
89
- * Always extracts components in LOCAL timezone for display/editing.
90
- *
91
- * PORTED FROM: parse-value in date_picker.cljs
92
- */
16
+ let id = elementIds.get(element);
17
+ if (id === undefined) {
18
+ id = ++elementIdCounter;
19
+ elementIds.set(element, id);
20
+ }
21
+ return id;
22
+ }
93
23
  function parseValue(value, withTime) {
94
- if (!value)
95
- return null;
96
- let dateObj = null;
97
- if (typeof value === 'string') {
98
- // Date-only format: YYYY-MM-DD
99
- if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
100
- const [yearStr, monthStr, dayStr] = value.split('-');
101
- const year = parseInt(yearStr, 10);
102
- const month = parseInt(monthStr, 10) - 1; // 0-based for JS Date
103
- const day = parseInt(dayStr, 10);
104
- // Create at midnight local time
105
- dateObj = new Date(year, month, day, 0, 0, 0, 0);
106
- }
107
- else {
108
- // Let JS Date handle datetime-local, UTC, with timezone
109
- dateObj = new Date(value);
110
- }
111
- }
112
- else if (typeof value === 'number') {
113
- // Numeric inputs (milliseconds)
114
- dateObj = new Date(value);
115
- }
116
- if (!dateObj || isNaN(dateObj.getTime())) {
117
- return null;
118
- }
119
- // Extract components in LOCAL timezone for display
120
- const components = {
121
- year: dateObj.getFullYear(),
122
- month: dateObj.getMonth() + 1, // Convert to 1-based
123
- day: dateObj.getDate(),
124
- hour: withTime ? dateObj.getHours() : 0,
125
- minute: withTime ? dateObj.getMinutes() : 0,
126
- };
127
- return components;
128
- }
129
- /**
130
- * Convert internal components to Date object.
131
- *
132
- * PORTED FROM: components->date-object in date_picker.cljs
133
- */
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;
53
+ }
134
54
  function componentsToDateObject(components) {
135
- const { year, month, day, hour, minute } = components;
136
- if (!year || !month || !day)
137
- return null;
138
- // Create local date
139
- return new Date(year, month - 1, // Convert to 0-based
140
- day, hour || 0, minute || 0, 0, // seconds
141
- 0 // milliseconds
142
- );
143
- }
144
- /**
145
- * Convert internal components to UTC ISO 8601 format.
146
- *
147
- * For date+time mode: Outputs full UTC timestamp with milliseconds
148
- * Example: '2024-09-21T08:30:00.000Z'
149
- *
150
- * For date-only mode: Outputs UTC timestamp at midnight local time
151
- * Example: '2024-09-20T22:00:00.000Z' (midnight Sept 21 CEST = 10pm Sept 20 UTC)
152
- *
153
- * Always returns UTC to ensure unambiguous server communication.
154
- *
155
- * PORTED FROM: components->output-value in date_picker.cljs
156
- */
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);
59
+ }
157
60
  function componentsToOutputValue(components) {
158
- if (!components.year || !components.month || !components.day) {
159
- return null;
160
- }
161
- const dateObj = componentsToDateObject(components);
162
- return dateObj ? dateObj.toISOString() : null;
163
- }
164
- /**
165
- * Convert internal components to local datetime-local format.
166
- *
167
- * For date+time mode: Outputs local datetime without timezone
168
- * Example: '2024-09-21T10:30'
169
- *
170
- * For date-only mode: Outputs date only
171
- * Example: '2024-09-21'
172
- *
173
- * This format matches HTML5 <input type="datetime-local"> and is useful
174
- * for setting other inputs or displaying local time without timezone conversion.
175
- */
61
+ if (!components.year || !components.month || !components.day) {
62
+ return null;
63
+ }
64
+ const dateObj = componentsToDateObject(components);
65
+ return dateObj ? dateObj.toISOString() : null;
66
+ }
176
67
  function componentsToLocalValue(components, withTime) {
177
- if (!components.year || !components.month || !components.day) {
178
- return null;
179
- }
180
- const year = components.year.toString().padStart(4, '0');
181
- const month = components.month.toString().padStart(2, '0');
182
- const day = components.day.toString().padStart(2, '0');
183
- if (withTime) {
184
- const hour = (components.hour || 0).toString().padStart(2, '0');
185
- const minute = (components.minute || 0).toString().padStart(2, '0');
186
- return `${year}-${month}-${day}T${hour}:${minute}`;
187
- }
188
- else {
189
- return `${year}-${month}-${day}`;
190
- }
191
- }
192
- /**
193
- * Format components for display in input using Intl API.
194
- *
195
- * PORTED FROM: format-display-value in date_picker.cljs
196
- */
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
+ }
82
+ }
197
83
  function formatDisplayValue(components, formatType, locale, withTime) {
198
- if (!components.year || !components.month || !components.day) {
199
- return null;
200
- }
201
- const dateObj = componentsToDateObject(components);
202
- if (!dateObj)
203
- return null;
204
- const options = {
205
- dateStyle: formatType || 'long',
206
- };
207
- // Add time styling if with-time is enabled
208
- if (withTime) {
209
- options.timeStyle = 'short';
210
- }
211
- const formatter = new Intl.DateTimeFormat(locale, options);
212
- return formatter.format(dateObj);
213
- }
214
- // ============================================================================
215
- // Helper Functions - Time Input
216
- // ============================================================================
217
- /**
218
- * Parse hour and minute from raw digits (4 chars: "HHmm")
219
- *
220
- * PORTED FROM: parse-time-components in date_picker.cljs
221
- */
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);
98
+ }
222
99
  function parseTimeComponents(rawDigits) {
223
- if (!rawDigits || rawDigits.length !== 4)
224
- return null;
225
- const hourStr = rawDigits.substring(0, 2);
226
- const minuteStr = rawDigits.substring(2, 4);
227
- const hour = parseInt(hourStr, 10);
228
- const minute = parseInt(minuteStr, 10);
229
- if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
230
- return null;
231
- }
232
- return { hour, minute };
233
- }
234
- /**
235
- * Validate if digit is valid for given position
236
- * Position 0: first hour digit (0-2)
237
- * Position 1: second hour digit (0-9, but 0-3 if first is 2)
238
- * Position 3: first minute digit (0-5)
239
- * Position 4: second minute digit (0-9)
240
- *
241
- * PORTED FROM: validate-time-digit in date_picker.cljs
242
- */
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 };
110
+ }
243
111
  function validateTimeDigit(digit, position, currentDigits) {
244
- switch (position) {
245
- case 0:
246
- return digit <= 2; // First hour digit: 0-2
247
- case 1: {
248
- const firstHour = parseInt(currentDigits[0], 10);
249
- return firstHour === 2 ? digit <= 3 : true; // If hour starts with 2, max 23
250
- }
251
- case 3:
252
- return digit <= 5; // First minute digit: 0-5
253
- case 4:
254
- return true; // Second minute digit: 0-9
255
- default:
256
- return false;
257
- }
258
- }
259
- /**
260
- * Format hour and minute into "HH:mm" display
261
- *
262
- * PORTED FROM: format-time-display in date_picker.cljs
263
- */
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
+ }
126
+ }
264
127
  function formatTimeDisplay(hour, minute) {
265
- const hourStr = hour.toString().padStart(2, '0');
266
- const minuteStr = minute.toString().padStart(2, '0');
267
- return `${hourStr}:${minuteStr}`;
268
- }
269
- /**
270
- * Find next editable position, skipping delimiter at position 2
271
- *
272
- * PORTED FROM: find-next-editable-position in date_picker.cljs
273
- */
128
+ const hourStr = hour.toString().padStart(2, '0');
129
+ const minuteStr = minute.toString().padStart(2, '0');
130
+ return `${hourStr}:${minuteStr}`;
131
+ }
274
132
  function findNextEditablePosition(currentPos) {
275
- switch (currentPos) {
276
- case 0: return 1; // 0 -> 1 (within hour)
277
- case 1: return 3; // 1 -> 3 (skip delimiter, go to minute)
278
- case 3: return 4; // 3 -> 4 (within minute)
279
- case 4: return 5; // 4 -> 5 (after last digit)
280
- case 5: return 5; // 5 -> 5 (stay at end)
281
- default: return currentPos;
282
- }
283
- }
284
- /**
285
- * Find previous editable position, skipping delimiter at position 2
286
- *
287
- * PORTED FROM: find-prev-editable-position in date_picker.cljs
288
- */
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
+ }
141
+ }
289
142
  function findPrevEditablePosition(currentPos) {
290
- switch (currentPos) {
291
- case 5: return 4; // 5 -> 4 (from after last digit)
292
- case 4: return 3; // 4 -> 3 (within minute)
293
- case 3: return 1; // 3 -> 1 (skip delimiter, go to hour)
294
- case 1: return 0; // 1 -> 0 (within hour)
295
- case 0: return 0; // 0 -> 0 (stay at start)
296
- default: return currentPos;
297
- }
298
- }
299
- /**
300
- * Convert internal position (0,1,3,4) to raw digits index (0,1,2,3)
301
- *
302
- * PORTED FROM: position->raw-digit-index in date_picker.cljs
303
- */
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
+ }
151
+ }
304
152
  function positionToRawDigitIndex(internalPos) {
305
- switch (internalPos) {
306
- case 0: return 0; // Position 0 → raw digit 0 (first hour)
307
- case 1: return 1; // Position 1 → raw digit 1 (second hour)
308
- case 3: return 2; // Position 3 → raw digit 2 (first minute)
309
- case 4: return 3; // Position 4 → raw digit 3 (second minute)
310
- default: return 0;
311
- }
312
- }
313
- // ============================================================================
314
- // TimeInput Helper Class
315
- // ============================================================================
316
- /**
317
- * TimeInput manages the state and behavior of the time input element.
318
- * Handles smart cursor navigation, digit replacement, and validation.
319
- *
320
- * PORTED FROM: Time input state management functions in date_picker.cljs
321
- */
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
+ }
160
+ }
322
161
  class TimeInput {
323
- constructor(element, datePickerElement, hour = 0, minute = 0) {
324
- this.element = element;
325
- this.datePickerElement = datePickerElement;
326
- // Initialize state
327
- const display = formatTimeDisplay(hour, minute);
328
- const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
329
- this.state = {
330
- hour,
331
- minute,
332
- caretPosition: 0,
333
- displayValue: display,
334
- rawDigits,
335
- };
336
- // Set initial value
337
- this.element.value = display;
338
- // Bind event handlers
339
- this.setupEventListeners();
340
- }
341
- /**
342
- * Setup all event listeners for the time input
343
- */
344
- setupEventListeners() {
345
- this.element.addEventListener('keydown', (e) => this.handleKeyDown(e));
346
- this.element.addEventListener('input', (e) => this.handleInput(e));
347
- this.element.addEventListener('click', () => this.handleClick());
348
- this.element.addEventListener('focus', () => this.handleFocus());
349
- }
350
- /**
351
- * Handle keydown events
352
- */
353
- handleKeyDown(event) {
354
- const key = event.key;
355
- switch (key) {
356
- case 'ArrowRight':
357
- this.handleArrowRight(event);
358
- break;
359
- case 'ArrowLeft':
360
- this.handleArrowLeft(event);
361
- break;
362
- case 'Backspace':
363
- this.handleBackspace(event);
364
- break;
365
- case 'Delete':
366
- this.handleDelete(event);
367
- break;
368
- case 'Home':
369
- event.preventDefault();
370
- this.updateState({ caretPosition: 0 });
371
- break;
372
- case 'End':
373
- event.preventDefault();
374
- this.updateState({ caretPosition: 5 });
375
- break;
376
- case 'Tab':
377
- // Allow default tab behavior
378
- break;
379
- default:
380
- // Handle digit input
381
- if (/^\d$/.test(key)) {
382
- this.handleDigitInput(event, parseInt(key, 10));
383
- }
384
- break;
385
- }
386
- }
387
- /**
388
- * Handle input events (prevent default browser input)
389
- */
390
- handleInput(event) {
391
- event.preventDefault();
392
- }
393
- /**
394
- * Handle click events - position cursor at first digit
395
- */
396
- handleClick() {
397
- this.updateState({ caretPosition: 0 });
398
- }
399
- /**
400
- * Handle focus events - position cursor at first digit
401
- */
402
- handleFocus() {
403
- this.updateState({ caretPosition: 0 });
404
- }
405
- /**
406
- * Handle arrow right - move to next editable position
407
- *
408
- * PORTED FROM: handle-time-arrow-right! in date_picker.cljs
409
- */
410
- handleArrowRight(event) {
411
- event.preventDefault();
412
- const nextPos = findNextEditablePosition(this.state.caretPosition);
413
- this.updateState({ caretPosition: nextPos });
414
- }
415
- /**
416
- * Handle arrow left - move to previous editable position
417
- *
418
- * PORTED FROM: handle-time-arrow-left! in date_picker.cljs
419
- */
420
- handleArrowLeft(event) {
421
- event.preventDefault();
422
- const prevPos = findPrevEditablePosition(this.state.caretPosition);
423
- this.updateState({ caretPosition: prevPos });
424
- }
425
- /**
426
- * Handle digit input - replace digit at cursor position
427
- *
428
- * PORTED FROM: handle-time-digit-input! in date_picker.cljs
429
- */
430
- handleDigitInput(event, digit) {
431
- event.preventDefault();
432
- const currentPos = this.state.caretPosition;
433
- // Only process if at editable position and digit is valid
434
- if (![0, 1, 3, 4].includes(currentPos))
435
- return;
436
- if (!validateTimeDigit(digit, currentPos, this.state.rawDigits))
437
- return;
438
- // Replace digit at current position
439
- const newState = this.replaceDigitAtPosition(currentPos, digit);
440
- if (!newState)
441
- return;
442
- // Move to next position
443
- const nextPos = findNextEditablePosition(currentPos);
444
- this.updateState({ ...newState, caretPosition: nextPos });
445
- // Notify date picker of time change
446
- this.notifyTimeChange();
447
- }
448
- /**
449
- * Handle backspace - zero digit and move back
450
- *
451
- * PORTED FROM: handle-time-backspace! in date_picker.cljs
452
- */
453
- handleBackspace(event) {
454
- event.preventDefault();
455
- const currentPos = this.state.caretPosition;
456
- // Can't go back from position 0
457
- if (currentPos === 0)
458
- return;
459
- // Find target position to zero
460
- const targetPos = currentPos === 1 ? 0 :
461
- currentPos === 3 ? 1 :
462
- currentPos === 4 ? 3 :
463
- currentPos === 5 ? 4 : 0;
464
- const newState = this.zeroDigitAtPosition(targetPos);
465
- if (!newState)
466
- return;
467
- this.updateState({ ...newState, caretPosition: targetPos });
468
- this.notifyTimeChange();
469
- }
470
- /**
471
- * Handle delete - zero digit at current position
472
- *
473
- * PORTED FROM: handle-time-delete! in date_picker.cljs
474
- */
475
- handleDelete(event) {
476
- event.preventDefault();
477
- const currentPos = this.state.caretPosition;
478
- // Only at editable positions
479
- if (![0, 1, 3, 4].includes(currentPos))
480
- return;
481
- const newState = this.zeroDigitAtPosition(currentPos);
482
- if (!newState)
483
- return;
484
- this.updateState({ ...newState, caretPosition: currentPos });
485
- this.notifyTimeChange();
486
- }
487
- /**
488
- * Replace digit at specific position
489
- *
490
- * PORTED FROM: replace-digit-at-position in date_picker.cljs
491
- */
492
- replaceDigitAtPosition(position, newDigit) {
493
- const rawIndex = positionToRawDigitIndex(position);
494
- const digitsArray = this.state.rawDigits.split('');
495
- digitsArray[rawIndex] = newDigit.toString();
496
- const newDigits = digitsArray.join('');
497
- const parsed = parseTimeComponents(newDigits);
498
- if (!parsed)
499
- return null;
500
- return {
501
- rawDigits: newDigits,
502
- hour: parsed.hour,
503
- minute: parsed.minute,
504
- displayValue: formatTimeDisplay(parsed.hour, parsed.minute),
505
- };
506
- }
507
- /**
508
- * Zero digit at specific position
509
- *
510
- * PORTED FROM: zero-digit-at-position in date_picker.cljs
511
- */
512
- zeroDigitAtPosition(position) {
513
- return this.replaceDigitAtPosition(position, 0);
514
- }
515
- /**
516
- * Update internal state and refresh display
517
- *
518
- * PORTED FROM: update-time-input-state! in date_picker.cljs
519
- */
520
- updateState(updates) {
521
- this.state = { ...this.state, ...updates };
522
- // Update display value
523
- this.element.value = this.state.displayValue;
524
- // Set cursor position, mapping internal positions to DOM positions
525
- const caretPos = this.state.caretPosition;
526
- const actualPos = caretPos === 0 ? 0 :
527
- caretPos === 1 ? 1 :
528
- caretPos === 2 ? 3 :
529
- caretPos === 3 ? 3 :
530
- caretPos === 4 ? 4 :
531
- caretPos === 5 ? 5 : caretPos;
532
- this.element.setSelectionRange(actualPos, actualPos);
533
- }
534
- /**
535
- * Notify date picker of time change
536
- */
537
- notifyTimeChange() {
538
- this.datePickerElement.handleTimeInputChange(this.state.hour, this.state.minute);
539
- }
540
- /**
541
- * Get current time values
542
- */
543
- getTime() {
544
- return {
545
- hour: this.state.hour,
546
- minute: this.state.minute,
547
- };
548
- }
549
- /**
550
- * Update time from external source
551
- */
552
- setTime(hour, minute) {
553
- const display = formatTimeDisplay(hour, minute);
554
- const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
555
- this.state = {
556
- ...this.state,
557
- hour,
558
- minute,
559
- displayValue: display,
560
- rawDigits,
561
- };
562
- this.element.value = display;
563
- }
564
- }
565
- // ============================================================================
566
- // TyDatePicker Custom Element
567
- // ============================================================================
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
+ }
325
+ }
568
326
  export class TyDatePicker extends TyComponent {
569
- // observedAttributes is auto-generated by TyComponent from properties config
570
- constructor() {
571
- super(); // TyComponent handles attachShadow and attachInternals
572
- // ============================================================================
573
- // INTERNAL STATE
574
- // ============================================================================
575
- // Internal state
576
- this._state = {
577
- withTime: false,
578
- open: false,
579
- };
580
- // Reopen guard — prevents click event from reopening after pointerdown close
581
- this._closeTimestamp = 0;
582
- this._customFlavorSheet = null;
583
- // Initialize styles in shadow root
584
- const shadow = this.shadowRoot;
585
- ensureStyles(shadow, { css: datePickerStyles, id: 'ty-date-picker' });
586
- }
587
- // ==========================================================================
588
- // Lifecycle Hooks (TyComponent)
589
- // ==========================================================================
590
- /**
591
- * Called when component connects to DOM
592
- * TyComponent handles property capture automatically
593
- */
594
- onConnect() {
595
- // Initialize component state
596
- this.initializeState();
597
- // Initial render
598
- this.render();
599
- // Setup locale observer to watch for ancestor lang changes
600
- this._localeObserver = observeLocaleChanges(this, () => {
601
- this.render();
602
- });
603
- this._syncCustomFlavor();
604
- }
605
- /** Custom (non-built-in) flavors — see utils/flavor-sheet.ts. */
606
- _syncCustomFlavor() {
607
- // 'default' is this component's neutral: map it to a built-in so no
608
- // custom sheet is generated for it.
609
- const flavor = this.flavor === 'default' ? 'neutral' : this.flavor;
610
- this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, flavor, ({ base }) => datePickerCustomFlavorCss(base));
611
- }
612
- /**
613
- * Called when component disconnects from DOM
614
- */
615
- onDisconnect() {
616
- // Cleanup locale observer
617
- if (this._localeObserver) {
618
- this._localeObserver();
619
- this._localeObserver = undefined;
620
- }
621
- // Cleanup event listeners and state
622
- this.cleanup();
623
- }
624
- /**
625
- * Called when properties change
626
- * Handle state updates BEFORE render
627
- */
628
- onPropertiesChanged(changes) {
629
- for (const { name, newValue } of changes) {
630
- switch (name) {
631
- case 'flavor':
632
- this._syncCustomFlavor();
633
- break;
634
- // Simple visual properties - just trigger re-render (handled by TyComponent)
635
- case 'size':
636
- case 'label':
637
- case 'placeholder':
638
- case 'required':
639
- case 'disabled':
640
- case 'clearable':
641
- case 'format':
642
- case 'locale':
643
- // These properties just affect rendering, no internal state to update
644
- // TyComponent will call render() automatically for visual properties
645
- break;
646
- // Complex properties
647
- case 'value': {
648
- // Parse the new value using current withTime setting
649
- const newComponents = parseValue(newValue, this._state.withTime);
650
- // If newComponents is null, CLEAR the state completely
651
- if (newComponents === null) {
652
- // Check if we actually have a date to clear
653
- const hasDate = this._state.year !== undefined ||
654
- this._state.month !== undefined ||
655
- this._state.day !== undefined;
656
- if (hasDate) {
657
- // Clear all date components, keeping only withTime and open flags
658
- this._state = {
659
- withTime: this._state.withTime,
660
- open: this._state.open,
661
- // year, month, day, hour, minute are now undefined
662
- };
663
- }
664
- break;
665
- }
666
- // Check if components actually changed
667
- const currentComponents = {
668
- year: this._state.year,
669
- month: this._state.month,
670
- day: this._state.day,
671
- hour: this._state.hour,
672
- minute: this._state.minute,
673
- };
674
- const changed = newComponents?.year !== currentComponents.year ||
675
- newComponents?.month !== currentComponents.month ||
676
- newComponents?.day !== currentComponents.day ||
677
- newComponents?.hour !== currentComponents.hour ||
678
- newComponents?.minute !== currentComponents.minute;
679
- if (changed) {
680
- // Update state with new date components
681
- this._state = {
682
- ...this._state,
683
- ...newComponents,
684
- };
685
- }
686
- break;
687
- }
688
- case 'with-time': {
689
- // Update internal withTime flag
690
- const newWithTime = newValue;
691
- const oldWithTime = this._state.withTime;
692
- if (newWithTime !== oldWithTime) {
693
- this._state.withTime = newWithTime;
694
- // If we have an existing date, re-sync form value
695
- // (format changes based on withTime flag)
696
- const hasDate = this._state.year !== undefined &&
697
- this._state.month !== undefined &&
698
- this._state.day !== undefined;
699
- if (hasDate) {
700
- this.syncFormValue();
701
- }
702
- }
703
- break;
704
- }
705
- }
706
- }
707
- }
708
- // attributeChangedCallback removed - TyComponent handles all attribute changes via onPropertiesChanged
709
- /**
710
- * Get form value - returns UTC string from current state
711
- * TyComponent calls this automatically when value property changes
712
- */
713
- getFormValue() {
714
- return componentsToOutputValue(this.getComponents());
715
- }
716
- // ==========================================================================
717
- // Internal Helpers
718
- // ==========================================================================
719
- /**
720
- * Extract current date components from internal state
721
- */
722
- getComponents() {
723
- return {
724
- year: this._state.year,
725
- month: this._state.month,
726
- day: this._state.day,
727
- hour: this._state.hour,
728
- minute: this._state.minute,
729
- };
730
- }
731
- /**
732
- * Whether internal state has a complete date
733
- */
734
- hasDate() {
735
- return !!(this._state.year && this._state.month && this._state.day);
736
- }
737
- /**
738
- * Format current components for display using Intl API
739
- */
740
- getFormattedValue(components) {
741
- const c = components || this.getComponents();
742
- if (!c.year || !c.month || !c.day)
743
- return null;
744
- return formatDisplayValue(c, this.getProperty('format') || 'long', getEffectiveLocale(this, this.getProperty('locale')), this._state.withTime);
745
- }
746
- // ==========================================================================
747
- // State Management
748
- // ==========================================================================
749
- /**
750
- * Initialize component state from attributes
751
- *
752
- * PORTED FROM: init-component-state! in date_picker.cljs
753
- */
754
- initializeState() {
755
- const valueAttr = this.getProperty('value');
756
- const withTime = this.getProperty('with-time');
757
- const components = parseValue(valueAttr, withTime);
758
- // If components is null, initialize with empty state
759
- if (components === null) {
760
- this._state = {
761
- withTime,
762
- open: false,
763
- };
764
- }
765
- else {
766
- this._state = {
767
- ...components,
768
- withTime,
769
- open: false,
770
- };
771
- }
772
- // Convert to UTC and sync to form
773
- this.syncFormValue();
774
- }
775
- /**
776
- * Update internal state
777
- *
778
- * PORTED FROM: update-component-state! in date_picker.cljs
779
- */
780
- updateState(updates, forceSync = false) {
781
- this._state = { ...this._state, ...updates };
782
- // STAGING: Only sync attributes if dialog is closed OR force-sync is true
783
- // This prevents re-renders during time input editing
784
- const shouldSync = forceSync || !this._state.open || !this._state.withTime;
785
- if (shouldSync) {
786
- this.syncFormValue();
787
- }
788
- }
789
- /**
790
- * Sync form value with current state
791
- * Compares before setting to prevent circular triggers
792
- */
793
- syncFormValue() {
794
- const utcValue = this.getFormValue();
795
- const currentValue = this.getProperty('value');
796
- // Only update property if value actually changed (prevents circular triggers)
797
- if (utcValue !== currentValue) {
798
- // Use property setter to maintain TyComponent lifecycle
799
- // This will automatically handle attribute sync and form value update
800
- if (utcValue) {
801
- this.setProperty('value', utcValue);
802
- }
803
- else {
804
- this.setProperty('value', null);
805
- }
806
- }
807
- // Form value sync handled automatically by TyComponent (formValue: true)
808
- }
809
- // handleValueChange removed - logic moved to onPropertiesChanged hook
810
- // ==========================================================================
811
- // Time Input Handling
812
- // ==========================================================================
813
- /**
814
- * Handle time input changes from TimeInput class
815
- *
816
- * PORTED FROM: handle-time-change! in date_picker.cljs
817
- */
818
- handleTimeInputChange(hour, minute) {
819
- if (!this.hasDate())
820
- return;
821
- const components = { ...this.getComponents(), hour, minute };
822
- this.updateState(components, true);
823
- this.emitChangeEvent(components, 'time-change');
824
- }
825
- /**
826
- * Emit change event
827
- *
828
- * PORTED FROM: emit-change-event! in date_picker.cljs
829
- */
830
- emitChangeEvent(components, source) {
831
- const utcValue = components ? componentsToOutputValue(components) : null;
832
- const localValue = components ? componentsToLocalValue(components, this._state.withTime) : null;
833
- const milliseconds = components ? componentsToDateObject(components)?.getTime() ?? null : null;
834
- const formatted = components ? this.getFormattedValue(components) : null;
835
- const detail = {
836
- value: utcValue,
837
- localValue,
838
- milliseconds,
839
- formatted,
840
- source,
841
- };
842
- const event = new CustomEvent('change', {
843
- detail,
844
- bubbles: true,
845
- cancelable: true,
846
- });
847
- this.dispatchEvent(event);
848
- }
849
- // ==========================================================================
850
- // Public API (Properties) - Simple Accessors
851
- // ==========================================================================
852
- // String properties
853
- get size() { return this.getProperty('size'); }
854
- set size(v) { this.setProperty('size', v); }
855
- get flavor() { return this.getProperty('flavor'); }
856
- set flavor(v) { this.setProperty('flavor', v); }
857
- get label() { return this.getProperty('label'); }
858
- set label(v) { this.setProperty('label', v); }
859
- get placeholder() { return this.getProperty('placeholder'); }
860
- set placeholder(v) { this.setProperty('placeholder', v); }
861
- get name() { return this.getProperty('name'); }
862
- set name(v) { this.setProperty('name', v); }
863
- get format() { return this.getProperty('format'); }
864
- set format(v) { this.setProperty('format', v); }
865
- get locale() { return this.getProperty('locale'); }
866
- set locale(v) { this.setProperty('locale', v); }
867
- // Boolean properties
868
- get required() { return this.getProperty('required'); }
869
- set required(v) { this.setProperty('required', v); }
870
- get disabled() { return this.getProperty('disabled'); }
871
- set disabled(v) { this.setProperty('disabled', v); }
872
- get clearable() { return this.getProperty('clearable'); }
873
- set clearable(v) { this.setProperty('clearable', v); }
874
- get min() { return this.getProperty('min'); }
875
- set min(v) { this.setProperty('min', v ?? ''); }
876
- get max() { return this.getProperty('max'); }
877
- set max(v) { this.setProperty('max', v ?? ''); }
878
- // With-time property
879
- get withTime() { return this.getProperty('with-time'); }
880
- set withTime(v) { this.setProperty('with-time', v); }
881
- // Value property - Keep custom getter/setter for now (complex UTC parsing logic)
882
- /**
883
- * Get current value (UTC ISO string)
884
- */
885
- get value() {
886
- return this.getProperty('value') || null;
887
- }
888
- /**
889
- * Set value (UTC ISO string, Date object, or null)
890
- *
891
- * When set to null/undefined/empty string, the attribute is removed.
892
- * When set to a valid date, the attribute is set to ISO UTC string.
893
- */
894
- set value(val) {
895
- if (val === null || val === undefined || val === '') {
896
- this.setProperty('value', null); // TyComponent will remove attribute
897
- }
898
- else {
899
- const strValue = val instanceof Date ? val.toISOString() : val;
900
- this.setProperty('value', strValue);
901
- }
902
- }
903
- /**
904
- * Clear the date value programmatically WITH a change event (source:
905
- * 'clear'). Works regardless of the `clearable` attribute — lets any
906
- * external trigger (e.g. a button elsewhere in the page) clear the value
907
- * without needing the built-in clear button. Mirrors ty-select's clear().
908
- */
909
- clear() {
910
- this.clearValue();
911
- }
912
- // ==========================================================================
913
- // Rendering & DOM Management
914
- // ==========================================================================
915
- /**
916
- * Build CSS classes for the stub element
917
- *
918
- * PORTED FROM: build-stub-classes in date_picker.cljs
919
- */
920
- buildStubClasses() {
921
- const classes = ['date-picker-stub'];
922
- const size = this.getProperty('size') || 'md';
923
- classes.push(size);
924
- if (this.getProperty('disabled'))
925
- classes.push('disabled');
926
- if (this.getProperty('required'))
927
- classes.push('required');
928
- if (this._state.open)
929
- classes.push('open');
930
- return classes.join(' ');
931
- }
932
- /**
933
- * Render the date picker stub (input display)
934
- *
935
- * PORTED FROM: render-date-picker-stub in date_picker.cljs
936
- */
937
- renderStub() {
938
- const stub = document.createElement('div');
939
- stub.className = this.buildStubClasses();
940
- const isDisabled = this.getProperty('disabled');
941
- if (isDisabled) {
942
- stub.setAttribute('disabled', 'true');
943
- }
944
- // Not text-editable (click/Enter/Space just opens the calendar dialog),
945
- // so role="button" — not role="combobox", which implies typing. The
946
- // popup is a real <dialog> opened via showModal() (see openDropdown),
947
- // so it already gets role="dialog" + focus-trap from the browser for
948
- // free; only the trigger itself needs wiring.
949
- stub.setAttribute('role', 'button');
950
- stub.setAttribute('aria-haspopup', 'dialog');
951
- stub.setAttribute('aria-expanded', String(this._state.open));
952
- stub.setAttribute('tabindex', isDisabled ? '-1' : '0');
953
- if (isDisabled)
954
- stub.setAttribute('aria-disabled', 'true');
955
- if (this.getProperty('label')) {
956
- stub.setAttribute('aria-labelledby', `date-picker-label-${getElementHash(this)}`);
957
- }
958
- // Same gap as ty-select had: a plain div's Enter/Space don't turn into
959
- // click events on their own, so without this a keyboard-only user could
960
- // Tab to the field but never open it.
961
- stub.addEventListener('keydown', (e) => {
962
- const ke = e;
963
- if (this._state.open)
964
- return;
965
- if (ke.key === 'Enter' || ke.key === ' ') {
966
- ke.preventDefault();
967
- this.handleStubClick(e);
968
- }
969
- });
970
- // Start slot — leading icon (search, calendar variant, etc.)
971
- const startSlot = document.createElement('slot');
972
- startSlot.name = 'start';
973
- // Display text
974
- const displayText = document.createElement('span');
975
- displayText.className = 'stub-text';
976
- const formattedValue = this.getFormattedValue();
977
- const placeholder = this.getProperty('placeholder') || 'Select date...';
978
- displayText.textContent = formattedValue || placeholder;
979
- if (!formattedValue) {
980
- displayText.classList.add('placeholder');
981
- }
982
- // Icons container
983
- const iconContainer = document.createElement('div');
984
- iconContainer.className = 'stub-icons';
985
- // Clear button
986
- const clearable = this.getProperty('clearable');
987
- if (clearable && formattedValue && !isDisabled) {
988
- const clearButton = document.createElement('button');
989
- clearButton.className = 'stub-clear';
990
- clearButton.type = 'button';
991
- clearButton.innerHTML = CLEAR_ICON_SVG;
992
- clearButton.addEventListener('click', (e) => this.handleClearClick(e));
993
- iconContainer.appendChild(clearButton);
994
- }
995
- // Calendar icon
996
- const calendarIcon = document.createElement('span');
997
- calendarIcon.className = 'stub-arrow';
998
- calendarIcon.innerHTML = CALENDAR_ICON_SVG;
999
- iconContainer.appendChild(calendarIcon);
1000
- // Stub click handler
1001
- stub.addEventListener('click', (e) => this.handleStubClick(e));
1002
- stub.appendChild(startSlot);
1003
- stub.appendChild(displayText);
1004
- stub.appendChild(iconContainer);
1005
- return stub;
1006
- }
1007
- /**
1008
- * Create time input element
1009
- *
1010
- * PORTED FROM: create-time-input in date_picker.cljs
1011
- */
1012
- createTimeInputElement() {
1013
- const input = document.createElement('input');
1014
- input.type = 'text';
1015
- input.className = 'time-input';
1016
- input.placeholder = 'HH:mm';
1017
- input.autocomplete = 'off';
1018
- input.maxLength = 5;
1019
- return input;
1020
- }
1021
- /**
1022
- * Render time input section
1023
- */
1024
- renderTimeSection() {
1025
- const timeSection = document.createElement('div');
1026
- timeSection.className = 'time-section';
1027
- const timeLabel = document.createElement('label');
1028
- timeLabel.className = 'time-label';
1029
- timeLabel.textContent = 'Time:';
1030
- const timeInputElement = this.createTimeInputElement();
1031
- // Create TimeInput instance
1032
- const hour = this._state.hour || 0;
1033
- const minute = this._state.minute || 0;
1034
- this._timeInput = new TimeInput(timeInputElement, this, hour, minute);
1035
- const timeIcon = document.createElement('span');
1036
- timeIcon.className = 'time-icon';
1037
- timeIcon.innerHTML = SCHEDULE_ICON_SVG;
1038
- timeSection.appendChild(timeLabel);
1039
- timeSection.appendChild(timeInputElement);
1040
- timeSection.appendChild(timeIcon);
1041
- return timeSection;
1042
- }
1043
- /**
1044
- * Render calendar dropdown with dialog
1045
- *
1046
- * PORTED FROM: render-calendar-dropdown in date_picker.cljs
1047
- */
1048
- renderCalendarDropdown() {
1049
- const dialog = document.createElement('dialog');
1050
- dialog.className = 'calendar-dialog';
1051
- const contentWrapper = document.createElement('div');
1052
- contentWrapper.className = 'calendar-content';
1053
- // Create ty-calendar
1054
- const calendar = document.createElement('ty-calendar');
1055
- // Set current date if available
1056
- if (this._state.year && this._state.month && this._state.day) {
1057
- calendar.setAttribute('year', this._state.year.toString());
1058
- calendar.setAttribute('month', this._state.month.toString());
1059
- calendar.setAttribute('day', this._state.day.toString());
1060
- }
1061
- const locale = getEffectiveLocale(this, this.getProperty('locale'));
1062
- if (locale) {
1063
- calendar.setAttribute('locale', locale);
1064
- }
1065
- // Pass through date bounds
1066
- const min = this.getProperty('min');
1067
- const max = this.getProperty('max');
1068
- if (min)
1069
- calendar.setAttribute('min', min);
1070
- if (max)
1071
- calendar.setAttribute('max', max);
1072
- // Match the popup calendar to the stub's flavor. 'default' is the
1073
- // stub's own unstyled state, not a real flavor — leave the calendar on
1074
- // its own default rather than forwarding the literal string.
1075
- const flavor = this.getProperty('flavor');
1076
- if (flavor && flavor !== 'default')
1077
- calendar.setAttribute('flavor', flavor);
1078
- // Add calendar change handler
1079
- calendar.addEventListener('change', (e) => this.handleCalendarChange(e));
1080
- contentWrapper.appendChild(calendar);
1081
- // Add time section if with-time enabled
1082
- if (this._state.withTime) {
1083
- contentWrapper.appendChild(this.renderTimeSection());
1084
- }
1085
- dialog.appendChild(contentWrapper);
1086
- // Dialog close handler
1087
- dialog.addEventListener('close', () => {
1088
- this.updateState({ open: false });
1089
- });
1090
- return dialog;
1091
- }
1092
- /**
1093
- * Render native date input for mobile touch devices.
1094
- * Uses <input type="date"> or <input type="datetime-local"> when with-time.
1095
- * Reuses .date-picker-stub styling and existing event/state infrastructure.
1096
- */
1097
- renderNativeInput() {
1098
- const wrapper = document.createElement('div');
1099
- wrapper.className = 'dropdown-wrapper';
1100
- const stub = document.createElement('div');
1101
- stub.className = this.buildStubClasses();
1102
- const isDisabled = this.getProperty('disabled');
1103
- if (isDisabled) {
1104
- stub.setAttribute('disabled', 'true');
1105
- }
1106
- // Native input (hidden, activated via picker indicator)
1107
- const input = document.createElement('input');
1108
- input.className = 'native-date-input';
1109
- input.type = this._state.withTime ? 'datetime-local' : 'date';
1110
- if (isDisabled)
1111
- input.disabled = true;
1112
- if (this.getProperty('required'))
1113
- input.required = true;
1114
- // Native min/max (datetime-local wants a time suffix)
1115
- const minBound = this.getProperty('min');
1116
- const maxBound = this.getProperty('max');
1117
- if (minBound)
1118
- input.min = this._state.withTime ? `${minBound}T00:00` : minBound;
1119
- if (maxBound)
1120
- input.max = this._state.withTime ? `${maxBound}T23:59` : maxBound;
1121
- // Pre-fill value
1122
- const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
1123
- if (localValue) {
1124
- input.value = localValue;
1125
- }
1126
- // Placeholder span (native date inputs ignore placeholder attr)
1127
- const placeholder = this.getProperty('placeholder') || 'Select date...';
1128
- const formattedValue = this.getFormattedValue();
1129
- const placeholderEl = document.createElement('span');
1130
- placeholderEl.className = 'stub-text' + (formattedValue ? '' : ' placeholder');
1131
- placeholderEl.textContent = formattedValue || placeholder;
1132
- // Change listener
1133
- input.addEventListener('change', () => {
1134
- if (!input.value) {
1135
- this.clearValue();
1136
- return;
1137
- }
1138
- const parsed = parseValue(input.value, this._state.withTime);
1139
- if (parsed) {
1140
- this.updateState(parsed, true);
1141
- this.emitChangeEvent(parsed, 'selection');
1142
- }
1143
- });
1144
- // Start slot — leading icon
1145
- const startSlot = document.createElement('slot');
1146
- startSlot.name = 'start';
1147
- stub.appendChild(startSlot);
1148
- stub.appendChild(input);
1149
- stub.appendChild(placeholderEl);
1150
- // Icons container (clear + calendar icon)
1151
- const iconContainer = document.createElement('div');
1152
- iconContainer.className = 'stub-icons';
1153
- const clearable = this.getProperty('clearable');
1154
- if (clearable && localValue && !isDisabled) {
1155
- const clearButton = document.createElement('button');
1156
- clearButton.className = 'stub-clear';
1157
- clearButton.type = 'button';
1158
- clearButton.innerHTML = CLEAR_ICON_SVG;
1159
- clearButton.addEventListener('click', (e) => this.handleClearClick(e));
1160
- iconContainer.appendChild(clearButton);
1161
- }
1162
- const calendarIcon = document.createElement('span');
1163
- calendarIcon.className = 'stub-arrow';
1164
- calendarIcon.innerHTML = CALENDAR_ICON_SVG;
1165
- iconContainer.appendChild(calendarIcon);
1166
- stub.appendChild(iconContainer);
1167
- wrapper.appendChild(stub);
1168
- return wrapper;
1169
- }
1170
- /**
1171
- * Calculate calendar position
1172
- *
1173
- * PORTED FROM: calculate-calendar-position! in date_picker.cljs
1174
- */
1175
- calculateCalendarPosition() {
1176
- if (!this.shadowRoot)
1177
- return;
1178
- // Mobile uses native input, no positioning needed
1179
- if (isMobileTouch())
1180
- return;
1181
- const stub = this.shadowRoot.querySelector('.date-picker-stub');
1182
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1183
- if (!stub || !dialog)
1184
- return;
1185
- const stubRect = stub.getBoundingClientRect();
1186
- // Measure when shown; the estimate only covers the pre-layout call
1187
- const dialogRect = dialog.getBoundingClientRect();
1188
- const estimatedHeight = dialogRect.height || 400;
1189
- const popupWidth = dialogRect.width || 320;
1190
- const wrapPadding = 8;
1191
- const pos = computeAnchoredPosition({
1192
- anchorRect: stubRect,
1193
- popupWidth,
1194
- popupHeight: estimatedHeight,
1195
- gap: 0,
1196
- });
1197
- const positionBelow = pos.below;
1198
- const x = pos.x - wrapPadding;
1199
- const y = positionBelow ? pos.topY : pos.bottomY;
1200
- // Set CSS variables for positioning
1201
- this.style.setProperty('--calendar-x', `${x}px`);
1202
- this.style.setProperty('--calendar-y', `${y}px`);
1203
- this.style.setProperty('--calendar-offset-x', '0px');
1204
- this.style.setProperty('--calendar-offset-y', '0px');
1205
- // Set direction classes
1206
- if (positionBelow) {
1207
- dialog.classList.add('position-below');
1208
- dialog.classList.remove('position-above');
1209
- }
1210
- else {
1211
- dialog.classList.add('position-above');
1212
- dialog.classList.remove('position-below');
1213
- }
1214
- }
1215
- /**
1216
- * Render container structure
1217
- *
1218
- * PORTED FROM: render-container-structure in date_picker.cljs
1219
- */
1220
- renderContainer() {
1221
- const container = document.createElement('div');
1222
- container.className = 'dropdown-container';
1223
- const label = this.getProperty('label');
1224
- if (label) {
1225
- const labelEl = document.createElement('label');
1226
- labelEl.className = 'ty-field-label';
1227
- labelEl.id = `date-picker-label-${getElementHash(this)}`;
1228
- labelEl.innerHTML = label + (this.getProperty('required') ? '<span class="required-icon">*</span>' : '');
1229
- container.appendChild(labelEl);
1230
- }
1231
- return container;
1232
- }
1233
- /**
1234
- * Main render method
1235
- *
1236
- * PORTED FROM: render! in date_picker.cljs
1237
- */
1238
- render() {
1239
- if (!this.shadowRoot)
1240
- return;
1241
- ensureStyles(this.shadowRoot, { css: datePickerStyles, id: 'ty-date-picker' });
1242
- // Mobile: native input, no dialog/event listeners/scroll lock
1243
- if (isMobileTouch()) {
1244
- const existingNativeInput = this.shadowRoot.querySelector('.native-date-input');
1245
- if (existingNativeInput) {
1246
- // PARTIAL UPDATE: don't destroy DOM while native picker may be open
1247
- this.updateDisplay();
1248
- return;
1249
- }
1250
- // FIRST RENDER: build from scratch
1251
- this.shadowRoot.innerHTML = '';
1252
- const container = this.renderContainer();
1253
- container.appendChild(this.renderNativeInput());
1254
- this.shadowRoot.appendChild(container);
1255
- return;
1256
- }
1257
- // Check if dialog is currently open
1258
- const existingDialog = this.shadowRoot.querySelector('.calendar-dialog');
1259
- const isDialogOpen = existingDialog && existingDialog.open;
1260
- if (isDialogOpen && this._state.open) {
1261
- // PARTIAL UPDATE: Dialog is open - just update display
1262
- this.updateDisplay();
1263
- this.calculateCalendarPosition();
1264
- return;
1265
- }
1266
- // FULL REBUILD: Dialog is closed or doesn't exist
1267
- this.shadowRoot.innerHTML = '';
1268
- const container = this.renderContainer();
1269
- const wrapper = document.createElement('div');
1270
- wrapper.className = 'dropdown-wrapper';
1271
- wrapper.appendChild(this.renderStub());
1272
- wrapper.appendChild(this.renderCalendarDropdown());
1273
- container.appendChild(wrapper);
1274
- this.shadowRoot.appendChild(container);
1275
- // Setup document-level event listeners
1276
- this.setupEventListeners();
1277
- }
1278
- /**
1279
- * Update display without destroying DOM (for open dialog)
1280
- *
1281
- * PORTED FROM: update-display! in date_picker.cljs
1282
- */
1283
- updateDisplay() {
1284
- if (!this.shadowRoot)
1285
- return;
1286
- const formattedValue = this.getFormattedValue();
1287
- const placeholder = this.getProperty('placeholder') || 'Select date...';
1288
- const hasValue = this.hasDate();
1289
- const clearable = this.getProperty('clearable');
1290
- const isDisabled = this.getProperty('disabled');
1291
- // Update stub text (shared between mobile and desktop)
1292
- const stubText = this.shadowRoot.querySelector('.stub-text');
1293
- if (stubText) {
1294
- stubText.textContent = formattedValue || placeholder;
1295
- stubText.classList.toggle('placeholder', !formattedValue);
1296
- }
1297
- // Update clear button visibility
1298
- const clearButton = this.shadowRoot.querySelector('.stub-clear');
1299
- if (clearButton) {
1300
- clearButton.style.display = (clearable && hasValue && !isDisabled) ? '' : 'none';
1301
- }
1302
- // Mobile: also sync native input value
1303
- if (isMobileTouch()) {
1304
- const nativeInput = this.shadowRoot.querySelector('.native-date-input');
1305
- if (nativeInput) {
1306
- const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
1307
- nativeInput.value = localValue || '';
1308
- }
1309
- return;
1310
- }
1311
- // Desktop: update calendar and time input
1312
- const calendar = this.shadowRoot.querySelector('ty-calendar');
1313
- if (calendar && hasValue) {
1314
- calendar.setAttribute('year', this._state.year.toString());
1315
- calendar.setAttribute('month', this._state.month.toString());
1316
- calendar.setAttribute('day', this._state.day.toString());
1317
- }
1318
- // Keep an already-open popup's flavor in sync. render() only rebuilds
1319
- // (and re-forwards flavor) while the dialog is CLOSED — while open it
1320
- // takes this partial-update path instead, so without this the calendar
1321
- // would stay stuck on whatever flavor was active when it was opened.
1322
- if (calendar) {
1323
- const flavor = this.getProperty('flavor');
1324
- if (flavor && flavor !== 'default')
1325
- calendar.setAttribute('flavor', flavor);
1326
- else
1327
- calendar.removeAttribute('flavor');
1328
- }
1329
- if (this._timeInput && this._state.hour !== undefined && this._state.minute !== undefined) {
1330
- this._timeInput.setTime(this._state.hour, this._state.minute);
1331
- }
1332
- }
1333
- /**
1334
- * Setup document-level event listeners
1335
- */
1336
- setupEventListeners() {
1337
- // Remove old listeners if they exist
1338
- if (this._clickListener) {
1339
- document.removeEventListener('click', this._clickListener);
1340
- }
1341
- if (this._keydownListener) {
1342
- document.removeEventListener('keydown', this._keydownListener);
1343
- }
1344
- // Create new listeners
1345
- this._clickListener = (e) => this.handleOutsideClick(e);
1346
- this._keydownListener = (e) => this.handleEscapeKey(e);
1347
- // Add document listeners
1348
- document.addEventListener('click', this._clickListener);
1349
- document.addEventListener('keydown', this._keydownListener);
1350
- // Add dialog click listener
1351
- const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
1352
- if (dialog) {
1353
- this._dialogClickListener = (e) => this.handleDialogClick(e);
1354
- dialog.addEventListener('click', this._dialogClickListener);
1355
- }
1356
- }
1357
- // ==========================================================================
1358
- // Event Handlers
1359
- // ==========================================================================
1360
- handleStubClick(event) {
1361
- event.preventDefault();
1362
- // Heal state/DOM desync: a host re-render can replace the dialog element
1363
- // while `open` is still true, stranding the picker (openDropdown
1364
- // early-returns on _state.open). Reality wins: closed dialog → closed state.
1365
- const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
1366
- if (this._state.open && (!dialog || !dialog.open)) {
1367
- this.updateState({ open: false }, true);
1368
- unlockScroll(`date-picker-${this.id || this.toString()}`);
1369
- }
1370
- const disabled = this.getProperty('disabled');
1371
- // FIXME: Timestamp guard is a workaround. Calendar day cells use pointerdown,
1372
- // which closes the dialog before the click event fires — the click then hits the
1373
- // stub and reopens. Proper fix: switch calendar-month day cells from pointerdown
1374
- // to click so the event is consumed before the dialog closes.
1375
- if (!disabled && Date.now() - this._closeTimestamp > 300) {
1376
- this.openDropdown();
1377
- }
1378
- }
1379
- /**
1380
- * Clear the date value, sync state, emit event, and re-render.
1381
- */
1382
- clearValue() {
1383
- this.updateState({
1384
- year: undefined,
1385
- month: undefined,
1386
- day: undefined,
1387
- hour: undefined,
1388
- minute: undefined,
1389
- }, true);
1390
- this.value = null;
1391
- this.emitChangeEvent(null, 'clear');
1392
- this.render();
1393
- }
1394
- handleClearClick(event) {
1395
- event.preventDefault();
1396
- event.stopPropagation();
1397
- this.clearValue();
1398
- }
1399
- /**
1400
- * Handle calendar date selection
1401
- *
1402
- * PORTED FROM: handle-calendar-change! in date_picker.cljs
1403
- */
1404
- handleCalendarChange(event) {
1405
- event.preventDefault();
1406
- event.stopPropagation();
1407
- const customEvent = event;
1408
- const detail = customEvent.detail;
1409
- const dayContext = detail.dayContext;
1410
- if (!dayContext)
1411
- return;
1412
- // Update state with new date
1413
- const newComponents = {
1414
- year: dayContext.year,
1415
- month: dayContext.month,
1416
- day: dayContext.dayInMonth,
1417
- hour: this._state.hour,
1418
- minute: this._state.minute,
1419
- };
1420
- // Force sync when calendar date changes
1421
- this.updateState(newComponents, true);
1422
- this.emitChangeEvent(newComponents, 'selection');
1423
- // Close calendar if time is not required
1424
- if (!this._state.withTime) {
1425
- this.closeDropdown();
1426
- }
1427
- else {
1428
- // Auto-focus time input after date selection
1429
- requestAnimationFrame(() => {
1430
- if (!this.shadowRoot)
1431
- return;
1432
- const timeInput = this.shadowRoot.querySelector('.time-input');
1433
- if (timeInput) {
1434
- timeInput.focus();
1435
- }
1436
- });
1437
- }
1438
- }
1439
- /**
1440
- * Handle dialog backdrop clicks
1441
- *
1442
- * PORTED FROM: handle-dialog-click! in date_picker.cljs
1443
- */
1444
- handleDialogClick(event) {
1445
- if (!this.shadowRoot)
1446
- return;
1447
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1448
- const content = this.shadowRoot.querySelector('.calendar-content');
1449
- // Close if clicking on dialog backdrop (not calendar content)
1450
- if (event.target === dialog && this._state.open && content && !content.contains(event.target)) {
1451
- event.preventDefault();
1452
- event.stopPropagation();
1453
- this.closeDropdown();
1454
- }
1455
- }
1456
- /**
1457
- * Handle clicks outside the date picker
1458
- *
1459
- * PORTED FROM: handle-outside-click! in date_picker.cljs
1460
- */
1461
- handleOutsideClick(event) {
1462
- if (!this.shadowRoot)
1463
- return;
1464
- const target = event.target;
1465
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1466
- // Check if click is truly outside everything
1467
- if (this._state.open && dialog && !this.contains(target) && !dialog.contains(target)) {
1468
- this.closeDropdown();
1469
- }
1470
- }
1471
- /**
1472
- * Handle Escape key press
1473
- *
1474
- * PORTED FROM: handle-escape-key! in date_picker.cljs
1475
- */
1476
- handleEscapeKey(event) {
1477
- const keyboardEvent = event;
1478
- if (keyboardEvent.key === 'Escape' && this._state.open) {
1479
- keyboardEvent.preventDefault();
1480
- this.closeDropdown();
1481
- }
1482
- }
1483
- /**
1484
- * Open calendar dropdown
1485
- *
1486
- * PORTED FROM: open-dropdown! in date_picker.cljs
1487
- */
1488
- openDropdown() {
1489
- if (this._state.open)
1490
- return;
1491
- if (isMobileTouch())
1492
- return;
1493
- this.updateState({ open: true });
1494
- // Dispatch open event
1495
- this.dispatchEvent(new CustomEvent('open', {
1496
- bubbles: true,
1497
- composed: true
1498
- }));
1499
- this.render();
1500
- requestAnimationFrame(() => {
1501
- if (!this.shadowRoot)
1502
- return;
1503
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1504
- if (dialog) {
1505
- const pickerId = `date-picker-${this.id || this.toString()}`;
1506
- lockScroll(pickerId);
1507
- dialog.showModal();
1508
- this.calculateCalendarPosition();
1509
- dialog.classList.add('open');
1510
- const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
1511
- if (stubEl)
1512
- stubEl.setAttribute('aria-expanded', 'true');
1513
- // Remove focus from any focused elements to prevent the blue outline
1514
- const focusedElement = this.shadowRoot.activeElement;
1515
- if (focusedElement) {
1516
- focusedElement.blur();
1517
- }
1518
- }
1519
- });
1520
- }
1521
- /**
1522
- * Close calendar dropdown
1523
- *
1524
- * PORTED FROM: close-dropdown! in date_picker.cljs
1525
- */
1526
- closeDropdown() {
1527
- if (!this._state.open)
1528
- return;
1529
- this._closeTimestamp = Date.now();
1530
- const pickerId = `date-picker-${this.id || this.toString()}`;
1531
- // Force sync any staged updates when closing
1532
- this.updateState({ open: false }, true);
1533
- unlockScroll(pickerId);
1534
- if (this.shadowRoot) {
1535
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1536
- if (dialog) {
1537
- dialog.classList.remove('position-above', 'position-below', 'open');
1538
- dialog.close();
1539
- }
1540
- // closeDropdown() doesn't call render(), so the stub's aria-expanded
1541
- // (still "true" from open) needs an explicit flip here too.
1542
- const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
1543
- if (stubEl)
1544
- stubEl.setAttribute('aria-expanded', 'false');
1545
- }
1546
- this.dispatchEvent(new CustomEvent('close', {
1547
- bubbles: true,
1548
- composed: true
1549
- }));
1550
- this.render();
1551
- }
1552
- // ==========================================================================
1553
- // Cleanup
1554
- // ==========================================================================
1555
- /**
1556
- * Clean up event listeners and state
1557
- *
1558
- * PORTED FROM: cleanup! in date_picker.cljs
1559
- */
1560
- cleanup() {
1561
- // Remove document listeners
1562
- if (this._clickListener) {
1563
- document.removeEventListener('click', this._clickListener);
1564
- }
1565
- if (this._keydownListener) {
1566
- document.removeEventListener('keydown', this._keydownListener);
1567
- }
1568
- // Remove dialog listener
1569
- if (this._dialogClickListener && this.shadowRoot) {
1570
- const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1571
- if (dialog) {
1572
- dialog.removeEventListener('click', this._dialogClickListener);
1573
- }
1574
- }
1575
- // Unlock scroll if open
1576
- if (this._state.open) {
1577
- const pickerId = `date-picker-${this.id || this.toString()}`;
1578
- unlockScroll(pickerId);
1579
- }
1580
- // Clear references
1581
- this._clickListener = undefined;
1582
- this._keydownListener = undefined;
1583
- this._dialogClickListener = undefined;
1584
- this._timeInput = undefined;
1585
- }
1586
- }
1587
- // ============================================================================
1588
- // PROPERTY CONFIGURATION - Single source of truth
1589
- // ============================================================================
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
+ this.syncFormValue();
385
+ break;
386
+ }
387
+ const currentComponents = {
388
+ year: this._state.year,
389
+ month: this._state.month,
390
+ day: this._state.day,
391
+ hour: this._state.hour,
392
+ minute: this._state.minute,
393
+ };
394
+ const changed = newComponents?.year !== currentComponents.year ||
395
+ newComponents?.month !== currentComponents.month ||
396
+ newComponents?.day !== currentComponents.day ||
397
+ newComponents?.hour !== currentComponents.hour ||
398
+ newComponents?.minute !== currentComponents.minute;
399
+ if (changed) {
400
+ this._state = {
401
+ ...this._state,
402
+ ...newComponents,
403
+ };
404
+ this.syncFormValue();
405
+ }
406
+ break;
407
+ }
408
+ case 'with-time': {
409
+ const newWithTime = newValue;
410
+ const oldWithTime = this._state.withTime;
411
+ if (newWithTime !== oldWithTime) {
412
+ this._state.withTime = newWithTime;
413
+ const hasDate = this._state.year !== undefined &&
414
+ this._state.month !== undefined &&
415
+ this._state.day !== undefined;
416
+ if (hasDate) {
417
+ this.syncFormValue();
418
+ }
419
+ }
420
+ break;
421
+ }
422
+ }
423
+ }
424
+ }
425
+ getFormValue() {
426
+ return componentsToOutputValue(this.getComponents());
427
+ }
428
+ getComponents() {
429
+ return {
430
+ year: this._state.year,
431
+ month: this._state.month,
432
+ day: this._state.day,
433
+ hour: this._state.hour,
434
+ minute: this._state.minute,
435
+ };
436
+ }
437
+ hasDate() {
438
+ return !!(this._state.year && this._state.month && this._state.day);
439
+ }
440
+ getFormattedValue(components) {
441
+ const c = components || this.getComponents();
442
+ if (!c.year || !c.month || !c.day)
443
+ return null;
444
+ return formatDisplayValue(c, this.getProperty('format') || 'long', getEffectiveLocale(this, this.getProperty('locale')), this._state.withTime);
445
+ }
446
+ initializeState() {
447
+ const valueAttr = this.getProperty('value');
448
+ const withTime = this.getProperty('with-time');
449
+ const components = parseValue(valueAttr, withTime);
450
+ if (components === null) {
451
+ this._state = {
452
+ withTime,
453
+ open: false,
454
+ };
455
+ }
456
+ else {
457
+ this._state = {
458
+ ...components,
459
+ withTime,
460
+ open: false,
461
+ };
462
+ }
463
+ this.syncFormValue();
464
+ }
465
+ updateState(updates, forceSync = false) {
466
+ this._state = { ...this._state, ...updates };
467
+ const shouldSync = forceSync || !this._state.open || !this._state.withTime;
468
+ if (shouldSync) {
469
+ this.syncFormValue();
470
+ }
471
+ }
472
+ syncFormValue() {
473
+ const utcValue = this.getFormValue();
474
+ const currentValue = this.getProperty('value');
475
+ if (utcValue !== currentValue) {
476
+ if (utcValue) {
477
+ this.setProperty('value', utcValue);
478
+ }
479
+ else {
480
+ this.setProperty('value', null);
481
+ }
482
+ }
483
+ }
484
+ updateValidity() {
485
+ if (this.disabled) {
486
+ this._internals.setValidity({});
487
+ return;
488
+ }
489
+ if (this.required && !this.value) {
490
+ this._internals.setValidity({ valueMissing: true }, 'Please fill out this field');
491
+ }
492
+ else {
493
+ this._internals.setValidity({});
494
+ }
495
+ }
496
+ handleTimeInputChange(hour, minute) {
497
+ if (!this.hasDate())
498
+ return;
499
+ const components = { ...this.getComponents(), hour, minute };
500
+ this.updateState(components, true);
501
+ this.emitChangeEvent(components, 'time-change');
502
+ }
503
+ emitChangeEvent(components, source) {
504
+ const utcValue = components ? componentsToOutputValue(components) : null;
505
+ const localValue = components ? componentsToLocalValue(components, this._state.withTime) : null;
506
+ const milliseconds = components ? componentsToDateObject(components)?.getTime() ?? null : null;
507
+ const formatted = components ? this.getFormattedValue(components) : null;
508
+ const detail = {
509
+ value: utcValue,
510
+ localValue,
511
+ milliseconds,
512
+ formatted,
513
+ source,
514
+ };
515
+ const event = new CustomEvent('change', {
516
+ detail,
517
+ bubbles: true,
518
+ cancelable: true,
519
+ });
520
+ this.dispatchEvent(event);
521
+ }
522
+ get size() { return this.getProperty('size'); }
523
+ set size(v) { this.setProperty('size', v); }
524
+ get placement() { return this.getProperty('placement'); }
525
+ set placement(v) { this.setProperty('placement', v); }
526
+ get flavor() { return this.getProperty('flavor'); }
527
+ set flavor(v) { this.setProperty('flavor', v); }
528
+ get label() { return this.getProperty('label'); }
529
+ set label(v) { this.setProperty('label', v); }
530
+ get placeholder() { return this.getProperty('placeholder'); }
531
+ set placeholder(v) { this.setProperty('placeholder', v); }
532
+ get name() { return this.getProperty('name'); }
533
+ set name(v) { this.setProperty('name', v); }
534
+ get format() { return this.getProperty('format'); }
535
+ set format(v) { this.setProperty('format', v); }
536
+ get locale() { return this.getProperty('locale'); }
537
+ set locale(v) { this.setProperty('locale', v); }
538
+ get required() { return this.getProperty('required'); }
539
+ set required(v) { this.setProperty('required', v); }
540
+ get disabled() { return this.getProperty('disabled'); }
541
+ set disabled(v) { this.setProperty('disabled', v); }
542
+ get clearable() { return this.getProperty('clearable'); }
543
+ set clearable(v) { this.setProperty('clearable', v); }
544
+ get min() { return this.getProperty('min'); }
545
+ set min(v) { this.setProperty('min', v ?? ''); }
546
+ get max() { return this.getProperty('max'); }
547
+ set max(v) { this.setProperty('max', v ?? ''); }
548
+ get withTime() { return this.getProperty('with-time'); }
549
+ set withTime(v) { this.setProperty('with-time', v); }
550
+ get value() {
551
+ return this.getProperty('value') || null;
552
+ }
553
+ set value(val) {
554
+ if (val === null || val === undefined || val === '') {
555
+ this.setProperty('value', null);
556
+ }
557
+ else {
558
+ const strValue = val instanceof Date ? val.toISOString() : val;
559
+ this.setProperty('value', strValue);
560
+ }
561
+ }
562
+ clear() {
563
+ this.clearValue();
564
+ }
565
+ buildStubClasses() {
566
+ const classes = ['date-picker-stub'];
567
+ if (this.getProperty('disabled'))
568
+ classes.push('disabled');
569
+ if (this.getProperty('required'))
570
+ classes.push('required');
571
+ if (this._state.open)
572
+ classes.push('open');
573
+ return classes.join(' ');
574
+ }
575
+ renderStub() {
576
+ const stub = document.createElement('div');
577
+ stub.className = this.buildStubClasses();
578
+ const isDisabled = this.getProperty('disabled');
579
+ if (isDisabled) {
580
+ stub.setAttribute('disabled', 'true');
581
+ }
582
+ stub.setAttribute('role', 'button');
583
+ stub.setAttribute('aria-haspopup', 'dialog');
584
+ stub.setAttribute('aria-expanded', String(this._state.open));
585
+ stub.setAttribute('tabindex', isDisabled ? '-1' : '0');
586
+ if (isDisabled)
587
+ stub.setAttribute('aria-disabled', 'true');
588
+ if (this.getProperty('label')) {
589
+ stub.setAttribute('aria-labelledby', `date-picker-label-${getElementHash(this)}`);
590
+ }
591
+ stub.addEventListener('keydown', (e) => {
592
+ const ke = e;
593
+ if (this._state.open)
594
+ return;
595
+ if (ke.key === 'Enter' || ke.key === ' ') {
596
+ ke.preventDefault();
597
+ this.handleStubClick(e);
598
+ }
599
+ });
600
+ const startSlot = document.createElement('slot');
601
+ startSlot.name = 'start';
602
+ const displayText = document.createElement('span');
603
+ displayText.className = 'stub-text';
604
+ const formattedValue = this.getFormattedValue();
605
+ const placeholder = this.getProperty('placeholder') || 'Select date...';
606
+ displayText.textContent = formattedValue || placeholder;
607
+ if (!formattedValue) {
608
+ displayText.classList.add('placeholder');
609
+ }
610
+ const iconContainer = document.createElement('div');
611
+ iconContainer.className = 'stub-icons';
612
+ const clearable = this.getProperty('clearable');
613
+ if (clearable && formattedValue && !isDisabled) {
614
+ const clearButton = document.createElement('button');
615
+ clearButton.className = 'stub-clear';
616
+ clearButton.type = 'button';
617
+ clearButton.innerHTML = CLEAR_ICON_SVG;
618
+ clearButton.addEventListener('click', (e) => this.handleClearClick(e));
619
+ iconContainer.appendChild(clearButton);
620
+ }
621
+ const calendarIcon = document.createElement('span');
622
+ calendarIcon.className = 'stub-arrow';
623
+ calendarIcon.innerHTML = CALENDAR_ICON_SVG;
624
+ iconContainer.appendChild(calendarIcon);
625
+ stub.addEventListener('click', (e) => this.handleStubClick(e));
626
+ stub.appendChild(startSlot);
627
+ stub.appendChild(displayText);
628
+ stub.appendChild(iconContainer);
629
+ return stub;
630
+ }
631
+ createTimeInputElement() {
632
+ const input = document.createElement('input');
633
+ input.type = 'text';
634
+ input.className = 'time-input';
635
+ input.placeholder = 'HH:mm';
636
+ input.autocomplete = 'off';
637
+ input.maxLength = 5;
638
+ return input;
639
+ }
640
+ renderTimeSection() {
641
+ const timeSection = document.createElement('div');
642
+ timeSection.className = 'time-section';
643
+ const timeLabel = document.createElement('label');
644
+ timeLabel.className = 'time-label';
645
+ timeLabel.textContent = 'Time:';
646
+ const timeInputElement = this.createTimeInputElement();
647
+ const hour = this._state.hour || 0;
648
+ const minute = this._state.minute || 0;
649
+ this._timeInput = new TimeInput(timeInputElement, this, hour, minute);
650
+ const timeIcon = document.createElement('span');
651
+ timeIcon.className = 'time-icon';
652
+ timeIcon.innerHTML = SCHEDULE_ICON_SVG;
653
+ timeSection.appendChild(timeLabel);
654
+ timeSection.appendChild(timeInputElement);
655
+ timeSection.appendChild(timeIcon);
656
+ return timeSection;
657
+ }
658
+ renderCalendarDropdown() {
659
+ const dialog = document.createElement('dialog');
660
+ dialog.className = 'calendar-dialog';
661
+ const contentWrapper = document.createElement('div');
662
+ contentWrapper.className = 'calendar-content';
663
+ const calendar = document.createElement('ty-calendar');
664
+ if (this._state.year && this._state.month && this._state.day) {
665
+ calendar.setAttribute('year', this._state.year.toString());
666
+ calendar.setAttribute('month', this._state.month.toString());
667
+ calendar.setAttribute('day', this._state.day.toString());
668
+ }
669
+ const locale = getEffectiveLocale(this, this.getProperty('locale'));
670
+ if (locale) {
671
+ calendar.setAttribute('locale', locale);
672
+ }
673
+ const min = this.getProperty('min');
674
+ const max = this.getProperty('max');
675
+ if (min)
676
+ calendar.setAttribute('min', min);
677
+ if (max)
678
+ calendar.setAttribute('max', max);
679
+ const flavor = this.getProperty('flavor');
680
+ if (flavor && flavor !== 'default')
681
+ calendar.setAttribute('flavor', flavor);
682
+ calendar.addEventListener('change', (e) => this.handleCalendarChange(e));
683
+ contentWrapper.appendChild(calendar);
684
+ if (this._state.withTime) {
685
+ contentWrapper.appendChild(this.renderTimeSection());
686
+ }
687
+ dialog.appendChild(contentWrapper);
688
+ dialog.addEventListener('close', () => {
689
+ this.updateState({ open: false });
690
+ });
691
+ return dialog;
692
+ }
693
+ renderNativeInput() {
694
+ const wrapper = document.createElement('div');
695
+ wrapper.className = 'dropdown-wrapper';
696
+ const stub = document.createElement('div');
697
+ stub.className = this.buildStubClasses();
698
+ const isDisabled = this.getProperty('disabled');
699
+ if (isDisabled) {
700
+ stub.setAttribute('disabled', 'true');
701
+ }
702
+ const input = document.createElement('input');
703
+ input.className = 'native-date-input';
704
+ input.type = this._state.withTime ? 'datetime-local' : 'date';
705
+ if (isDisabled)
706
+ input.disabled = true;
707
+ if (this.getProperty('required'))
708
+ input.required = true;
709
+ const minBound = this.getProperty('min');
710
+ const maxBound = this.getProperty('max');
711
+ if (minBound)
712
+ input.min = this._state.withTime ? `${minBound}T00:00` : minBound;
713
+ if (maxBound)
714
+ input.max = this._state.withTime ? `${maxBound}T23:59` : maxBound;
715
+ const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
716
+ if (localValue) {
717
+ input.value = localValue;
718
+ }
719
+ const placeholder = this.getProperty('placeholder') || 'Select date...';
720
+ const formattedValue = this.getFormattedValue();
721
+ const placeholderEl = document.createElement('span');
722
+ placeholderEl.className = 'stub-text' + (formattedValue ? '' : ' placeholder');
723
+ placeholderEl.textContent = formattedValue || placeholder;
724
+ input.addEventListener('change', () => {
725
+ if (!input.value) {
726
+ this.clearValue();
727
+ return;
728
+ }
729
+ const parsed = parseValue(input.value, this._state.withTime);
730
+ if (parsed) {
731
+ this.updateState(parsed, true);
732
+ this.emitChangeEvent(parsed, 'selection');
733
+ }
734
+ });
735
+ const startSlot = document.createElement('slot');
736
+ startSlot.name = 'start';
737
+ stub.appendChild(startSlot);
738
+ stub.appendChild(input);
739
+ stub.appendChild(placeholderEl);
740
+ const iconContainer = document.createElement('div');
741
+ iconContainer.className = 'stub-icons';
742
+ const clearable = this.getProperty('clearable');
743
+ if (clearable && localValue && !isDisabled) {
744
+ const clearButton = document.createElement('button');
745
+ clearButton.className = 'stub-clear';
746
+ clearButton.type = 'button';
747
+ clearButton.innerHTML = CLEAR_ICON_SVG;
748
+ clearButton.addEventListener('click', (e) => this.handleClearClick(e));
749
+ iconContainer.appendChild(clearButton);
750
+ }
751
+ const calendarIcon = document.createElement('span');
752
+ calendarIcon.className = 'stub-arrow';
753
+ calendarIcon.innerHTML = CALENDAR_ICON_SVG;
754
+ iconContainer.appendChild(calendarIcon);
755
+ stub.appendChild(iconContainer);
756
+ wrapper.appendChild(stub);
757
+ return wrapper;
758
+ }
759
+ calculateCalendarPosition() {
760
+ if (!this.shadowRoot)
761
+ return;
762
+ if (isMobileTouch())
763
+ return;
764
+ const stub = this.shadowRoot.querySelector('.date-picker-stub');
765
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
766
+ if (!stub || !dialog)
767
+ return;
768
+ const stubRect = stub.getBoundingClientRect();
769
+ const dialogRect = dialog.getBoundingClientRect();
770
+ const estimatedHeight = dialogRect.height || 400;
771
+ const popupWidth = dialogRect.width || 320;
772
+ const wrapPadding = 8;
773
+ const anchored = placementToAnchored(this.getProperty('placement'));
774
+ const pos = computeAnchoredPosition({
775
+ anchorRect: stubRect,
776
+ popupWidth,
777
+ popupHeight: estimatedHeight,
778
+ gap: 0,
779
+ align: anchored.align,
780
+ side: anchored.side,
781
+ });
782
+ const positionBelow = pos.below;
783
+ const x = pos.x - wrapPadding;
784
+ const y = positionBelow ? pos.topY : pos.bottomY;
785
+ this.style.setProperty('--calendar-x', `${x}px`);
786
+ this.style.setProperty('--calendar-y', `${y}px`);
787
+ this.style.setProperty('--calendar-offset-x', '0px');
788
+ this.style.setProperty('--calendar-offset-y', '0px');
789
+ if (positionBelow) {
790
+ dialog.classList.add('position-below');
791
+ dialog.classList.remove('position-above');
792
+ }
793
+ else {
794
+ dialog.classList.add('position-above');
795
+ dialog.classList.remove('position-below');
796
+ }
797
+ }
798
+ renderContainer() {
799
+ const container = document.createElement('div');
800
+ container.className = 'dropdown-container';
801
+ const label = this.getProperty('label');
802
+ if (label) {
803
+ const labelEl = document.createElement('label');
804
+ labelEl.className = 'ty-field-label';
805
+ labelEl.id = `date-picker-label-${getElementHash(this)}`;
806
+ labelEl.innerHTML = label + (this.getProperty('required') ? '<span class="required-icon">*</span>' : '');
807
+ container.appendChild(labelEl);
808
+ }
809
+ return container;
810
+ }
811
+ render() {
812
+ if (!this.shadowRoot)
813
+ return;
814
+ this.updateValidity();
815
+ ensureStyles(this.shadowRoot, { css: datePickerStyles, id: 'ty-date-picker' });
816
+ if (isMobileTouch()) {
817
+ const existingNativeInput = this.shadowRoot.querySelector('.native-date-input');
818
+ if (existingNativeInput) {
819
+ this.updateDisplay();
820
+ return;
821
+ }
822
+ this.shadowRoot.innerHTML = '';
823
+ const container = this.renderContainer();
824
+ container.appendChild(this.renderNativeInput());
825
+ this.shadowRoot.appendChild(container);
826
+ return;
827
+ }
828
+ const existingDialog = this.shadowRoot.querySelector('.calendar-dialog');
829
+ const isDialogOpen = existingDialog && existingDialog.open;
830
+ if (isDialogOpen && this._state.open) {
831
+ this.updateDisplay();
832
+ this.calculateCalendarPosition();
833
+ return;
834
+ }
835
+ this.shadowRoot.innerHTML = '';
836
+ const container = this.renderContainer();
837
+ const wrapper = document.createElement('div');
838
+ wrapper.className = 'dropdown-wrapper';
839
+ wrapper.appendChild(this.renderStub());
840
+ wrapper.appendChild(this.renderCalendarDropdown());
841
+ container.appendChild(wrapper);
842
+ this.shadowRoot.appendChild(container);
843
+ this.setupEventListeners();
844
+ }
845
+ updateDisplay() {
846
+ if (!this.shadowRoot)
847
+ return;
848
+ const formattedValue = this.getFormattedValue();
849
+ const placeholder = this.getProperty('placeholder') || 'Select date...';
850
+ const hasValue = this.hasDate();
851
+ const clearable = this.getProperty('clearable');
852
+ const isDisabled = this.getProperty('disabled');
853
+ const stubText = this.shadowRoot.querySelector('.stub-text');
854
+ if (stubText) {
855
+ stubText.textContent = formattedValue || placeholder;
856
+ stubText.classList.toggle('placeholder', !formattedValue);
857
+ }
858
+ const clearButton = this.shadowRoot.querySelector('.stub-clear');
859
+ if (clearButton) {
860
+ clearButton.style.display = (clearable && hasValue && !isDisabled) ? '' : 'none';
861
+ }
862
+ if (isMobileTouch()) {
863
+ const nativeInput = this.shadowRoot.querySelector('.native-date-input');
864
+ if (nativeInput) {
865
+ const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
866
+ nativeInput.value = localValue || '';
867
+ }
868
+ return;
869
+ }
870
+ const calendar = this.shadowRoot.querySelector('ty-calendar');
871
+ if (calendar && hasValue) {
872
+ calendar.setAttribute('year', this._state.year.toString());
873
+ calendar.setAttribute('month', this._state.month.toString());
874
+ calendar.setAttribute('day', this._state.day.toString());
875
+ }
876
+ if (calendar) {
877
+ const flavor = this.getProperty('flavor');
878
+ if (flavor && flavor !== 'default')
879
+ calendar.setAttribute('flavor', flavor);
880
+ else
881
+ calendar.removeAttribute('flavor');
882
+ const min = this.getProperty('min');
883
+ const max = this.getProperty('max');
884
+ if (min)
885
+ calendar.setAttribute('min', min);
886
+ else
887
+ calendar.removeAttribute('min');
888
+ if (max)
889
+ calendar.setAttribute('max', max);
890
+ else
891
+ calendar.removeAttribute('max');
892
+ }
893
+ if (this._timeInput && this._state.hour !== undefined && this._state.minute !== undefined) {
894
+ this._timeInput.setTime(this._state.hour, this._state.minute);
895
+ }
896
+ }
897
+ setupEventListeners() {
898
+ if (this._clickListener) {
899
+ document.removeEventListener('click', this._clickListener);
900
+ }
901
+ if (this._keydownListener) {
902
+ document.removeEventListener('keydown', this._keydownListener);
903
+ }
904
+ this._clickListener = (e) => this.handleOutsideClick(e);
905
+ this._keydownListener = (e) => this.handleEscapeKey(e);
906
+ document.addEventListener('click', this._clickListener);
907
+ document.addEventListener('keydown', this._keydownListener);
908
+ const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
909
+ if (dialog) {
910
+ this._dialogClickListener = (e) => this.handleDialogClick(e);
911
+ dialog.addEventListener('click', this._dialogClickListener);
912
+ }
913
+ }
914
+ handleStubClick(event) {
915
+ event.preventDefault();
916
+ const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
917
+ if (this._state.open && (!dialog || !dialog.open)) {
918
+ this.updateState({ open: false }, true);
919
+ unlockScroll(`date-picker-${this.id || this.toString()}`);
920
+ }
921
+ const disabled = this.getProperty('disabled');
922
+ if (!disabled && Date.now() - this._closeTimestamp > 300) {
923
+ this.openDropdown();
924
+ }
925
+ }
926
+ clearValue() {
927
+ this.updateState({
928
+ year: undefined,
929
+ month: undefined,
930
+ day: undefined,
931
+ hour: undefined,
932
+ minute: undefined,
933
+ }, true);
934
+ this.value = null;
935
+ this.emitChangeEvent(null, 'clear');
936
+ this.render();
937
+ }
938
+ handleClearClick(event) {
939
+ event.preventDefault();
940
+ event.stopPropagation();
941
+ this.clearValue();
942
+ }
943
+ handleCalendarChange(event) {
944
+ event.preventDefault();
945
+ event.stopPropagation();
946
+ const customEvent = event;
947
+ const detail = customEvent.detail;
948
+ const dayContext = detail.dayContext;
949
+ if (!dayContext)
950
+ return;
951
+ const newComponents = {
952
+ year: dayContext.year,
953
+ month: dayContext.month,
954
+ day: dayContext.dayInMonth,
955
+ hour: this._state.hour,
956
+ minute: this._state.minute,
957
+ };
958
+ this.updateState(newComponents, true);
959
+ this.emitChangeEvent(newComponents, 'selection');
960
+ if (!this._state.withTime) {
961
+ this.closeDropdown();
962
+ }
963
+ else {
964
+ requestAnimationFrame(() => {
965
+ if (!this.shadowRoot)
966
+ return;
967
+ const timeInput = this.shadowRoot.querySelector('.time-input');
968
+ if (timeInput) {
969
+ timeInput.focus();
970
+ }
971
+ });
972
+ }
973
+ }
974
+ handleDialogClick(event) {
975
+ if (!this.shadowRoot)
976
+ return;
977
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
978
+ const content = this.shadowRoot.querySelector('.calendar-content');
979
+ if (event.target === dialog && this._state.open && content && !content.contains(event.target)) {
980
+ event.preventDefault();
981
+ event.stopPropagation();
982
+ this.closeDropdown();
983
+ }
984
+ }
985
+ handleOutsideClick(event) {
986
+ if (!this.shadowRoot)
987
+ return;
988
+ const target = event.target;
989
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
990
+ if (this._state.open && dialog && !this.contains(target) && !dialog.contains(target)) {
991
+ this.closeDropdown();
992
+ }
993
+ }
994
+ handleEscapeKey(event) {
995
+ const keyboardEvent = event;
996
+ if (keyboardEvent.key === 'Escape' && this._state.open) {
997
+ keyboardEvent.preventDefault();
998
+ this.closeDropdown();
999
+ }
1000
+ }
1001
+ openDropdown() {
1002
+ if (this._state.open)
1003
+ return;
1004
+ if (isMobileTouch())
1005
+ return;
1006
+ this.updateState({ open: true });
1007
+ this.dispatchEvent(new CustomEvent('open'));
1008
+ this.render();
1009
+ requestAnimationFrame(() => {
1010
+ if (!this.shadowRoot)
1011
+ return;
1012
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1013
+ if (dialog) {
1014
+ const pickerId = `date-picker-${this.id || this.toString()}`;
1015
+ lockScroll(pickerId);
1016
+ dialog.showModal();
1017
+ this.calculateCalendarPosition();
1018
+ dialog.classList.add('open');
1019
+ const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
1020
+ if (stubEl)
1021
+ stubEl.setAttribute('aria-expanded', 'true');
1022
+ const focusedElement = this.shadowRoot.activeElement;
1023
+ if (focusedElement) {
1024
+ focusedElement.blur();
1025
+ }
1026
+ }
1027
+ });
1028
+ }
1029
+ closeDropdown() {
1030
+ if (!this._state.open)
1031
+ return;
1032
+ this._closeTimestamp = Date.now();
1033
+ const pickerId = `date-picker-${this.id || this.toString()}`;
1034
+ this.updateState({ open: false }, true);
1035
+ unlockScroll(pickerId);
1036
+ if (this.shadowRoot) {
1037
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1038
+ if (dialog) {
1039
+ dialog.classList.remove('position-above', 'position-below', 'open');
1040
+ dialog.close();
1041
+ }
1042
+ const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
1043
+ if (stubEl)
1044
+ stubEl.setAttribute('aria-expanded', 'false');
1045
+ }
1046
+ this.dispatchEvent(new CustomEvent('close'));
1047
+ this.render();
1048
+ }
1049
+ cleanup() {
1050
+ if (this._clickListener) {
1051
+ document.removeEventListener('click', this._clickListener);
1052
+ }
1053
+ if (this._keydownListener) {
1054
+ document.removeEventListener('keydown', this._keydownListener);
1055
+ }
1056
+ if (this._dialogClickListener && this.shadowRoot) {
1057
+ const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1058
+ if (dialog) {
1059
+ dialog.removeEventListener('click', this._dialogClickListener);
1060
+ }
1061
+ }
1062
+ if (this._state.open) {
1063
+ const pickerId = `date-picker-${this.id || this.toString()}`;
1064
+ unlockScroll(pickerId);
1065
+ }
1066
+ this._clickListener = undefined;
1067
+ this._keydownListener = undefined;
1068
+ this._dialogClickListener = undefined;
1069
+ this._timeInput = undefined;
1070
+ }
1071
+ }
1590
1072
  TyDatePicker.properties = {
1591
- // String properties
1592
- size: {
1593
- type: 'string',
1594
- visual: true,
1595
- default: 'md',
1596
- // Fields come in exactly three sizes; legacy xs/xl map to sm/lg.
1597
- validate: (v) => ['sm', 'md', 'lg'].includes(v),
1598
- coerce: (v) => {
1599
- if (v === 'xs')
1600
- return 'sm';
1601
- if (v === 'xl')
1602
- return 'lg';
1603
- if (!['sm', 'md', 'lg'].includes(v)) {
1604
- console.warn(`[ty-date-picker] Invalid size '${v}'. Using 'md'.`);
1605
- return 'md';
1606
- }
1607
- return v;
1608
- }
1609
- },
1610
- flavor: {
1611
- type: 'string',
1612
- visual: true,
1613
- default: 'default'
1614
- },
1615
- label: {
1616
- type: 'string',
1617
- visual: true,
1618
- default: ''
1619
- },
1620
- placeholder: {
1621
- type: 'string',
1622
- visual: true,
1623
- default: 'Select date...'
1624
- },
1625
- name: {
1626
- type: 'string',
1627
- visual: false, // Non-visual, just for form field name
1628
- default: ''
1629
- },
1630
- // Date bounds (ISO) — passed through to the embedded ty-calendar
1631
- min: {
1632
- type: 'string',
1633
- visual: true,
1634
- default: ''
1635
- },
1636
- max: {
1637
- type: 'string',
1638
- visual: true,
1639
- default: ''
1640
- },
1641
- // Boolean properties
1642
- required: {
1643
- type: 'boolean',
1644
- visual: true,
1645
- default: false
1646
- },
1647
- disabled: {
1648
- type: 'boolean',
1649
- visual: true,
1650
- default: false
1651
- },
1652
- clearable: {
1653
- type: 'boolean',
1654
- visual: true,
1655
- default: true,
1656
- aliases: {
1657
- 'not-clearable': false
1658
- }
1659
- },
1660
- // Format property (medium complexity)
1661
- format: {
1662
- type: 'string',
1663
- visual: true,
1664
- default: 'long',
1665
- validate: (v) => ['short', 'medium', 'long', 'full'].includes(v),
1666
- coerce: (v) => {
1667
- const validFormats = ['short', 'medium', 'long', 'full'];
1668
- if (!validFormats.includes(v)) {
1669
- console.warn(`[ty-date-picker] Invalid format '${v}'. Using 'long'.`);
1670
- return 'long';
1671
- }
1672
- return v;
1673
- }
1674
- },
1675
- // Locale property (medium complexity - has observer)
1676
- locale: {
1677
- type: 'string',
1678
- visual: true,
1679
- default: ''
1680
- },
1681
- // Value property (high complexity - UTC parsing, date components)
1682
- // Note: Custom getter/setter will handle the complex logic
1683
- value: {
1684
- type: 'string',
1685
- visual: true,
1686
- formValue: true, // Syncs to form
1687
- emitChange: false, // We emit custom 'change' events manually with full detail
1688
- default: null // null when no date selected
1689
- },
1690
- // With-time property (high complexity - affects parsing and rendering)
1691
- 'with-time': {
1692
- type: 'boolean',
1693
- visual: true,
1694
- default: false
1695
- }
1073
+ placement: {
1074
+ type: 'string',
1075
+ visual: true,
1076
+ default: '',
1077
+ },
1078
+ size: {
1079
+ type: 'string',
1080
+ visual: true,
1081
+ default: DEFAULT_SIZE,
1082
+ validate: (v) => ["xs", "sm", "md", "lg", "xl"].includes(v),
1083
+ coerce: (v) => {
1084
+ if (!["xs", "sm", "md", "lg", "xl"].includes(v)) {
1085
+ console.warn(`[ty-date-picker] Invalid size '${v}'. Using '${DEFAULT_SIZE}'.`);
1086
+ return DEFAULT_SIZE;
1087
+ }
1088
+ return v;
1089
+ }
1090
+ },
1091
+ flavor: {
1092
+ type: 'string',
1093
+ visual: true,
1094
+ default: 'default'
1095
+ },
1096
+ label: {
1097
+ type: 'string',
1098
+ visual: true,
1099
+ default: ''
1100
+ },
1101
+ placeholder: {
1102
+ type: 'string',
1103
+ visual: true,
1104
+ default: 'Select date...'
1105
+ },
1106
+ name: {
1107
+ type: 'string',
1108
+ visual: false,
1109
+ default: ''
1110
+ },
1111
+ min: {
1112
+ type: 'string',
1113
+ visual: true,
1114
+ default: ''
1115
+ },
1116
+ max: {
1117
+ type: 'string',
1118
+ visual: true,
1119
+ default: ''
1120
+ },
1121
+ required: {
1122
+ type: 'boolean',
1123
+ visual: true,
1124
+ default: false
1125
+ },
1126
+ disabled: {
1127
+ type: 'boolean',
1128
+ visual: true,
1129
+ default: false
1130
+ },
1131
+ clearable: {
1132
+ type: 'boolean',
1133
+ visual: true,
1134
+ default: true,
1135
+ aliases: {
1136
+ 'not-clearable': false
1137
+ }
1138
+ },
1139
+ format: {
1140
+ type: 'string',
1141
+ visual: true,
1142
+ default: 'long',
1143
+ validate: (v) => ['short', 'medium', 'long', 'full'].includes(v),
1144
+ coerce: (v) => {
1145
+ const validFormats = ['short', 'medium', 'long', 'full'];
1146
+ if (!validFormats.includes(v)) {
1147
+ console.warn(`[ty-date-picker] Invalid format '${v}'. Using 'long'.`);
1148
+ return 'long';
1149
+ }
1150
+ return v;
1151
+ }
1152
+ },
1153
+ locale: {
1154
+ type: 'string',
1155
+ visual: true,
1156
+ default: ''
1157
+ },
1158
+ value: {
1159
+ type: 'string',
1160
+ visual: true,
1161
+ formValue: true,
1162
+ emitChange: false,
1163
+ default: null
1164
+ },
1165
+ 'with-time': {
1166
+ type: 'boolean',
1167
+ visual: true,
1168
+ default: false
1169
+ }
1696
1170
  };
1697
- /**
1698
- * Form-associated custom element
1699
- */
1700
1171
  TyDatePicker.formAssociated = true;
1701
- // Register custom element
1702
1172
  if (!customElements.get('ty-date-picker')) {
1703
- customElements.define('ty-date-picker', TyDatePicker);
1173
+ customElements.define('ty-date-picker', TyDatePicker);
1704
1174
  }
1705
- //# sourceMappingURL=date-picker.js.map