vectis-ui 0.10.1 → 0.10.2

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 (330) hide show
  1. package/dist/components/VAccordion/VAccordion.js.map +1 -1
  2. package/dist/components/VAccordion/VAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
  3. package/dist/components/VAccordion/VAccordionItem.d.ts +6 -21
  4. package/dist/components/VAccordion/VAccordionItem.js.map +1 -1
  5. package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  6. package/dist/components/VAccordion/context.js.map +1 -1
  7. package/dist/components/VAvatar/VAvatar.d.ts +2 -12
  8. package/dist/components/VAvatar/VAvatar.js.map +1 -1
  9. package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  10. package/dist/components/VAvatar/VAvatarGroup.d.ts +2 -2
  11. package/dist/components/VAvatar/VAvatarGroup.js.map +1 -1
  12. package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js +6 -6
  13. package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  14. package/dist/components/VAvatar/context.d.ts +1 -5
  15. package/dist/components/VAvatar/context.js.map +1 -1
  16. package/dist/components/VBadge/VBadge.d.ts +13 -27
  17. package/dist/components/VBadge/VBadge.js.map +1 -1
  18. package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js.map +1 -1
  19. package/dist/components/VBreadcrumb/VBreadcrumb.d.ts +3 -9
  20. package/dist/components/VBreadcrumb/VBreadcrumb.js.map +1 -1
  21. package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
  22. package/dist/components/VButton/VButton.d.ts +10 -17
  23. package/dist/components/VButton/VButton.js.map +1 -1
  24. package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  25. package/dist/components/VButton/VButtonGroup.d.ts +6 -15
  26. package/dist/components/VButton/VButtonGroup.js.map +1 -1
  27. package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  28. package/dist/components/VButton/context.d.ts +1 -9
  29. package/dist/components/VButton/context.js.map +1 -1
  30. package/dist/components/VCalendar/VCalendar.d.ts +9 -24
  31. package/dist/components/VCalendar/VCalendar.js.map +1 -1
  32. package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  33. package/dist/components/VCalendar/VCalendarEvent.d.ts +4 -13
  34. package/dist/components/VCalendar/VCalendarEvent.js.map +1 -1
  35. package/dist/components/VCalendar/VCalendarEvent.vue_vue_type_script_setup_true_lang.js.map +1 -1
  36. package/dist/components/VCalendar/VCalendarMonth.js.map +1 -1
  37. package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js.map +1 -1
  38. package/dist/components/VCalendar/VCalendarTimeGrid.d.ts +1 -5
  39. package/dist/components/VCalendar/VCalendarTimeGrid.js.map +1 -1
  40. package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js.map +1 -1
  41. package/dist/components/VCalendar/VCalendarYear.js.map +1 -1
  42. package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js.map +1 -1
  43. package/dist/components/VCalendar/color.d.ts +1 -7
  44. package/dist/components/VCalendar/color.js.map +1 -1
  45. package/dist/components/VCalendar/edgeStep.d.ts +5 -15
  46. package/dist/components/VCalendar/edgeStep.js.map +1 -1
  47. package/dist/components/VCalendar/gesture.d.ts +12 -36
  48. package/dist/components/VCalendar/gesture.js.map +1 -1
  49. package/dist/components/VCalendar/keyboard.d.ts +5 -42
  50. package/dist/components/VCalendar/keyboard.js.map +1 -1
  51. package/dist/components/VCalendar/layout.d.ts +38 -199
  52. package/dist/components/VCalendar/layout.js.map +1 -1
  53. package/dist/components/VCalendar/timeGrid.d.ts +3 -25
  54. package/dist/components/VCalendar/timeGrid.js.map +1 -1
  55. package/dist/components/VCalendar/types.d.ts +6 -23
  56. package/dist/components/VCarousel/VCarousel.d.ts +25 -96
  57. package/dist/components/VCarousel/VCarousel.js.map +1 -1
  58. package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js +6 -6
  59. package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  60. package/dist/components/VCarousel/VCarouselItem.d.ts +1 -6
  61. package/dist/components/VCarousel/VCarouselItem.js.map +1 -1
  62. package/dist/components/VCarousel/VCarouselItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  63. package/dist/components/VCarousel/context.js.map +1 -1
  64. package/dist/components/VCheckbox/VCheckbox.d.ts +1 -5
  65. package/dist/components/VCheckbox/VCheckbox.js.map +1 -1
  66. package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  67. package/dist/components/VChip/VChip.d.ts +2 -8
  68. package/dist/components/VChip/VChip.js.map +1 -1
  69. package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  70. package/dist/components/VCombobox/VCombobox.d.ts +21 -59
  71. package/dist/components/VCombobox/VCombobox.js.map +1 -1
  72. package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +117 -119
  73. package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  74. package/dist/components/VCombobox/VComboboxGroup.js.map +1 -1
  75. package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  76. package/dist/components/VCombobox/VComboboxOption.js.map +1 -1
  77. package/dist/components/VCombobox/VComboboxOption.vue_vue_type_script_setup_true_lang.js.map +1 -1
  78. package/dist/components/VCombobox/VComboboxSeparator.js.map +1 -1
  79. package/dist/components/VCombobox/VComboboxSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  80. package/dist/components/VCombobox/infiniteScroll.d.ts +4 -10
  81. package/dist/components/VCombobox/infiniteScroll.js.map +1 -1
  82. package/dist/components/VDataTable/VDataTable.d.ts +11 -36
  83. package/dist/components/VDataTable/VDataTable.js.map +1 -1
  84. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +23 -20
  85. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
  86. package/dist/components/VDateInput/VDateInput.d.ts +12 -21
  87. package/dist/components/VDateInput/VDateInput.js.map +1 -1
  88. package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  89. package/dist/components/VDateInput/mask.d.ts +14 -59
  90. package/dist/components/VDateInput/mask.js.map +1 -1
  91. package/dist/components/VDatePicker/VDatePicker.d.ts +6 -12
  92. package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
  93. package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  94. package/dist/components/VDatePicker/keyboard.d.ts +10 -42
  95. package/dist/components/VDatePicker/keyboard.js.map +1 -1
  96. package/dist/components/VDatePicker/names.d.ts +8 -22
  97. package/dist/components/VDatePicker/names.js.map +1 -1
  98. package/dist/components/VDialog/VDialog.d.ts +5 -10
  99. package/dist/components/VDialog/VDialog.js.map +1 -1
  100. package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js +51 -53
  101. package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
  102. package/dist/components/VDialog/VDialogAlert.d.ts +2 -3
  103. package/dist/components/VDialog/VDialogAlert.js.map +1 -1
  104. package/dist/components/VDialog/VDialogAlert.vue_vue_type_script_setup_true_lang.js.map +1 -1
  105. package/dist/components/VFileInput/VFileInput.d.ts +11 -37
  106. package/dist/components/VFileInput/VFileInput.js.map +1 -1
  107. package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  108. package/dist/components/VFileInput/truncate.d.ts +3 -23
  109. package/dist/components/VFileInput/truncate.js.map +1 -1
  110. package/dist/components/VFilePicker/VFilePicker.d.ts +15 -65
  111. package/dist/components/VFilePicker/VFilePicker.js.map +1 -1
  112. package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  113. package/dist/components/VFilePicker/fileKind.js.map +1 -1
  114. package/dist/components/VHotkeys/VHotkeys.d.ts +6 -15
  115. package/dist/components/VHotkeys/VHotkeys.js.map +1 -1
  116. package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
  117. package/dist/components/VHotkeys/platform.d.ts +10 -49
  118. package/dist/components/VHotkeys/platform.js.map +1 -1
  119. package/dist/components/VIcon/VIcon.d.ts +7 -19
  120. package/dist/components/VIcon/VIcon.js.map +1 -1
  121. package/dist/components/VIcon/VIcon.vue_vue_type_script_setup_true_lang.js.map +1 -1
  122. package/dist/components/VIcon/iconProps.d.ts +5 -19
  123. package/dist/components/VIcon/iconProps.js.map +1 -1
  124. package/dist/components/VIcon/resolver.d.ts +15 -43
  125. package/dist/components/VIcon/resolver.js.map +1 -1
  126. package/dist/components/VIcon/types.d.ts +7 -34
  127. package/dist/components/VIconButton/VIconButton.d.ts +2 -3
  128. package/dist/components/VIconButton/VIconButton.js.map +1 -1
  129. package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  130. package/dist/components/VInput/VInput.d.ts +14 -29
  131. package/dist/components/VInput/VInput.js.map +1 -1
  132. package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  133. package/dist/components/VInput/VInputGroup.js.map +1 -1
  134. package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js +34 -50
  135. package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  136. package/dist/components/VInput/context.js.map +1 -1
  137. package/dist/components/VInputOTP/VInputOTP.d.ts +8 -16
  138. package/dist/components/VInputOTP/VInputOTP.js.map +1 -1
  139. package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js +55 -63
  140. package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js.map +1 -1
  141. package/dist/components/VMenu/VMenu.js.map +1 -1
  142. package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  143. package/dist/components/VMenu/VMenuGroup.d.ts +1 -6
  144. package/dist/components/VMenu/VMenuGroup.js.map +1 -1
  145. package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  146. package/dist/components/VMenu/VMenuItem.d.ts +0 -5
  147. package/dist/components/VMenu/VMenuItem.js.map +1 -1
  148. package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  149. package/dist/components/VMenu/VMenuPanel.d.ts +1 -6
  150. package/dist/components/VMenu/VMenuPanel.js.map +1 -1
  151. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  152. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  153. package/dist/components/VMenu/VMenuSeparator.js.map +1 -1
  154. package/dist/components/VMenu/VMenuSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  155. package/dist/components/VMenu/context.d.ts +3 -11
  156. package/dist/components/VMenu/context.js.map +1 -1
  157. package/dist/components/VPagination/VPagination.d.ts +9 -20
  158. package/dist/components/VPagination/VPagination.js.map +1 -1
  159. package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
  160. package/dist/components/VPopover/VPopover.d.ts +7 -18
  161. package/dist/components/VPopover/VPopover.js.map +1 -1
  162. package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js +23 -27
  163. package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
  164. package/dist/components/VProgressCircular/VProgressCircular.d.ts +2 -6
  165. package/dist/components/VProgressCircular/VProgressCircular.js.map +1 -1
  166. package/dist/components/VProgressCircular/VProgressCircular.vue_vue_type_script_setup_true_lang.js.map +1 -1
  167. package/dist/components/VProgressLinear/VProgressLinear.d.ts +4 -15
  168. package/dist/components/VProgressLinear/VProgressLinear.js.map +1 -1
  169. package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js.map +1 -1
  170. package/dist/components/VRadio/VRadio.d.ts +1 -5
  171. package/dist/components/VRadio/VRadio.js.map +1 -1
  172. package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
  173. package/dist/components/VSeparator/VSeparator.d.ts +2 -15
  174. package/dist/components/VSeparator/VSeparator.js.map +1 -1
  175. package/dist/components/VSeparator/VSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  176. package/dist/components/VSideNavigation/VSideNavigation.d.ts +1 -1
  177. package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
  178. package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
  179. package/dist/components/VSideNavigation/VSideNavigationGroup.d.ts +1 -5
  180. package/dist/components/VSideNavigation/VSideNavigationGroup.js.map +1 -1
  181. package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  182. package/dist/components/VSideNavigation/VSideNavigationItem.d.ts +1 -5
  183. package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
  184. package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js +80 -75
  185. package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  186. package/dist/components/VSideNavigation/VSideNavigationSeparator.js.map +1 -1
  187. package/dist/components/VSideNavigation/context.js.map +1 -1
  188. package/dist/components/VSkeletonLoader/VSkeletonLoader.d.ts +6 -19
  189. package/dist/components/VSkeletonLoader/VSkeletonLoader.js.map +1 -1
  190. package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js.map +1 -1
  191. package/dist/components/VSlider/VSlider.d.ts +5 -13
  192. package/dist/components/VSlider/VSlider.js.map +1 -1
  193. package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
  194. package/dist/components/VSnackbar/VSnackbar.js.map +1 -1
  195. package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  196. package/dist/components/VSnackbar/state.d.ts +4 -17
  197. package/dist/components/VSnackbar/state.js.map +1 -1
  198. package/dist/components/VSpinner/VSpinner.d.ts +3 -5
  199. package/dist/components/VSpinner/VSpinner.js.map +1 -1
  200. package/dist/components/VSpinner/VSpinner.vue_vue_type_script_setup_true_lang.js.map +1 -1
  201. package/dist/components/VSwitch/VSwitch.js.map +1 -1
  202. package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
  203. package/dist/components/VTabs/VTab.d.ts +1 -5
  204. package/dist/components/VTabs/VTab.js.map +1 -1
  205. package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js.map +1 -1
  206. package/dist/components/VTabs/VTabPanel.js.map +1 -1
  207. package/dist/components/VTabs/VTabPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  208. package/dist/components/VTabs/VTabs.d.ts +5 -17
  209. package/dist/components/VTabs/VTabs.js.map +1 -1
  210. package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
  211. package/dist/components/VTabs/context.js.map +1 -1
  212. package/dist/components/VTextarea/VTextarea.d.ts +13 -20
  213. package/dist/components/VTextarea/VTextarea.js.map +1 -1
  214. package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
  215. package/dist/components/VTimeInput/VTimeInput.d.ts +8 -21
  216. package/dist/components/VTimeInput/VTimeInput.js.map +1 -1
  217. package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  218. package/dist/components/VTimeInput/search.d.ts +3 -34
  219. package/dist/components/VTimeInput/search.js.map +1 -1
  220. package/dist/components/VTimePicker/VTimePicker.d.ts +9 -31
  221. package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
  222. package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  223. package/dist/components/VTimePicker/limits.d.ts +18 -45
  224. package/dist/components/VTimePicker/limits.js.map +1 -1
  225. package/dist/components/VToast/VToast.js.map +1 -1
  226. package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js.map +1 -1
  227. package/dist/components/VToast/VToaster.js.map +1 -1
  228. package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
  229. package/dist/components/VToast/state.d.ts +2 -7
  230. package/dist/components/VToast/state.js.map +1 -1
  231. package/dist/components/VToggle/VToggle.d.ts +5 -9
  232. package/dist/components/VToggle/VToggle.js.map +1 -1
  233. package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
  234. package/dist/components/VToggle/VToggleItem.d.ts +1 -9
  235. package/dist/components/VToggle/VToggleItem.js.map +1 -1
  236. package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  237. package/dist/components/VToggle/context.js.map +1 -1
  238. package/dist/components/VTooltip/VTooltip.d.ts +3 -16
  239. package/dist/components/VTooltip/VTooltip.js.map +1 -1
  240. package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js +2 -2
  241. package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  242. package/dist/components/VTypography/VTypography.d.ts +4 -8
  243. package/dist/components/VTypography/VTypography.js.map +1 -1
  244. package/dist/components/VTypography/VTypography.vue_vue_type_script_setup_true_lang.js.map +1 -1
  245. package/dist/composables/useAriaLabel.js.map +1 -1
  246. package/dist/composables/useClearable.d.ts +2 -8
  247. package/dist/composables/useClearable.js.map +1 -1
  248. package/dist/composables/useControlShape.js.map +1 -1
  249. package/dist/composables/useDetailsOpen.d.ts +2 -10
  250. package/dist/composables/useDetailsOpen.js.map +1 -1
  251. package/dist/composables/useFieldIds.js.map +1 -1
  252. package/dist/composables/useFieldPanel.d.ts +4 -17
  253. package/dist/composables/useFieldPanel.js.map +1 -1
  254. package/dist/composables/useFileDrop.js.map +1 -1
  255. package/dist/composables/useFileField.d.ts +1 -5
  256. package/dist/composables/useFileField.js.map +1 -1
  257. package/dist/composables/useFocusoutDismiss.js.map +1 -1
  258. package/dist/composables/useIconClickHandlers.d.ts +4 -19
  259. package/dist/composables/useIconClickHandlers.js.map +1 -1
  260. package/dist/composables/useInertLink.d.ts +1 -1
  261. package/dist/composables/useInertLink.js.map +1 -1
  262. package/dist/composables/useLiveAnnouncer.js.map +1 -1
  263. package/dist/composables/useMaskedField.d.ts +12 -27
  264. package/dist/composables/useMaskedField.js.map +1 -1
  265. package/dist/composables/usePopover.d.ts +3 -11
  266. package/dist/composables/usePopover.js.map +1 -1
  267. package/dist/composables/useProgressValue.js.map +1 -1
  268. package/dist/composables/useRootAttrs.d.ts +0 -9
  269. package/dist/composables/useRootAttrs.js +11 -4
  270. package/dist/composables/useRootAttrs.js.map +1 -1
  271. package/dist/composables/useSlotNodes.d.ts +3 -14
  272. package/dist/composables/useSlotNodes.js.map +1 -1
  273. package/dist/composables/useTextLimit.d.ts +4 -16
  274. package/dist/composables/useTextLimit.js.map +1 -1
  275. package/dist/composables/useTimer.d.ts +2 -15
  276. package/dist/composables/useTimer.js.map +1 -1
  277. package/dist/i18n/en.js.map +1 -1
  278. package/dist/i18n/fr.js.map +1 -1
  279. package/dist/i18n/state.d.ts +9 -38
  280. package/dist/i18n/state.js.map +1 -1
  281. package/dist/i18n/types.d.ts +19 -81
  282. package/dist/icons.d.ts +2 -12
  283. package/dist/tokens/css.d.ts +3 -16
  284. package/dist/tokens/css.js.map +1 -1
  285. package/dist/tokens/index.js.map +1 -1
  286. package/dist/tokens/primitives.js.map +1 -1
  287. package/dist/tokens/semantic.d.ts +1 -23
  288. package/dist/tokens/semantic.js.map +1 -1
  289. package/dist/tokens/themes/dark.js.map +1 -1
  290. package/dist/tokens/types.d.ts +2 -12
  291. package/dist/tokens/types.js.map +1 -1
  292. package/dist/types.d.ts +3 -21
  293. package/dist/utils/array.d.ts +2 -4
  294. package/dist/utils/array.js.map +1 -1
  295. package/dist/utils/arrowNav.d.ts +2 -26
  296. package/dist/utils/arrowNav.js.map +1 -1
  297. package/dist/utils/chip.d.ts +1 -8
  298. package/dist/utils/chip.js.map +1 -1
  299. package/dist/utils/clock.d.ts +16 -39
  300. package/dist/utils/clock.js.map +1 -1
  301. package/dist/utils/css.d.ts +5 -16
  302. package/dist/utils/css.js.map +1 -1
  303. package/dist/utils/date.d.ts +17 -60
  304. package/dist/utils/date.js.map +1 -1
  305. package/dist/utils/direction.d.ts +2 -9
  306. package/dist/utils/direction.js.map +1 -1
  307. package/dist/utils/env.d.ts +2 -15
  308. package/dist/utils/env.js.map +1 -1
  309. package/dist/utils/file.d.ts +8 -46
  310. package/dist/utils/file.js.map +1 -1
  311. package/dist/utils/focus.d.ts +1 -14
  312. package/dist/utils/focus.js +1 -6
  313. package/dist/utils/focus.js.map +1 -1
  314. package/dist/utils/hostWarns.d.ts +3 -8
  315. package/dist/utils/hostWarns.js.map +1 -1
  316. package/dist/utils/ids.d.ts +2 -7
  317. package/dist/utils/ids.js.map +1 -1
  318. package/dist/utils/matcher.d.ts +1 -8
  319. package/dist/utils/matcher.js.map +1 -1
  320. package/dist/utils/memo.d.ts +1 -11
  321. package/dist/utils/memo.js.map +1 -1
  322. package/dist/utils/number.d.ts +1 -7
  323. package/dist/utils/number.js.map +1 -1
  324. package/dist/utils/text.d.ts +4 -23
  325. package/dist/utils/text.js.map +1 -1
  326. package/dist/utils/time.d.ts +4 -12
  327. package/dist/utils/time.js.map +1 -1
  328. package/dist/utils/vnode.d.ts +0 -10
  329. package/dist/utils/vnode.js.map +1 -1
  330. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"VInputOTP.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VInputOTP/VInputOTP.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * The row of boxes a one-time code is typed into, one character per box.\n *\n * The platform has no such control, so each box is a real `<input>` and the JS is what makes\n * the row behave as one field: a character moves to the next box, Backspace on an empty one\n * goes back, the arrows walk along, and a code pasted anywhere is spread across them. The\n * first box carries `autocomplete=\"one-time-code\"`, so a code arriving from an SMS or a\n * password manager is spread the same way.\n *\n * `pattern` cuts the row up: every `#` is a box, every other character a decorative literal\n * shown between them and never part of the value. Pasting understands those literals — a\n * code copied formatted, `GT-123`, is consumed with them in place — and `format` filters the\n * rest, forcing capitals outside a numeric code so the value has one canonical form.\n *\n * The value is the filled boxes read in order, so the filled boxes always form a PREFIX:\n * typing or focusing past the first empty box lands in that box, and emptying a box closes\n * the gap. Without that invariant `1 _ 3 _` would be the value `13`, which the next sync\n * would redraw as `1 3 _ _`.\n *\n * A row of boxes is not a form control, so a visually hidden native input carries the code\n * for the form: `name`, `form` and `required` land on it, and a `pattern` of the full\n * length makes a half-typed code invalid.\n */\n\nimport { computed, ref, useAttrs, useId, watch } from 'vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport type { IconSource } from '../VIcon/types'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { isDev } from '../../utils/env'\nimport { joinIds } from '../../utils/ids'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** The height of the boxes: 32, 40 or 48 pixels. */\nexport type InputOTPSize = 'sm' | 'md' | 'lg'\n\n/** Which characters the code is made of. */\nexport type InputOTPFormat = 'numeric' | 'alpha' | 'alphanumeric'\n\ninterface InputOTPProps {\n /** How many boxes the code has. It is ignored as soon as a `pattern` is given. */\n length?: number\n /**\n * Which characters the code is made of. It filters what can be typed or pasted, and\n * decides which keyboard a phone offers.\n */\n format?: InputOTPFormat\n /**\n * The shape of the code: each `#` is a box to fill, and every other character is a\n * separator shown between the boxes without ever being part of the value, as in\n * `'GT-###'` or `'###.###.###'`. It wins over `length`.\n */\n pattern?: string\n /**\n * An icon drawn in place of EVERY separator of the pattern. It suits a template\n * whose separators are purely decorative, `'###-###'`, and should not be used with\n * one carrying meaningful text such as `'GT-###'`, which the icon would erase.\n */\n separatorIcon?: IconSource\n /** The size of the boxes: 32, 40 or 48 pixels. */\n size?: InputOTPSize\n /** Takes 4px off the boxes, leaving the text and the icons as they are. */\n compact?: boolean\n /** Makes every box unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows the code without letting it be changed. The boxes keep their focus and the code\n * can still be selected and copied, which is what separates it from `disabled`.\n */\n readonly?: boolean\n /** Marks the code as wrong, which colours the boxes and tells assistive technology so. */\n invalid?: boolean\n /**\n * What screen readers announce for the row as a whole. It falls back to the design\n * system dictionary.\n *\n * Unlike the `label` of VInput and the other text fields, NOTHING is rendered from it:\n * a row of boxes carries its own instructions above it, written by the page. Use `hint`\n * for a line the reader can see.\n */\n label?: string\n /**\n * A line of help under the boxes: where the code was sent, how long it lasts. It is\n * tied to the row for assistive technology, so it is read out along with the label.\n */\n hint?: string\n}\n\nconst props = withDefaults(defineProps<InputOTPProps>(), {\n length: 6,\n format: 'numeric',\n pattern: undefined,\n separatorIcon: undefined,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n label: undefined,\n hint: undefined,\n})\n\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.inputOTP.label)\n\n// @a11y\n/*\n * The root IS the group, so the consumer's attributes belong on it — but they are bound\n * BEFORE the two the component decides itself. `aria-describedby` is a list, and the hint\n * is appended to whatever the consumer already pointed at instead of replacing it; left\n * to the fallthrough, their value would land last and take the hint out of the\n * announcement with nothing to show for it.\n */\ndefineOptions({ inheritAttrs: false })\n\nconst attrs = useAttrs()\n\n// What the FORM reads goes on the hidden native input, and everything else on the group:\n// on the group a `name` submits nothing and `required` validates nothing, silently.\nconst NATIVE_ONLY = ['name', 'form', 'required']\nconst groupAttrs = computed(() =>\n Object.fromEntries(Object.entries(attrs).filter(([key]) => !NATIVE_ONLY.includes(key))),\n)\nconst nativeAttrs = computed(() =>\n Object.fromEntries(Object.entries(attrs).filter(([key]) => NATIVE_ONLY.includes(key))),\n)\n// No `useFieldIds` here: the row is named by `aria-label` and renders no `<label>`, so the\n// field id that composable generates would have nothing to point it at.\nconst hintId = useId()\nconst describedBy = computed(() =>\n joinIds(attrs['aria-describedby'] as string | undefined, !!props.hint && hintId),\n)\n\n/**\n * The code as one string, without the separators: a `GT-###` template still yields three\n * characters. It is empty to begin with, and shorter than the full length while it is being\n * typed.\n */\nconst model = defineModel<string>({ default: '' })\n\nconst emit = defineEmits<{\n /**\n * Emitted the moment the code BECOMES complete, carrying it. Retyping a character of a\n * complete code with the same one does not emit it again.\n */\n complete: [code: string]\n}>()\n\ntype Cell = { type: 'slot'; slotIndex: number } | { type: 'literal'; char: string }\n\nconst cells = computed<Cell[]>(() => {\n if (props.pattern?.includes('#')) {\n let slotIndex = 0\n return [...props.pattern].map((char): Cell =>\n char === '#' ? { type: 'slot', slotIndex: slotIndex++ } : { type: 'literal', char },\n )\n }\n return Array.from({ length: props.length }, (_, i) => ({ type: 'slot', slotIndex: i }))\n})\nconst slotCount = computed(() => cells.value.filter((cell) => cell.type === 'slot').length)\n\n// @devwarn\nif (isDev) {\n if (props.pattern && !props.pattern.includes('#'))\n console.warn(\"[VInputOTP] pattern without '#' — falling back to `length`.\")\n}\n\nconst filters: Record<NonNullable<InputOTPProps['format']>, RegExp> = {\n numeric: /[^0-9]/g,\n alpha: /[^A-Z]/g,\n alphanumeric: /[^A-Z0-9]/g,\n}\n\n/**\n * Keeps only what the format allows. Outside a numeric code the text is put in\n * capitals first, so that the value has a single canonical form whichever case the\n * reader typed or pasted.\n */\nfunction sanitize(text: string) {\n const upper = props.format === 'numeric' ? text : text.toUpperCase()\n return upper.replace(filters[props.format], '')\n}\n\nconst inputs = ref<(HTMLInputElement | null)[]>([])\nconst rootEl = ref<HTMLElement | null>(null)\nconst digits = ref<string[]>([])\n\nfunction syncFromModel(value: string) {\n // A value longer than the row is simply shown cut short, and one holding characters the\n // format refuses is shown filtered — but the model is NOT rewritten to match: writing\n // back here would feed the watcher below and the two would keep correcting each other.\n const clean = sanitize(value)\n digits.value = Array.from({ length: slotCount.value }, (_, i) => clean[i] ?? '')\n}\nsyncFromModel(model.value)\nwatch([model, slotCount], ([value, count]) => {\n if (value !== digits.value.join('') || digits.value.length !== count) syncFromModel(value)\n})\n\n/*\n * TRAP — the list of boxes is trimmed AFTER the render, never before it. A box that\n * disappears calls its function ref with `null` while it is being unmounted, at its OLD\n * position: trimmed in a `pre` watcher, that late assignment would stretch the array back\n * out, and a pattern that shortened would keep its departed boxes as trailing entries.\n */\nwatch(\n slotCount,\n (count) => {\n inputs.value.length = count\n },\n { flush: 'post' },\n)\n\n/** The first empty box, or -1 once the code is complete. Every box before it is filled. */\nconst firstEmpty = () => digits.value.findIndex((d) => !d)\n\n/** Takes a character out and moves the following ones back, so no gap is left behind. */\nfunction removeAt(slot: number) {\n digits.value.splice(slot, 1)\n digits.value.push('')\n}\n\nfunction focusBox(slot: number) {\n const el = inputs.value[slot]\n el?.focus()\n // Selected, so the next keystroke REPLACES the character: a box that already has the\n // focus receives no focus event to select it.\n el?.select()\n}\n\nfunction commit() {\n const code = digits.value.join('')\n const previous = model.value\n model.value = code\n // Only when the code CHANGES into a full one: retyping a character of a complete code\n // must not submit it a second time for a consumer listening to `complete`.\n if (code.length === slotCount.value && code !== previous) emit('complete', code)\n}\n\n// @core\n/**\n * Spreads a run of characters across the boxes, starting from the one that received\n * them. It walks the row: a separator swallows the incoming character when the two\n * match, which is what lets a code pasted in its formatted shape line up, and a box\n * takes the next character the format accepts.\n *\n * It returns the last box it filled, so the caller knows where to put the focus, and\n * `null` when nothing in the text was usable.\n */\nfunction distribute(raw: string, startSlot: number): number | null {\n const allCells = cells.value\n let start = allCells.findIndex((cell) => cell.type === 'slot' && cell.slotIndex === startSlot)\n const chars = [...raw]\n // Step back over the separators immediately before that box: pasting the whole\n // string onto the first box means pasting its prefix too, and \"GT-\" has to be\n // matched rather than treated as characters of the code. Never for ONE character: a\n // `G` typed into the first box of `GT-###` is the code's first character, not the\n // separator, and swallowing it would make such a code impossible to type.\n if (chars.length > 1) while (start > 0 && allCells[start - 1]?.type === 'literal') start--\n let charIndex = 0\n let lastFilled: number | null = null\n for (const cell of allCells.slice(start)) {\n if (charIndex >= chars.length) break\n if (cell.type === 'literal') {\n if (chars[charIndex]?.toUpperCase() === cell.char.toUpperCase()) charIndex++\n continue\n }\n let char = ''\n while (charIndex < chars.length && !char) {\n char = sanitize(chars[charIndex] ?? '')\n charIndex++\n }\n if (!char) break\n digits.value[cell.slotIndex] = char\n lastFilled = cell.slotIndex\n }\n return lastFilled\n}\n\n// @keyboard @core — moving the focus forward on its own is what turns a row of\n// separate inputs into something that types like one field; spreading the characters\n// is the core behaviour underneath it.\nfunction onInput(slotIndex: number, event: Event) {\n const el = event.target as HTMLInputElement\n const empty = firstEmpty()\n const filled = empty === -1 || slotIndex < empty\n\n // A filled box whose character was not selected receives the new one NEXT to it. What\n // was inserted is what lies just before the caret, and it is what counts.\n const previous = digits.value[slotIndex] ?? ''\n let raw = el.value\n const inserted = raw.length - previous.length\n if (previous && inserted > 0 && el.selectionStart !== null) {\n raw = raw.slice(Math.max(0, el.selectionStart - inserted), el.selectionStart)\n }\n\n // The same path serves a single keystroke and a pasted code: both are spread from this\n // box onwards — or from the first empty box, when this one lies past it.\n const lastFilled = distribute(raw, filled ? slotIndex : empty)\n if (lastFilled === null) {\n // Either the box was emptied, or nothing typed was valid for this format. A filled box\n // loses its character and the ones after it close the gap.\n if (filled) removeAt(slotIndex)\n el.value = digits.value[slotIndex] ?? ''\n commit()\n return\n }\n // Written by hand: when the characters went to another box, this one's value is still\n // '' in the render, and the patch would leave the typed character on screen.\n el.value = digits.value[slotIndex] ?? ''\n focusBox(Math.min(lastFilled + 1, slotCount.value - 1))\n commit()\n}\n\n// @keyboard — the filled boxes form a prefix, so focusing a box past the first empty one\n// sends the focus there instead: a pointer landing on box 5 of a two-character code types\n// into box 3.\nfunction onFocus(slotIndex: number, event: FocusEvent) {\n const empty = firstEmpty()\n if (empty !== -1 && slotIndex > empty) inputs.value[empty]?.focus()\n else (event.target as HTMLInputElement).select()\n}\n\n// @keyboard\nfunction onKeydown(slotIndex: number, event: KeyboardEvent) {\n const empty = firstEmpty()\n // The furthest box the focus may reach: the first empty one, or the last of a full code.\n const reachable = empty === -1 ? slotCount.value - 1 : empty\n let target: number | null = null\n switch (event.key) {\n case 'Backspace':\n // A read-only box refuses its own edits natively, but this erases ANOTHER box, which\n // nothing native guards.\n if (props.readonly || props.disabled || digits.value[slotIndex] || slotIndex === 0) return\n removeAt(slotIndex - 1)\n commit()\n target = slotIndex - 1\n break\n case 'ArrowLeft':\n if (slotIndex > 0) target = slotIndex - 1\n break\n case 'ArrowRight':\n if (slotIndex < reachable) target = slotIndex + 1\n break\n case 'Home':\n target = 0\n break\n case 'End':\n target = reachable\n break\n }\n if (target === null) return\n event.preventDefault()\n focusBox(target)\n}\n\n// Function refs are handed to the template ONCE per box: an inline one is a new function\n// on every render, which Vue answers with a null-then-element call on every keystroke.\nconst refSetters = new Map<number, (el: unknown) => void>()\nfunction inputRef(slot: number) {\n let set = refSetters.get(slot)\n if (!set) {\n set = (el) => {\n inputs.value[slot] = el as HTMLInputElement | null\n }\n refSetters.set(slot, set)\n }\n return set\n}\n/*\n * The same trio every other field of the size scale exposes. `focus` goes to the FIRST\n * EMPTY box rather than to the first box outright, which is where a reader resuming a\n * half-entered code expects to land.\n */\ndefineExpose({\n /** Moves the focus to the first empty box, or to the last one when the code is complete. */\n focus: (options?: FocusOptions) => {\n const empty = digits.value.findIndex((d) => !d)\n const target = empty === -1 ? slotCount.value - 1 : empty\n inputs.value[target]?.focus(options)\n },\n /** Selects the box the focus is on, as clicking into one already does. */\n select: () => {\n const active = inputs.value.find((el) => el === document.activeElement)\n ;(active ?? inputs.value[0])?.select()\n },\n /** The row itself, for what neither of the two above covers. */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-bind=\"groupAttrs\"\n class=\"v-input-otp v-control\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :data-invalid=\"invalid ? '' : undefined\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n >\n <div class=\"v-input-otp-boxes\">\n <template v-for=\"(cell, i) in cells\" :key=\"i\">\n <input\n v-if=\"cell.type === 'slot'\"\n :ref=\"inputRef(cell.slotIndex)\"\n type=\"text\"\n class=\"v-input-otp-input\"\n :inputmode=\"format === 'numeric' ? 'numeric' : 'text'\"\n :autocomplete=\"cell.slotIndex === 0 ? 'one-time-code' : 'off'\"\n :value=\"digits[cell.slotIndex]\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n :aria-label=\"m.inputOTP.slot(cell.slotIndex + 1, slotCount)\"\n :aria-invalid=\"invalid || undefined\"\n @input=\"onInput(cell.slotIndex, $event)\"\n @keydown=\"onKeydown(cell.slotIndex, $event)\"\n @focus=\"onFocus(cell.slotIndex, $event)\"\n />\n <!-- A separator from the pattern: shown, never focusable, and never part of the\n value. It is hidden from screen readers, each box already announcing its\n own position in the code. -->\n <span v-else class=\"v-input-otp-literal\" aria-hidden=\"true\">\n <VIcon v-if=\"separatorIcon\" v-bind=\"iconProps(separatorIcon)\" />\n <template v-else>{{ cell.char }}</template>\n </span>\n </template>\n </div>\n\n <!-- What a form reads: the code, under the consumer's name. Out of the tab order and\n hidden from assistive technology, the boxes being the control; a browser focusing\n it to report an invalid code is sent on to the box to fill. -->\n <input\n v-bind=\"nativeAttrs\"\n class=\"v-input-otp-native v-visually-hidden\"\n type=\"text\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n autocomplete=\"off\"\n :value=\"model\"\n :pattern=\"`.{${slotCount}}`\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n @focus=\"focusBox(firstEmpty() === -1 ? 0 : firstEmpty())\"\n />\n\n <VTypography v-if=\"hint\" :id=\"hintId\" variant=\"caption\" tone=\"muted\" class=\"v-input-otp-hint\">\n {{ hint }}\n </VTypography>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-input-otp {\n /*\n * The heights and the icon context come from the shared v-control class\n * (styles/control-size.css). The type is the one thing kept local, and set one or\n * two steps above the other fields: a single character has a whole square to\n * itself, and at the usual field size it would look lost in it.\n */\n --input-otp-font-size: var(--vectis-font-size-lg);\n\n /* A column, so the hint sits under the boxes rather than beside them; the row itself\n is the box below. `inline-flex` keeps the component's own width, and the start\n alignment stops the hint from stretching the row to its own length. */\n display: inline-flex;\n flex-direction: column;\n align-items: start;\n gap: var(--vectis-space-1);\n }\n\n /* A code reads left to right in every language, like the HH:MM of VTimeInput: mirrored\n under `dir=\"rtl\"`, the arrows would walk against the boxes and a `GT-###` pattern would\n be drawn backwards. */\n .v-input-otp-boxes {\n direction: ltr;\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n }\n\n .v-input-otp-input {\n /* The boxes are square: reading the same variable for both dimensions is what\n makes the size and the density scale them together. */\n width: var(--control-height);\n height: var(--control-height);\n text-align: center;\n background: var(--vectis-color-surface);\n color: var(--vectis-color-text);\n border: 1px solid var(--vectis-color-border-strong);\n border-radius: var(--vectis-radius-interactive);\n font-family: var(--vectis-text-family-code);\n font-size: var(--input-otp-font-size);\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n box-shadow var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n /* The states follow VInput's, in VInput's order: read-only, hover, focus, invalid,\n disabled. Read-only, focus, invalid and disabled all weigh (0,2,0), so the source order\n arbitrates between them and read-only has to come FIRST, or it would repaint the focus\n and error borders grey.\n\n A read-only row takes the sunken background and the lighter border of a read-only\n VInput, and the state is read from [data-readonly] rather than from `:read-only`, which\n the browser also matches on a disabled box. */\n .v-input-otp[data-readonly] .v-input-otp-input {\n background: var(--vectis-color-surface-sunken);\n border-color: var(--vectis-color-border);\n }\n\n /* The hover weighs (0,5,0) and keeps itself off a focused, invalid or disabled box\n through its own `:not()`, as VInput's does. */\n .v-input-otp:not([data-invalid]) .v-input-otp-input:hover:not(:focus, :disabled) {\n border-color: color-mix(\n in oklab,\n var(--vectis-color-border-strong),\n var(--vectis-color-text) 15%\n );\n }\n\n /* The focused box appears to have a two-pixel border, exactly as in VInput and\n VTextarea: its own 1px border plus a 1px shadow of the same colour just outside\n it. `:focus` rather than `:focus-visible`, like any text field, which shows its focus\n when clicked into too. The transparent outline is the safety net for Windows forced\n colours, which drop box-shadows entirely. */\n .v-input-otp-input:focus {\n border-color: var(--vectis-color-accent);\n box-shadow: 0 0 0 1px var(--vectis-color-accent);\n outline: var(--vectis-focus-ring-width) solid transparent;\n }\n\n .v-input-otp[data-invalid] .v-input-otp-input {\n border-color: var(--vectis-color-danger);\n }\n\n .v-input-otp[data-invalid] .v-input-otp-input:focus {\n box-shadow: 0 0 0 1px var(--vectis-color-danger);\n }\n\n .v-input-otp-literal {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--vectis-color-text-muted);\n font-family: var(--vectis-text-family-code);\n font-size: var(--input-otp-font-size);\n user-select: none;\n }\n\n /* A disabled row greys out through the colour tokens and never through opacity, the\n same treatment as VInput — `text-muted` inside the box, where `text-subtle` would fall\n under 4.5:1 against `surface-muted`, and `text-subtle` for what sits on the page. */\n .v-input-otp[data-disabled] .v-input-otp-input {\n background: var(--vectis-color-surface-muted);\n color: var(--vectis-color-text-muted);\n border-color: var(--vectis-color-border);\n cursor: not-allowed;\n }\n\n .v-input-otp[data-disabled] .v-input-otp-literal,\n .v-input-otp[data-disabled] .v-input-otp-hint {\n color: var(--vectis-color-text-subtle);\n }\n\n /* Of the whole size scale, only the raised type is restated here; the dimensions\n themselves come from v-control. */\n .v-input-otp[data-size='sm'] {\n --input-otp-font-size: var(--vectis-font-size-md);\n }\n\n .v-input-otp[data-size='lg'] {\n --input-otp-font-size: var(--vectis-font-size-xl);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-input-otp-input {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"VInputOTP.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VInputOTP/VInputOTP.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * Coordinate character inputs because HTML has no segmented code field. JavaScript filters\n * values, distributes paste and moves focus; pattern literals stay outside the model.\n */\n\nimport { computed, ref, useAttrs, useId, watch } from 'vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport type { IconSource } from '../VIcon/types'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { isDev } from '../../utils/env'\nimport { joinIds } from '../../utils/ids'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** The height of the boxes: 32, 40 or 48 pixels. */\nexport type InputOTPSize = 'sm' | 'md' | 'lg'\n\n/** Which characters the code is made of. */\nexport type InputOTPFormat = 'numeric' | 'alpha' | 'alphanumeric'\n\ninterface InputOTPProps {\n /** How many boxes the code has. It is ignored as soon as a `pattern` is given. */\n length?: number\n /**\n * Which characters the code is made of. It filters what can be typed or pasted, and\n * decides which keyboard a phone offers.\n */\n format?: InputOTPFormat\n /**\n * The shape of the code: each `#` is a box to fill, and every other character is a separator\n * shown between the boxes without ever being part of the value, as in `'GT-###'` or\n * `'###.###.###'`.\n */\n pattern?: string\n /** An icon drawn in place of every separator of the pattern. */\n separatorIcon?: IconSource\n /** The size of the boxes: 32, 40 or 48 pixels. */\n size?: InputOTPSize\n /** Takes 4px off the boxes, leaving the text and the icons as they are. */\n compact?: boolean\n /** Makes every box unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows the code without letting it be changed. The boxes keep their focus and the code can\n * still be selected and copied, which separates it from `disabled`.\n */\n readonly?: boolean\n /** Marks the code as wrong, which colours the boxes and tells assistive technology so. */\n invalid?: boolean\n /**\n * What screen readers announce for the row as a whole. It falls back to the design system\n * dictionary.\n */\n label?: string\n /**\n * A line of help under the boxes: where the code was sent, how long it lasts. It is\n * tied to the row for assistive technology, so it is read out along with the label.\n */\n hint?: string\n}\n\nconst props = withDefaults(defineProps<InputOTPProps>(), {\n length: 6,\n format: 'numeric',\n pattern: undefined,\n separatorIcon: undefined,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n label: undefined,\n hint: undefined,\n})\n\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.inputOTP.label)\n\n// @a11y\n/*\n * `aria-describedby` is a list, and the hint is appended to whatever the consumer already\n * pointed at instead of replacing it; left to the fallthrough, their value would land last and\n * take the hint out of the announcement with nothing to show for it.\n */\ndefineOptions({ inheritAttrs: false })\n\nconst attrs = useAttrs()\n\n// What the FORM reads goes on the hidden native input, and everything else on the group:\n// on the group a `name` submits nothing and `required` validates nothing, silently.\nconst NATIVE_ONLY = ['name', 'form', 'required']\nconst groupAttrs = computed(() =>\n Object.fromEntries(Object.entries(attrs).filter(([key]) => !NATIVE_ONLY.includes(key))),\n)\nconst nativeAttrs = computed(() =>\n Object.fromEntries(Object.entries(attrs).filter(([key]) => NATIVE_ONLY.includes(key))),\n)\n// No `useFieldIds` here: the row is named by `aria-label` and renders no `<label>`, so the\n// field id that composable generates would have nothing to point it at.\nconst hintId = useId()\nconst describedBy = computed(() =>\n joinIds(attrs['aria-describedby'] as string | undefined, !!props.hint && hintId),\n)\n\n/**\n * The code as one string, without the separators: a `GT-###` template still yields three\n * characters. It is empty to begin with, and shorter than the full length while it is being\n * typed.\n */\nconst model = defineModel<string>({ default: '' })\n\nconst emit = defineEmits<{\n /**\n * Emitted the moment the code BECOMES complete, carrying it. Retyping a character of a\n * complete code with the same one does not emit it again.\n */\n complete: [code: string]\n}>()\n\ntype Cell = { type: 'slot'; slotIndex: number } | { type: 'literal'; char: string }\n\nconst cells = computed<Cell[]>(() => {\n if (props.pattern?.includes('#')) {\n let slotIndex = 0\n return [...props.pattern].map((char): Cell =>\n char === '#' ? { type: 'slot', slotIndex: slotIndex++ } : { type: 'literal', char },\n )\n }\n return Array.from({ length: props.length }, (_, i) => ({ type: 'slot', slotIndex: i }))\n})\nconst slotCount = computed(() => cells.value.filter((cell) => cell.type === 'slot').length)\n\n// @devwarn\nif (isDev) {\n if (props.pattern && !props.pattern.includes('#'))\n console.warn(\"[VInputOTP] pattern without '#' — falling back to `length`.\")\n}\n\nconst filters: Record<NonNullable<InputOTPProps['format']>, RegExp> = {\n numeric: /[^0-9]/g,\n alpha: /[^A-Z]/g,\n alphanumeric: /[^A-Z0-9]/g,\n}\n\n/**\n * Keeps only what the format allows. Outside a numeric code the text is put in\n * capitals first, so that the value has a single canonical form whichever case the\n * reader typed or pasted.\n */\nfunction sanitize(text: string) {\n const upper = props.format === 'numeric' ? text : text.toUpperCase()\n return upper.replace(filters[props.format], '')\n}\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst digits = ref<string[]>([])\n\n// @keyboard\n// Read the rendered order on demand; changing a pattern can move or replace its input cells.\nfunction inputAt(slot: number) {\n return rootEl.value?.querySelectorAll<HTMLInputElement>('.v-input-otp-input')[slot]\n}\n\nfunction syncFromModel(value: string) {\n // A value longer than the row is simply shown cut short, and one holding characters the\n // format refuses is shown filtered; but the model is not rewritten to match: writing back\n // here would feed the watcher below and the two would keep correcting each other.\n const clean = sanitize(value)\n digits.value = Array.from({ length: slotCount.value }, (_, i) => clean[i] ?? '')\n}\nsyncFromModel(model.value)\nwatch([model, slotCount], ([value, count]) => {\n if (value !== digits.value.join('') || digits.value.length !== count) syncFromModel(value)\n})\n\n/** The first empty box, or -1 once the code is complete. Every box before it is filled. */\nconst firstEmpty = () => digits.value.findIndex((d) => !d)\n\n/** Takes a character out and moves the following ones back, so no gap is left behind. */\nfunction removeAt(slot: number) {\n digits.value.splice(slot, 1)\n digits.value.push('')\n}\n\nfunction focusBox(slot: number) {\n const el = inputAt(slot)\n el?.focus()\n // Selected, so the next keystroke REPLACES the character: a box that already has the\n // focus receives no focus event to select it.\n el?.select()\n}\n\nfunction commit() {\n const code = digits.value.join('')\n const previous = model.value\n model.value = code\n // Only when the code CHANGES into a full one: retyping a character of a complete code\n // must not submit it a second time for a consumer listening to `complete`.\n if (code.length === slotCount.value && code !== previous) emit('complete', code)\n}\n\n// @core\n/**\n * Spreads a run of characters across the boxes, starting from the one that received them. It\n * returns the last box it filled, so the caller knows where to put the focus, and `null` when\n * nothing in the text was usable.\n */\nfunction distribute(raw: string, startSlot: number): number | null {\n const allCells = cells.value\n let start = allCells.findIndex((cell) => cell.type === 'slot' && cell.slotIndex === startSlot)\n const chars = [...raw]\n // Never for ONE character: a `G` typed into the first box of `GT-###` is the code's first\n // character, not the separator, and swallowing it would make such a code impossible to type.\n if (chars.length > 1) while (start > 0 && allCells[start - 1]?.type === 'literal') start--\n let charIndex = 0\n let lastFilled: number | null = null\n for (const cell of allCells.slice(start)) {\n if (charIndex >= chars.length) break\n if (cell.type === 'literal') {\n if (chars[charIndex]?.toUpperCase() === cell.char.toUpperCase()) charIndex++\n continue\n }\n let char = ''\n while (charIndex < chars.length && !char) {\n char = sanitize(chars[charIndex] ?? '')\n charIndex++\n }\n if (!char) break\n digits.value[cell.slotIndex] = char\n lastFilled = cell.slotIndex\n }\n return lastFilled\n}\n\n// @keyboard @core\n// Separate inputs into something that types like one field; spreading the characters is the\n// core behaviour underneath it.\nfunction onInput(slotIndex: number, event: Event) {\n const el = event.target as HTMLInputElement\n const empty = firstEmpty()\n const filled = empty === -1 || slotIndex < empty\n\n // A filled box whose character was not selected receives the new one NEXT to it. What\n // was inserted is what lies just before the caret, and it is what counts.\n const previous = digits.value[slotIndex] ?? ''\n let raw = el.value\n const inserted = raw.length - previous.length\n if (previous && inserted > 0 && el.selectionStart !== null) {\n raw = raw.slice(Math.max(0, el.selectionStart - inserted), el.selectionStart)\n }\n\n // The same path serves a single keystroke and a pasted code: both are spread from this box\n // onwards; or from the first empty box, when this one lies past it.\n const lastFilled = distribute(raw, filled ? slotIndex : empty)\n if (lastFilled === null) {\n // Either the box was emptied, or nothing typed was valid for this format. A filled box\n // loses its character and the ones after it close the gap.\n if (filled) removeAt(slotIndex)\n el.value = digits.value[slotIndex] ?? ''\n commit()\n return\n }\n // Written by hand: when the characters went to another box, this one's value is still\n // '' in the render, and the patch would leave the typed character on screen.\n el.value = digits.value[slotIndex] ?? ''\n focusBox(Math.min(lastFilled + 1, slotCount.value - 1))\n commit()\n}\n\n// @keyboard\n// Sends the focus there instead: a pointer landing on box 5 of a two-character code types into\n// box 3.\nfunction onFocus(slotIndex: number, event: FocusEvent) {\n const empty = firstEmpty()\n if (empty !== -1 && slotIndex > empty) inputAt(empty)?.focus()\n else (event.target as HTMLInputElement).select()\n}\n\n// @keyboard\nfunction onKeydown(slotIndex: number, event: KeyboardEvent) {\n const empty = firstEmpty()\n // The furthest box the focus may reach: the first empty one, or the last of a full code.\n const reachable = empty === -1 ? slotCount.value - 1 : empty\n let target: number | null = null\n switch (event.key) {\n case 'Backspace':\n // A read-only box refuses its own edits natively, but this erases ANOTHER box, which\n // nothing native guards.\n if (props.readonly || props.disabled || digits.value[slotIndex] || slotIndex === 0) return\n removeAt(slotIndex - 1)\n commit()\n target = slotIndex - 1\n break\n case 'ArrowLeft':\n if (slotIndex > 0) target = slotIndex - 1\n break\n case 'ArrowRight':\n if (slotIndex < reachable) target = slotIndex + 1\n break\n case 'Home':\n target = 0\n break\n case 'End':\n target = reachable\n break\n }\n if (target === null) return\n event.preventDefault()\n focusBox(target)\n}\n\n/*\n * `focus` goes to the first empty box rather than to the first box outright, which is where a\n * reader resuming a half-entered code expects to land.\n */\ndefineExpose({\n /** Moves the focus to the first empty box, or to the last one when the code is complete. */\n focus: (options?: FocusOptions) => {\n const empty = digits.value.findIndex((d) => !d)\n const target = empty === -1 ? slotCount.value - 1 : empty\n inputAt(target)?.focus(options)\n },\n /** Selects the box the focus is on, as clicking into one already does. */\n select: () => {\n const active = rootEl.value?.querySelector<HTMLInputElement>('.v-input-otp-input:focus')\n ;(active ?? inputAt(0))?.select()\n },\n /** The row itself, for what neither of the two above covers. */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-bind=\"groupAttrs\"\n class=\"v-input-otp v-control\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :data-invalid=\"invalid ? '' : undefined\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n >\n <div class=\"v-input-otp-boxes\">\n <template v-for=\"(cell, i) in cells\" :key=\"i\">\n <input\n v-if=\"cell.type === 'slot'\"\n type=\"text\"\n class=\"v-input-otp-input\"\n :inputmode=\"format === 'numeric' ? 'numeric' : 'text'\"\n :autocomplete=\"cell.slotIndex === 0 ? 'one-time-code' : 'off'\"\n :value=\"digits[cell.slotIndex]\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n :aria-label=\"m.inputOTP.slot(cell.slotIndex + 1, slotCount)\"\n :aria-invalid=\"invalid || undefined\"\n @input=\"onInput(cell.slotIndex, $event)\"\n @keydown=\"onKeydown(cell.slotIndex, $event)\"\n @focus=\"onFocus(cell.slotIndex, $event)\"\n />\n <!-- A separator from the pattern: shown, never focusable, and never part of the\n value. It is hidden from screen readers, each box already announcing its\n own position in the code. -->\n <span v-else class=\"v-input-otp-literal\" aria-hidden=\"true\">\n <VIcon v-if=\"separatorIcon\" v-bind=\"iconProps(separatorIcon)\" />\n <template v-else>{{ cell.char }}</template>\n </span>\n </template>\n </div>\n\n <!--\n Out of the tab order and hidden from assistive technology, the boxes being the control; a\n browser focusing it to report an invalid code is sent on to the box to fill.\n -->\n <input\n v-bind=\"nativeAttrs\"\n class=\"v-input-otp-native v-visually-hidden\"\n type=\"text\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n autocomplete=\"off\"\n :value=\"model\"\n :pattern=\"`.{${slotCount}}`\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n @focus=\"focusBox(firstEmpty() === -1 ? 0 : firstEmpty())\"\n />\n\n <VTypography v-if=\"hint\" :id=\"hintId\" variant=\"caption\" tone=\"muted\" class=\"v-input-otp-hint\">\n {{ hint }}\n </VTypography>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-input-otp {\n /*\n * The heights and the icon context come from the shared v-control class\n * (styles/control-size.css). The type is the one thing kept local, and set one or\n * two steps above the other fields: a single character has a whole square to\n * itself, and at the usual field size it would look lost in it.\n */\n --input-otp-font-size: var(--vectis-font-size-lg);\n\n /* A column, so the hint sits under the boxes rather than beside them; the row itself\n is the box below. `inline-flex` keeps the component's own width, and the start\n alignment stops the hint from stretching the row to its own length. */\n display: inline-flex;\n flex-direction: column;\n align-items: start;\n gap: var(--vectis-space-1);\n }\n\n /* A code reads left to right in every language, like the HH:MM of VTimeInput: mirrored\n under `dir=\"rtl\"`, the arrows would walk against the boxes and a `GT-###` pattern would\n be drawn backwards. */\n .v-input-otp-boxes {\n direction: ltr;\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n }\n\n .v-input-otp-input {\n /* The boxes are square: reading the same variable for both dimensions is what\n makes the size and the density scale them together. */\n width: var(--control-height);\n height: var(--control-height);\n text-align: center;\n background: var(--vectis-color-surface);\n color: var(--vectis-color-text);\n border: 1px solid var(--vectis-color-border-strong);\n border-radius: var(--vectis-radius-interactive);\n font-family: var(--vectis-text-family-code);\n font-size: var(--input-otp-font-size);\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n box-shadow var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n /*\n * The states follow VInput's, in VInput's order: read-only, hover, focus, invalid, disabled.\n * Read-only, focus, invalid and disabled all weigh (0,2,0), so the source order arbitrates\n * between them and read-only has to come first, or it would repaint the focus and error\n * borders grey.\n */\n .v-input-otp[data-readonly] .v-input-otp-input {\n background: var(--vectis-color-surface-sunken);\n border-color: var(--vectis-color-border);\n }\n\n /* The hover weighs (0,5,0) and keeps itself off a focused, invalid or disabled box\n through its own `:not()`, as VInput's does. */\n .v-input-otp:not([data-invalid]) .v-input-otp-input:hover:not(:focus, :disabled) {\n border-color: color-mix(\n in oklab,\n var(--vectis-color-border-strong),\n var(--vectis-color-text) 15%\n );\n }\n\n /*\n * `:focus` rather than `:focus-visible`, like any text field, which shows its focus when\n * clicked into too. The transparent outline is the safety net for Windows forced colours,\n * which drop box-shadows entirely.\n */\n .v-input-otp-input:focus {\n border-color: var(--vectis-color-accent);\n box-shadow: 0 0 0 1px var(--vectis-color-accent);\n outline: var(--vectis-focus-ring-width) solid transparent;\n }\n\n .v-input-otp[data-invalid] .v-input-otp-input {\n border-color: var(--vectis-color-danger);\n }\n\n .v-input-otp[data-invalid] .v-input-otp-input:focus {\n box-shadow: 0 0 0 1px var(--vectis-color-danger);\n }\n\n .v-input-otp-literal {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--vectis-color-text-muted);\n font-family: var(--vectis-text-family-code);\n font-size: var(--input-otp-font-size);\n user-select: none;\n }\n\n /*\n * A disabled row greys out through the colour tokens and never through opacity, the same\n * treatment as VInput; `text-muted` inside the box, where `text-subtle` would fall under\n * 4.5:1 against `surface-muted`, and `text-subtle` for what sits on the page.\n */\n .v-input-otp[data-disabled] .v-input-otp-input {\n background: var(--vectis-color-surface-muted);\n color: var(--vectis-color-text-muted);\n border-color: var(--vectis-color-border);\n cursor: not-allowed;\n }\n\n .v-input-otp[data-disabled] .v-input-otp-literal,\n .v-input-otp[data-disabled] .v-input-otp-hint {\n color: var(--vectis-color-text-subtle);\n }\n\n /* Of the whole size scale, only the raised type is restated here; the dimensions\n themselves come from v-control. */\n .v-input-otp[data-size='sm'] {\n --input-otp-font-size: var(--vectis-font-size-md);\n }\n\n .v-input-otp[data-size='lg'] {\n --input-otp-font-size: var(--vectis-font-size-xl);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-input-otp-input {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"VMenu.js","names":[],"sources":["../../../src/components/VMenu/VMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * A menu of actions, opened by a button and closed as soon as one is chosen. It follows the\n * ARIA menu pattern, which is what tells a screen reader these are commands rather than\n * links or options.\n *\n * It rests on `popover=\"auto\"`: the panel light-dismisses without a line of code and\n * positions itself in CSS against its `popovertarget` invoker, which is its implicit anchor.\n *\n * The JS covers only what the platform does not — keeping `v-model:open` in step, moving\n * focus into the panel and handing it back to the trigger. The keyboard inside the panel\n * lives in VMenuPanel.\n *\n * HOW FAR the focus goes in depends on how the menu was opened: from the keyboard it settles\n * on the first command, ready to be chosen; by pointer it stops at the panel, so nothing is\n * singled out for someone who has not asked for a choice yet. The arrows still enter the\n * list from there.\n */\n\nimport { computed, provide, ref, useId } from 'vue'\n\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuInvoker, menuKey } from './context'\nimport type { MenuPlacement, MenuSize } from './context'\nimport { usePopoverModel } from '../../composables/usePopover'\nimport { isKeyboardFocus } from '../../utils/focus'\n\ninterface MenuProps {\n /**\n * Where the panel opens relative to its trigger. The browser moves it to another\n * side by itself when there is not enough room.\n */\n placement?: MenuPlacement\n /**\n * How tall the rows are: 32, 40 or 48 pixels. Submenus inherit it, so it is set\n * once on the menu as a whole.\n */\n size?: MenuSize\n /** Takes 4px off the height of every row, submenus included. */\n compact?: boolean\n /**\n * A width for the panel: a number is read as pixels, a string as any CSS length or\n * keyword (`16rem`, `max-content`). It applies to the menu itself; submenus keep the\n * default width.\n */\n width?: number | string\n /**\n * Stops the panel from being narrower than the button that opened it, while leaving\n * it free to grow wider for its content. Submenus are unaffected.\n */\n matchTrigger?: boolean\n}\n\n// Deliberately not assigned to a variable: the template reads the props directly.\nwithDefaults(defineProps<MenuProps>(), {\n placement: 'bottom-start',\n size: 'sm',\n compact: false,\n width: undefined,\n matchTrigger: false,\n})\n\n/**\n * Whether the menu is showing. It starts closed and is fed BY the panel, so the browser's\n * own dismissal (a click outside, Escape, choosing a command) writes back to it.\n */\nconst open = defineModel<boolean>('open', { default: false })\n\n/**\n * What the trigger has to carry: the link to the panel it opens, and the two\n * attributes telling assistive technology that a menu is attached to this button and\n * whether it is currently open.\n */\nexport type MenuTriggerProps = {\n popovertarget: string\n 'aria-haspopup': 'menu'\n 'aria-expanded': boolean\n 'aria-controls': string\n}\n\ndefineSlots<{\n /**\n * The button that opens the menu. Bind the `triggerProps` it receives onto it: that\n * is what wires the two together.\n */\n trigger(props: { triggerProps: MenuTriggerProps }): unknown\n /** The contents of the menu: VMenuItem, VMenuGroup and VMenuSeparator. */\n default(): unknown\n}>()\n\nconst panelRef = ref<InstanceType<typeof VMenuPanel> | null>(null)\nconst menuId = useId()\n\nconst triggerProps = computed<MenuTriggerProps>(() => ({\n popovertarget: menuId,\n 'aria-haspopup': 'menu',\n 'aria-expanded': open.value,\n 'aria-controls': menuId,\n}))\n\n// Closing this panel closes every submenu with it: they are rendered inside it, and\n// the browser closes a stack of popovers from the outside in.\nprovide(menuKey, { closeAll: () => panelRef.value?.close() })\n\n// @a11y\n// The focus half of the bridge: into the panel when it opens, back to the\n// trigger when it closes. Keeping the state in step alone would leave a keyboard user\n// stranded at the top of the page every time a menu opened or closed.\n//\n// WHERE it lands in the panel is the whole question, and the browser is the one that\n// answers it. In a menu the focus IS the highlight (see the rule in VMenuItem), so\n// taking the first item singles out a command — which is what someone arriving on the\n// arrows wants, and what someone who has just clicked never asked for. `:focus-visible`\n// is exactly that distinction, read off whatever holds the focus at this instant: the\n// trigger, since opening a popover through `popovertarget` does not move the focus.\n//\n// The pointer branch focuses the PANEL rather than nothing at all, and that is\n// structural: every key the menu answers to is listened for on the panel element, so\n// focus left outside it — on a trigger that is a DOM sibling — would silence the arrows,\n// Home/End and the Tab that closes the menu. Both ways of being wrong are mild and\n// symmetric: a misread pointer costs one extra keystroke, a misread keyboard gives back\n// the older behaviour.\nfunction onToggle(value: boolean) {\n open.value = value\n if (value) {\n if (isKeyboardFocus(document.activeElement)) panelRef.value?.focusFirst()\n else panelRef.value?.focusPanel()\n } else {\n // The browser's own dismissal — a click outside, Escape — leaves the focus\n // nowhere, on the page body. Only then is it handed back to the trigger: if the\n // focus has already moved somewhere else deliberately, it must be left alone.\n const active = document.activeElement\n if (!active || active === document.body || panelRef.value?.el?.contains(active)) {\n menuInvoker(menuId)?.focus()\n }\n }\n}\n\n/*\n * TRAP — every opening that does not come from a click on the trigger has to NAME that\n * trigger. A popover opened through `popovertarget` takes its invoker as its implicit\n * anchor; opened from code with no `source`, it has no anchor at all and the browser\n * paints it at the corner of the viewport. Nothing errors, and the panel is otherwise\n * perfectly functional there.\n *\n * The invoker is searched for rather than held: it is rendered by the consumer, in a\n * slot this component cannot reach into.\n */\nfunction openAtTrigger() {\n panelRef.value?.show(menuInvoker(menuId) ?? undefined)\n}\n\n// The open state is the panel's, read back from it rather than copied here: the panel has\n// already recorded the browser's answer by the time it reports the toggle.\nusePopoverModel(\n open,\n () => panelRef.value?.shown ?? false,\n openAtTrigger,\n () => panelRef.value?.close(),\n)\n\n/*\n * The same trio VDialog, VDialogAlert and VPopover answer. It is a relay onto the panel,\n * which has held it all along: opening a menu from code was otherwise only possible through\n * the model, which inserts a tick where a focus move cannot afford one.\n */\ndefineExpose({\n /** Opens the menu at once, without waiting for the model to come round. */\n show: openAtTrigger,\n /** Closes it at once, submenus included. */\n close: () => panelRef.value?.close(),\n /** The panel element, for what neither of the two above covers. */\n el: computed(() => panelRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <VMenuPanel\n :id=\"menuId\"\n ref=\"panelRef\"\n :placement=\"placement\"\n :size=\"size\"\n :compact=\"compact\"\n :width=\"width\"\n :match-trigger=\"matchTrigger\"\n @toggle=\"onToggle\"\n >\n <slot />\n </VMenuPanel>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"VMenu.js","names":[],"sources":["../../../src/components/VMenu/VMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Native auto popovers own light dismissal and anchoring. JavaScript supplies the model bridge\n * and ARIA menu focus management.\n */\n\nimport { computed, provide, ref, useId } from 'vue'\n\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuInvoker, menuKey } from './context'\nimport type { MenuPlacement, MenuSize } from './context'\nimport { usePopoverModel } from '../../composables/usePopover'\nimport { isKeyboardFocus } from '../../utils/focus'\n\ninterface MenuProps {\n /**\n * Where the panel opens relative to its trigger. The browser moves it to another\n * side by itself when there is not enough room.\n */\n placement?: MenuPlacement\n /**\n * How tall the rows are: 32, 40 or 48 pixels. Submenus inherit it, so it is set\n * once on the menu as a whole.\n */\n size?: MenuSize\n /** Takes 4px off the height of every row, submenus included. */\n compact?: boolean\n /**\n * A width for the panel: a number is read as pixels, a string as any CSS length or\n * keyword (`16rem`, `max-content`). It applies to the menu itself; submenus keep the\n * default width.\n */\n width?: number | string\n /**\n * Stops the panel from being narrower than the button that opened it, while leaving\n * it free to grow wider for its content. Submenus are unaffected.\n */\n matchTrigger?: boolean\n}\n\nwithDefaults(defineProps<MenuProps>(), {\n placement: 'bottom-start',\n size: 'sm',\n compact: false,\n width: undefined,\n matchTrigger: false,\n})\n\n/**\n * Whether the menu is showing. It starts closed and is fed BY the panel, so the browser's\n * own dismissal (a click outside, Escape, choosing a command) writes back to it.\n */\nconst open = defineModel<boolean>('open', { default: false })\n\n/**\n * What the trigger has to carry: the link to the panel it opens, and the two\n * attributes telling assistive technology that a menu is attached to this button and\n * whether it is currently open.\n */\nexport type MenuTriggerProps = {\n popovertarget: string\n 'aria-haspopup': 'menu'\n 'aria-expanded': boolean\n 'aria-controls': string\n}\n\ndefineSlots<{\n /**\n * The button that opens the menu. Bind the `triggerProps` it receives onto it: that\n * is what wires the two together.\n */\n trigger(props: { triggerProps: MenuTriggerProps }): unknown\n /** The contents of the menu: VMenuItem, VMenuGroup and VMenuSeparator. */\n default(): unknown\n}>()\n\nconst panelRef = ref<InstanceType<typeof VMenuPanel> | null>(null)\nconst menuId = useId()\n\nconst triggerProps = computed<MenuTriggerProps>(() => ({\n popovertarget: menuId,\n 'aria-haspopup': 'menu',\n 'aria-expanded': open.value,\n 'aria-controls': menuId,\n}))\n\n// Closing this panel closes every submenu with it: they are rendered inside it, and\n// the browser closes a stack of popovers from the outside in.\nprovide(menuKey, { closeAll: () => panelRef.value?.close() })\n\n// @a11y\n// The focus half of the bridge: into the panel when it opens, back to the trigger when it\n// closes. Keeping the state in step alone would leave a keyboard user stranded at the top of\n// the page every time a menu opened or closed.\nfunction onToggle(value: boolean) {\n open.value = value\n if (value) {\n if (isKeyboardFocus(document.activeElement)) panelRef.value?.focusFirst()\n else panelRef.value?.focusPanel()\n } else {\n // The browser's own dismissal; a click outside, Escape; leaves the focus nowhere, on the\n // page body. Only then is it handed back to the trigger: if the focus has already moved\n // somewhere else deliberately, it must be left alone.\n const active = document.activeElement\n if (!active || active === document.body || panelRef.value?.el?.contains(active)) {\n menuInvoker(menuId)?.focus()\n }\n }\n}\n\n/*\n * Every opening that does not come from a click on the trigger has to NAME that trigger. A\n * popover opened through `popovertarget` takes its invoker as its implicit anchor; opened from\n * code with no `source`, it has no anchor at all and the browser paints it at the corner of the\n * viewport.\n */\nfunction openAtTrigger() {\n panelRef.value?.show(menuInvoker(menuId) ?? undefined)\n}\n\n// The open state is the panel's, read back from it rather than copied here: the panel has\n// already recorded the browser's answer by the time it reports the toggle.\nusePopoverModel(\n open,\n () => panelRef.value?.shown ?? false,\n openAtTrigger,\n () => panelRef.value?.close(),\n)\n\n/*\n * Relay the imperative panel API so programmatic opening can move focus immediately without\n * waiting for a model render.\n */\ndefineExpose({\n /** Opens the menu at once, without waiting for the model to come round. */\n show: openAtTrigger,\n /** Closes it at once, submenus included. */\n close: () => panelRef.value?.close(),\n /** The panel element, for what neither of the two above covers. */\n el: computed(() => panelRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <VMenuPanel\n :id=\"menuId\"\n ref=\"panelRef\"\n :placement=\"placement\"\n :size=\"size\"\n :compact=\"compact\"\n :width=\"width\"\n :match-trigger=\"matchTrigger\"\n @toggle=\"onToggle\"\n >\n <slot />\n </VMenuPanel>\n</template>\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"VMenu.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VMenu/VMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * A menu of actions, opened by a button and closed as soon as one is chosen. It follows the\n * ARIA menu pattern, which is what tells a screen reader these are commands rather than\n * links or options.\n *\n * It rests on `popover=\"auto\"`: the panel light-dismisses without a line of code and\n * positions itself in CSS against its `popovertarget` invoker, which is its implicit anchor.\n *\n * The JS covers only what the platform does not — keeping `v-model:open` in step, moving\n * focus into the panel and handing it back to the trigger. The keyboard inside the panel\n * lives in VMenuPanel.\n *\n * HOW FAR the focus goes in depends on how the menu was opened: from the keyboard it settles\n * on the first command, ready to be chosen; by pointer it stops at the panel, so nothing is\n * singled out for someone who has not asked for a choice yet. The arrows still enter the\n * list from there.\n */\n\nimport { computed, provide, ref, useId } from 'vue'\n\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuInvoker, menuKey } from './context'\nimport type { MenuPlacement, MenuSize } from './context'\nimport { usePopoverModel } from '../../composables/usePopover'\nimport { isKeyboardFocus } from '../../utils/focus'\n\ninterface MenuProps {\n /**\n * Where the panel opens relative to its trigger. The browser moves it to another\n * side by itself when there is not enough room.\n */\n placement?: MenuPlacement\n /**\n * How tall the rows are: 32, 40 or 48 pixels. Submenus inherit it, so it is set\n * once on the menu as a whole.\n */\n size?: MenuSize\n /** Takes 4px off the height of every row, submenus included. */\n compact?: boolean\n /**\n * A width for the panel: a number is read as pixels, a string as any CSS length or\n * keyword (`16rem`, `max-content`). It applies to the menu itself; submenus keep the\n * default width.\n */\n width?: number | string\n /**\n * Stops the panel from being narrower than the button that opened it, while leaving\n * it free to grow wider for its content. Submenus are unaffected.\n */\n matchTrigger?: boolean\n}\n\n// Deliberately not assigned to a variable: the template reads the props directly.\nwithDefaults(defineProps<MenuProps>(), {\n placement: 'bottom-start',\n size: 'sm',\n compact: false,\n width: undefined,\n matchTrigger: false,\n})\n\n/**\n * Whether the menu is showing. It starts closed and is fed BY the panel, so the browser's\n * own dismissal (a click outside, Escape, choosing a command) writes back to it.\n */\nconst open = defineModel<boolean>('open', { default: false })\n\n/**\n * What the trigger has to carry: the link to the panel it opens, and the two\n * attributes telling assistive technology that a menu is attached to this button and\n * whether it is currently open.\n */\nexport type MenuTriggerProps = {\n popovertarget: string\n 'aria-haspopup': 'menu'\n 'aria-expanded': boolean\n 'aria-controls': string\n}\n\ndefineSlots<{\n /**\n * The button that opens the menu. Bind the `triggerProps` it receives onto it: that\n * is what wires the two together.\n */\n trigger(props: { triggerProps: MenuTriggerProps }): unknown\n /** The contents of the menu: VMenuItem, VMenuGroup and VMenuSeparator. */\n default(): unknown\n}>()\n\nconst panelRef = ref<InstanceType<typeof VMenuPanel> | null>(null)\nconst menuId = useId()\n\nconst triggerProps = computed<MenuTriggerProps>(() => ({\n popovertarget: menuId,\n 'aria-haspopup': 'menu',\n 'aria-expanded': open.value,\n 'aria-controls': menuId,\n}))\n\n// Closing this panel closes every submenu with it: they are rendered inside it, and\n// the browser closes a stack of popovers from the outside in.\nprovide(menuKey, { closeAll: () => panelRef.value?.close() })\n\n// @a11y\n// The focus half of the bridge: into the panel when it opens, back to the\n// trigger when it closes. Keeping the state in step alone would leave a keyboard user\n// stranded at the top of the page every time a menu opened or closed.\n//\n// WHERE it lands in the panel is the whole question, and the browser is the one that\n// answers it. In a menu the focus IS the highlight (see the rule in VMenuItem), so\n// taking the first item singles out a command — which is what someone arriving on the\n// arrows wants, and what someone who has just clicked never asked for. `:focus-visible`\n// is exactly that distinction, read off whatever holds the focus at this instant: the\n// trigger, since opening a popover through `popovertarget` does not move the focus.\n//\n// The pointer branch focuses the PANEL rather than nothing at all, and that is\n// structural: every key the menu answers to is listened for on the panel element, so\n// focus left outside it — on a trigger that is a DOM sibling — would silence the arrows,\n// Home/End and the Tab that closes the menu. Both ways of being wrong are mild and\n// symmetric: a misread pointer costs one extra keystroke, a misread keyboard gives back\n// the older behaviour.\nfunction onToggle(value: boolean) {\n open.value = value\n if (value) {\n if (isKeyboardFocus(document.activeElement)) panelRef.value?.focusFirst()\n else panelRef.value?.focusPanel()\n } else {\n // The browser's own dismissal — a click outside, Escape — leaves the focus\n // nowhere, on the page body. Only then is it handed back to the trigger: if the\n // focus has already moved somewhere else deliberately, it must be left alone.\n const active = document.activeElement\n if (!active || active === document.body || panelRef.value?.el?.contains(active)) {\n menuInvoker(menuId)?.focus()\n }\n }\n}\n\n/*\n * TRAP — every opening that does not come from a click on the trigger has to NAME that\n * trigger. A popover opened through `popovertarget` takes its invoker as its implicit\n * anchor; opened from code with no `source`, it has no anchor at all and the browser\n * paints it at the corner of the viewport. Nothing errors, and the panel is otherwise\n * perfectly functional there.\n *\n * The invoker is searched for rather than held: it is rendered by the consumer, in a\n * slot this component cannot reach into.\n */\nfunction openAtTrigger() {\n panelRef.value?.show(menuInvoker(menuId) ?? undefined)\n}\n\n// The open state is the panel's, read back from it rather than copied here: the panel has\n// already recorded the browser's answer by the time it reports the toggle.\nusePopoverModel(\n open,\n () => panelRef.value?.shown ?? false,\n openAtTrigger,\n () => panelRef.value?.close(),\n)\n\n/*\n * The same trio VDialog, VDialogAlert and VPopover answer. It is a relay onto the panel,\n * which has held it all along: opening a menu from code was otherwise only possible through\n * the model, which inserts a tick where a focus move cannot afford one.\n */\ndefineExpose({\n /** Opens the menu at once, without waiting for the model to come round. */\n show: openAtTrigger,\n /** Closes it at once, submenus included. */\n close: () => panelRef.value?.close(),\n /** The panel element, for what neither of the two above covers. */\n el: computed(() => panelRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <VMenuPanel\n :id=\"menuId\"\n ref=\"panelRef\"\n :placement=\"placement\"\n :size=\"size\"\n :compact=\"compact\"\n :width=\"width\"\n :match-trigger=\"matchTrigger\"\n @toggle=\"onToggle\"\n >\n <slot />\n </VMenuPanel>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmEA,IAAM,IAAO,EAAoB,GAAC,MAA0B,GAwBtD,IAAW,EAA4C,IAAI,GAC3D,IAAS,EAAM,GAEf,IAAe,SAAkC;GACrD,eAAe;GACf,iBAAiB;GACjB,iBAAiB,EAAK;GACtB,iBAAiB;EACnB,EAAE;EAIF,EAAQ,GAAS,EAAE,gBAAgB,EAAS,OAAO,MAAM,EAAE,CAAC;EAoB5D,SAAS,EAAS,GAAgB;GAEhC,IADA,EAAK,QAAQ,GACT,GACF,AAAI,EAAgB,SAAS,aAAa,IAAG,EAAS,OAAO,WAAW,IACnE,EAAS,OAAO,WAAW;QAC3B;IAIL,IAAM,IAAS,SAAS;IACxB,CAAI,CAAC,KAAU,MAAW,SAAS,QAAQ,EAAS,OAAO,IAAI,SAAS,CAAM,MAC5E,EAAY,CAAM,CAAC,EAAE,MAAM;GAE/B;EACF;EAYA,SAAS,IAAgB;GACvB,EAAS,OAAO,KAAK,EAAY,CAAM,KAAK,KAAA,CAAS;EACvD;SAIA,EACE,SACM,EAAS,OAAO,SAAS,IAC/B,SACM,EAAS,OAAO,MAAM,CAC9B,GAOA,EAAa;GAEX,MAAM;GAEN,aAAa,EAAS,OAAO,MAAM;GAEnC,IAAI,QAAe,EAAS,OAAO,MAAM,IAAI;EAC/C,CAAC,+BAIC,EAAqD,EAAA,QAAA,WAAA,EAA/B,cAAe,EAAA,MAAY,CAAA,GACjD,EAWa,GAAA;GAVV,IAAI,EAAA,CAAA;YACD;GAAJ,KAAI;GACH,WAAW,EAAA;GACX,MAAM,EAAA;GACN,SAAS,EAAA;GACT,OAAO,EAAA;GACP,iBAAe,EAAA;GACP;;oBAED,CAAR,EAAQ,EAAA,QAAA,SAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"VMenu.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VMenu/VMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Native auto popovers own light dismissal and anchoring. JavaScript supplies the model bridge\n * and ARIA menu focus management.\n */\n\nimport { computed, provide, ref, useId } from 'vue'\n\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuInvoker, menuKey } from './context'\nimport type { MenuPlacement, MenuSize } from './context'\nimport { usePopoverModel } from '../../composables/usePopover'\nimport { isKeyboardFocus } from '../../utils/focus'\n\ninterface MenuProps {\n /**\n * Where the panel opens relative to its trigger. The browser moves it to another\n * side by itself when there is not enough room.\n */\n placement?: MenuPlacement\n /**\n * How tall the rows are: 32, 40 or 48 pixels. Submenus inherit it, so it is set\n * once on the menu as a whole.\n */\n size?: MenuSize\n /** Takes 4px off the height of every row, submenus included. */\n compact?: boolean\n /**\n * A width for the panel: a number is read as pixels, a string as any CSS length or\n * keyword (`16rem`, `max-content`). It applies to the menu itself; submenus keep the\n * default width.\n */\n width?: number | string\n /**\n * Stops the panel from being narrower than the button that opened it, while leaving\n * it free to grow wider for its content. Submenus are unaffected.\n */\n matchTrigger?: boolean\n}\n\nwithDefaults(defineProps<MenuProps>(), {\n placement: 'bottom-start',\n size: 'sm',\n compact: false,\n width: undefined,\n matchTrigger: false,\n})\n\n/**\n * Whether the menu is showing. It starts closed and is fed BY the panel, so the browser's\n * own dismissal (a click outside, Escape, choosing a command) writes back to it.\n */\nconst open = defineModel<boolean>('open', { default: false })\n\n/**\n * What the trigger has to carry: the link to the panel it opens, and the two\n * attributes telling assistive technology that a menu is attached to this button and\n * whether it is currently open.\n */\nexport type MenuTriggerProps = {\n popovertarget: string\n 'aria-haspopup': 'menu'\n 'aria-expanded': boolean\n 'aria-controls': string\n}\n\ndefineSlots<{\n /**\n * The button that opens the menu. Bind the `triggerProps` it receives onto it: that\n * is what wires the two together.\n */\n trigger(props: { triggerProps: MenuTriggerProps }): unknown\n /** The contents of the menu: VMenuItem, VMenuGroup and VMenuSeparator. */\n default(): unknown\n}>()\n\nconst panelRef = ref<InstanceType<typeof VMenuPanel> | null>(null)\nconst menuId = useId()\n\nconst triggerProps = computed<MenuTriggerProps>(() => ({\n popovertarget: menuId,\n 'aria-haspopup': 'menu',\n 'aria-expanded': open.value,\n 'aria-controls': menuId,\n}))\n\n// Closing this panel closes every submenu with it: they are rendered inside it, and\n// the browser closes a stack of popovers from the outside in.\nprovide(menuKey, { closeAll: () => panelRef.value?.close() })\n\n// @a11y\n// The focus half of the bridge: into the panel when it opens, back to the trigger when it\n// closes. Keeping the state in step alone would leave a keyboard user stranded at the top of\n// the page every time a menu opened or closed.\nfunction onToggle(value: boolean) {\n open.value = value\n if (value) {\n if (isKeyboardFocus(document.activeElement)) panelRef.value?.focusFirst()\n else panelRef.value?.focusPanel()\n } else {\n // The browser's own dismissal; a click outside, Escape; leaves the focus nowhere, on the\n // page body. Only then is it handed back to the trigger: if the focus has already moved\n // somewhere else deliberately, it must be left alone.\n const active = document.activeElement\n if (!active || active === document.body || panelRef.value?.el?.contains(active)) {\n menuInvoker(menuId)?.focus()\n }\n }\n}\n\n/*\n * Every opening that does not come from a click on the trigger has to NAME that trigger. A\n * popover opened through `popovertarget` takes its invoker as its implicit anchor; opened from\n * code with no `source`, it has no anchor at all and the browser paints it at the corner of the\n * viewport.\n */\nfunction openAtTrigger() {\n panelRef.value?.show(menuInvoker(menuId) ?? undefined)\n}\n\n// The open state is the panel's, read back from it rather than copied here: the panel has\n// already recorded the browser's answer by the time it reports the toggle.\nusePopoverModel(\n open,\n () => panelRef.value?.shown ?? false,\n openAtTrigger,\n () => panelRef.value?.close(),\n)\n\n/*\n * Relay the imperative panel API so programmatic opening can move focus immediately without\n * waiting for a model render.\n */\ndefineExpose({\n /** Opens the menu at once, without waiting for the model to come round. */\n show: openAtTrigger,\n /** Closes it at once, submenus included. */\n close: () => panelRef.value?.close(),\n /** The panel element, for what neither of the two above covers. */\n el: computed(() => panelRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <VMenuPanel\n :id=\"menuId\"\n ref=\"panelRef\"\n :placement=\"placement\"\n :size=\"size\"\n :compact=\"compact\"\n :width=\"width\"\n :match-trigger=\"matchTrigger\"\n @toggle=\"onToggle\"\n >\n <slot />\n </VMenuPanel>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqDA,IAAM,IAAO,EAAoB,GAAC,MAA0B,GAwBtD,IAAW,EAA4C,IAAI,GAC3D,IAAS,EAAM,GAEf,IAAe,SAAkC;GACrD,eAAe;GACf,iBAAiB;GACjB,iBAAiB,EAAK;GACtB,iBAAiB;EACnB,EAAE;EAIF,EAAQ,GAAS,EAAE,gBAAgB,EAAS,OAAO,MAAM,EAAE,CAAC;EAM5D,SAAS,EAAS,GAAgB;GAEhC,IADA,EAAK,QAAQ,GACT,GACF,AAAI,EAAgB,SAAS,aAAa,IAAG,EAAS,OAAO,WAAW,IACnE,EAAS,OAAO,WAAW;QAC3B;IAIL,IAAM,IAAS,SAAS;IACxB,CAAI,CAAC,KAAU,MAAW,SAAS,QAAQ,EAAS,OAAO,IAAI,SAAS,CAAM,MAC5E,EAAY,CAAM,CAAC,EAAE,MAAM;GAE/B;EACF;EAQA,SAAS,IAAgB;GACvB,EAAS,OAAO,KAAK,EAAY,CAAM,KAAK,KAAA,CAAS;EACvD;SAIA,EACE,SACM,EAAS,OAAO,SAAS,IAC/B,SACM,EAAS,OAAO,MAAM,CAC9B,GAMA,EAAa;GAEX,MAAM;GAEN,aAAa,EAAS,OAAO,MAAM;GAEnC,IAAI,QAAe,EAAS,OAAO,MAAM,IAAI;EAC/C,CAAC,+BAIC,EAAqD,EAAA,QAAA,WAAA,EAA/B,cAAe,EAAA,MAAY,CAAA,GACjD,EAWa,GAAA;GAVV,IAAI,EAAA,CAAA;YACD;GAAJ,KAAI;GACH,WAAW,EAAA;GACX,MAAM,EAAA;GACN,SAAS,EAAA;GACT,OAAO,EAAA;GACP,iBAAe,EAAA;GACP;;oBAED,CAAR,EAAQ,EAAA,QAAA,SAAA,CAAA,CAAA"}
@@ -1,10 +1,5 @@
1
1
  interface MenuGroupProps {
2
- /**
3
- * The name of the section, replaced by the `#label` slot. One of the two is REQUIRED:
4
- * it is what names the group, and a section named by neither points its
5
- * `aria-labelledby` at an empty element. It is a heading, not a command: nothing
6
- * happens on click.
7
- */
2
+ /** The name of the section, replaced by the `#label` slot. */
8
3
  label?: string;
9
4
  }
10
5
  type __VLS_Slots = {
@@ -1 +1 @@
1
- {"version":3,"file":"VMenuGroup.js","names":[],"sources":["../../../src/components/VMenu/VMenuGroup.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * A named section inside a menu: \"Sort by\", \"Recent files\". The name is announced as\n * the group's label rather than being read as one more item, and it is plain text: it\n * cannot be chosen, and the arrow keys walk straight past it.\n */\n\nimport { useId, useSlots, watchEffect } from 'vue'\n\nimport { isDev } from '../../utils/env'\n\ninterface MenuGroupProps {\n /**\n * The name of the section, replaced by the `#label` slot. One of the two is REQUIRED:\n * it is what names the group, and a section named by neither points its\n * `aria-labelledby` at an empty element. It is a heading, not a command: nothing\n * happens on click.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<MenuGroupProps>(), {\n label: undefined,\n})\n\ndefineSlots<{\n /** The commands belonging to this section. */\n default(): unknown\n /** A name made of markup, replacing the `label` prop. */\n label?(): unknown\n}>()\n\n// @a11y @ssr\n// The generated id ties the group to its own label, and matches between server and\n// client by construction, Vue's `useId` guaranteeing exactly that.\nconst labelId = useId()\n\n// @devwarn\n// A group named by neither points its `aria-labelledby` at an empty element, and a\n// screen reader announces an unnamed group with nothing on screen to say so.\nif (isDev) {\n const slots = useSlots()\n watchEffect(() => {\n if (!props.label && !slots.label)\n console.warn('[VMenuGroup] needs a name: give it a `label` or a `#label` slot.')\n })\n}\n</script>\n\n<template>\n <div role=\"group\" class=\"v-menu-group\" :aria-labelledby=\"labelId\">\n <span :id=\"labelId\" class=\"v-menu-group-label\"\n ><slot name=\"label\">{{ label }}</slot></span\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-group {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n }\n\n .v-menu-group-label {\n display: flex;\n align-items: center;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n font-size: var(--vectis-text-overline-size);\n font-weight: var(--vectis-text-overline-weight);\n letter-spacing: var(--vectis-text-overline-tracking);\n color: var(--vectis-color-text-muted);\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VMenuGroup.js","names":[],"sources":["../../../src/components/VMenu/VMenuGroup.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useId, useSlots, watchEffect } from 'vue'\n\nimport { isDev } from '../../utils/env'\n\ninterface MenuGroupProps {\n /** The name of the section, replaced by the `#label` slot. */\n label?: string\n}\n\nconst props = withDefaults(defineProps<MenuGroupProps>(), {\n label: undefined,\n})\n\ndefineSlots<{\n /** The commands belonging to this section. */\n default(): unknown\n /** A name made of markup, replacing the `label` prop. */\n label?(): unknown\n}>()\n\n// @a11y @ssr\n// The generated id ties the group to its own label, and matches between server and\n// client by construction, Vue's `useId` guaranteeing exactly that.\nconst labelId = useId()\n\n// @devwarn\n// A group named by neither points its `aria-labelledby` at an empty element, and a\n// screen reader announces an unnamed group with nothing on screen to say so.\nif (isDev) {\n const slots = useSlots()\n watchEffect(() => {\n if (!props.label && !slots.label)\n console.warn('[VMenuGroup] needs a name: give it a `label` or a `#label` slot.')\n })\n}\n</script>\n\n<template>\n <div role=\"group\" class=\"v-menu-group\" :aria-labelledby=\"labelId\">\n <span :id=\"labelId\" class=\"v-menu-group-label\"\n ><slot name=\"label\">{{ label }}</slot></span\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-group {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n }\n\n .v-menu-group-label {\n display: flex;\n align-items: center;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n font-size: var(--vectis-text-overline-size);\n font-weight: var(--vectis-text-overline-weight);\n letter-spacing: var(--vectis-text-overline-tracking);\n color: var(--vectis-color-text-muted);\n }\n}\n</style>\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"VMenuGroup.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VMenu/VMenuGroup.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * A named section inside a menu: \"Sort by\", \"Recent files\". The name is announced as\n * the group's label rather than being read as one more item, and it is plain text: it\n * cannot be chosen, and the arrow keys walk straight past it.\n */\n\nimport { useId, useSlots, watchEffect } from 'vue'\n\nimport { isDev } from '../../utils/env'\n\ninterface MenuGroupProps {\n /**\n * The name of the section, replaced by the `#label` slot. One of the two is REQUIRED:\n * it is what names the group, and a section named by neither points its\n * `aria-labelledby` at an empty element. It is a heading, not a command: nothing\n * happens on click.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<MenuGroupProps>(), {\n label: undefined,\n})\n\ndefineSlots<{\n /** The commands belonging to this section. */\n default(): unknown\n /** A name made of markup, replacing the `label` prop. */\n label?(): unknown\n}>()\n\n// @a11y @ssr\n// The generated id ties the group to its own label, and matches between server and\n// client by construction, Vue's `useId` guaranteeing exactly that.\nconst labelId = useId()\n\n// @devwarn\n// A group named by neither points its `aria-labelledby` at an empty element, and a\n// screen reader announces an unnamed group with nothing on screen to say so.\nif (isDev) {\n const slots = useSlots()\n watchEffect(() => {\n if (!props.label && !slots.label)\n console.warn('[VMenuGroup] needs a name: give it a `label` or a `#label` slot.')\n })\n}\n</script>\n\n<template>\n <div role=\"group\" class=\"v-menu-group\" :aria-labelledby=\"labelId\">\n <span :id=\"labelId\" class=\"v-menu-group-label\"\n ><slot name=\"label\">{{ label }}</slot></span\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-group {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n }\n\n .v-menu-group-label {\n display: flex;\n align-items: center;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n font-size: var(--vectis-text-overline-size);\n font-weight: var(--vectis-text-overline-weight);\n letter-spacing: var(--vectis-text-overline-tracking);\n color: var(--vectis-color-text-muted);\n }\n}\n</style>\n"],"mappings":";;;;;;;EAqBA,IAAM,IAAQ,GAcR,IAAU,EAAM;EAKtB,IAAI,GAAO;GACT,IAAM,IAAQ,EAAS;GACvB,QAAkB;IAChB,AAAI,CAAC,EAAM,SAAS,CAAC,EAAM,SACzB,QAAQ,KAAK,kEAAkE;GACnF,CAAC;EACH;yBAIE,EAKM,OAAA;GALD,MAAK;GAAQ,OAAM;GAAgB,mBAAiB,EAAA,CAAA;MACvD,EAEC,QAAA;GAFM,IAAI,EAAA,CAAA;GAAS,OAAM;MACvB,EAAqC,EAAA,QAAA,SAAA,CAAA,SAAA,CAAA,EAAA,EAAf,EAAA,KAAK,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,GAAA,CAAA,GAE9B,EAAQ,EAAA,QAAA,SAAA,CAAA,GAAA,GAAA,CAAA"}
1
+ {"version":3,"file":"VMenuGroup.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VMenu/VMenuGroup.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useId, useSlots, watchEffect } from 'vue'\n\nimport { isDev } from '../../utils/env'\n\ninterface MenuGroupProps {\n /** The name of the section, replaced by the `#label` slot. */\n label?: string\n}\n\nconst props = withDefaults(defineProps<MenuGroupProps>(), {\n label: undefined,\n})\n\ndefineSlots<{\n /** The commands belonging to this section. */\n default(): unknown\n /** A name made of markup, replacing the `label` prop. */\n label?(): unknown\n}>()\n\n// @a11y @ssr\n// The generated id ties the group to its own label, and matches between server and\n// client by construction, Vue's `useId` guaranteeing exactly that.\nconst labelId = useId()\n\n// @devwarn\n// A group named by neither points its `aria-labelledby` at an empty element, and a\n// screen reader announces an unnamed group with nothing on screen to say so.\nif (isDev) {\n const slots = useSlots()\n watchEffect(() => {\n if (!props.label && !slots.label)\n console.warn('[VMenuGroup] needs a name: give it a `label` or a `#label` slot.')\n })\n}\n</script>\n\n<template>\n <div role=\"group\" class=\"v-menu-group\" :aria-labelledby=\"labelId\">\n <span :id=\"labelId\" class=\"v-menu-group-label\"\n ><slot name=\"label\">{{ label }}</slot></span\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-group {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n }\n\n .v-menu-group-label {\n display: flex;\n align-items: center;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n font-size: var(--vectis-text-overline-size);\n font-weight: var(--vectis-text-overline-weight);\n letter-spacing: var(--vectis-text-overline-tracking);\n color: var(--vectis-color-text-muted);\n }\n}\n</style>\n"],"mappings":";;;;;;;EAUA,IAAM,IAAQ,GAcR,IAAU,EAAM;EAKtB,IAAI,GAAO;GACT,IAAM,IAAQ,EAAS;GACvB,QAAkB;IAChB,AAAI,CAAC,EAAM,SAAS,CAAC,EAAM,SACzB,QAAQ,KAAK,kEAAkE;GACnF,CAAC;EACH;yBAIE,EAKM,OAAA;GALD,MAAK;GAAQ,OAAM;GAAgB,mBAAiB,EAAA,CAAA;MACvD,EAEC,QAAA;GAFM,IAAI,EAAA,CAAA;GAAS,OAAM;MACvB,EAAqC,EAAA,QAAA,SAAA,CAAA,SAAA,CAAA,EAAA,EAAf,EAAA,KAAK,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,GAAA,CAAA,GAE9B,EAAQ,EAAA,QAAA,SAAA,CAAA,GAAA,GAAA,CAAA"}
@@ -24,13 +24,8 @@ interface MenuItemProps {
24
24
  /**
25
25
  * What the command means, in colour. `danger` marks it destructive (deleting something
26
26
  * belongs there), and `neutral`, the default, is every other command.
27
- *
28
- * A row is an action, so it takes the vocabulary of one: the same word on the same
29
- * prop as a VButton, rather than a boolean of its own. There is no `accent` here,
30
- * a menu having no primary command among its rows.
31
27
  */
32
28
  tone?: MenuItemTone;
33
- /** Makes the item unusable: it no longer responds and the arrows skip over it. */
34
29
  disabled?: boolean;
35
30
  /**
36
31
  * Turns the item into a link pointing at this address, for a menu that navigates
@@ -1 +1 @@
1
- {"version":3,"file":"VMenuItem.js","names":[],"sources":["../../../src/components/VMenu/VMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * One command in a menu. The panel moves the focus between items, and choosing one closes\n * the whole menu, submenus included.\n *\n * With an `href` it becomes a link, made inert through `useInertLink` when disabled: a\n * link has no `disabled` attribute, so the address is removed, `aria-disabled` says why\n * and the consumer's click listeners are dropped (an `<a>` without `href` still clicks).\n *\n * With the `#submenu` slot it becomes the trigger of a nested panel rendered INSIDE the\n * parent one. That nesting is what buys the submenu behaviour from the browser: the panels\n * form a native stack, a click outside closes all of them, and opening one branch closes its\n * sibling.\n *\n * The JS covers only the two openings the browser does not: the keyboard, and a hover held\n * long enough to show it was meant. A click already opens it natively, and ONLY opens it\n * (`popovertargetaction=\"show\"`): the hover has usually opened it a moment before the\n * click lands, and the default toggle would shut what the reader was reaching for.\n */\n\nimport { inject, ref, useAttrs, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { useInertLink } from '../../composables/useInertLink'\nimport { isRtl } from '../../utils/direction'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport type { IconSource } from '../VIcon/types'\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuKey, SUBMENU_HOVER_DELAY } from './context'\n\nimport { useTimer } from '../../composables/useTimer'\n\n/**\n * What a command means, in colour. It is the subset of VButton's tones a menu row can\n * carry: an ordinary command, or a destructive one.\n */\nexport type MenuItemTone = 'neutral' | 'danger'\n\ninterface MenuItemProps {\n /** What the command says. The default slot replaces it. */\n label?: string\n /** A second line under the label, for a shortcut or a short explanation. */\n sublabel?: string\n /**\n * An icon before the label: an icon name, an image address, or an explicit render.\n * The `#start` slot replaces it.\n */\n iconStart?: IconSource\n /** An icon after the label, in the same forms. The `#end` slot replaces it. */\n iconEnd?: IconSource\n /**\n * Marks this item as the one currently in effect: the chosen sort order, the active\n * view. It is coloured and announced as such.\n */\n selected?: boolean\n /**\n * What the command means, in colour. `danger` marks it destructive (deleting something\n * belongs there), and `neutral`, the default, is every other command.\n *\n * A row is an action, so it takes the vocabulary of one: the same word on the same\n * prop as a VButton, rather than a boolean of its own. There is no `accent` here,\n * a menu having no primary command among its rows.\n */\n tone?: MenuItemTone\n /** Makes the item unusable: it no longer responds and the arrows skip over it. */\n disabled?: boolean\n /**\n * Turns the item into a link pointing at this address, for a menu that navigates\n * rather than acts. A disabled link is made inert by hand.\n */\n href?: string\n}\n\n// With a submenu this component renders two elements — the item and the nested panel\n// — so there is no single root for Vue to put the consumer's attributes on. They are\n// placed on the item explicitly.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<MenuItemProps>(), {\n label: undefined,\n sublabel: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n selected: false,\n tone: 'neutral',\n disabled: false,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * The command was chosen, by click or by keyboard. It is emitted before the menu\n * closes, so the handler runs while the menu is still open.\n */\n select: []\n}>()\n\nconst slots = defineSlots<{\n /** The label, replacing the `label` prop. */\n default?(): unknown\n /** The second line, replacing the `sublabel` prop. */\n sublabel?(): unknown\n /** Free content before the label, which takes the place of `iconStart`. */\n start?(): unknown\n /** Free content after the label, which takes the place of `iconEnd`. */\n end?(): unknown\n /**\n * The contents of a submenu: items, groups and separators, this component included,\n * so menus may nest as deep as needed.\n */\n submenu?(): unknown\n}>()\n\n// TRAP — a function read by the template, never a `computed`: `slots` is not reactive, so a\n// computed would keep its first answer while a slot behind a `v-if` comes and goes.\nfunction hasSubmenu() {\n return !!slots.submenu\n}\nfunction tag() {\n return !hasSubmenu() && props.href !== undefined ? ('a' as const) : ('button' as const)\n}\n\nconst attrs = useAttrs()\nconst link = useInertLink({\n href: () => (hasSubmenu() ? undefined : props.href),\n inert: () => props.disabled,\n attrs: () => attrs,\n})\n\nconst menu = inject(menuKey, null)\n\nfunction onClick() {\n if (props.disabled || hasSubmenu()) return\n emit('select')\n menu?.closeAll()\n}\n\nconst subId = useId()\nconst subOpen = ref(false)\nconst subPanel = ref<InstanceType<typeof VMenuPanel> | null>(null)\n// TRAP — when a submenu is opened from code rather than by a click, the item must be\n// handed to the browser as the source of that opening. The implicit anchor is only\n// established natively, on click; without it the panel has nothing to position itself\n// against and lands at the corner of the viewport, with no error anywhere.\nconst itemEl = ref<HTMLElement | null>(null)\n\n// @keyboard\n// Opening a submenu from the keyboard, which the browser's own toggle does not cover\n// — it only reacts to a click.\nfunction onKeydown(event: KeyboardEvent) {\n // A key held with a modifier is the browser's: Alt+Right is Forward.\n if (event.altKey || event.ctrlKey || event.metaKey) return\n // @keyboard @a11y\n // A menu item is activated by Space as well as Enter, and a LINK answers Enter alone:\n // Space would scroll the page instead.\n if (!hasSubmenu()) {\n if (event.key === ' ' && tag() === 'a') {\n event.preventDefault()\n itemEl.value?.click()\n }\n return\n }\n if (props.disabled) return\n // The submenu opens towards the end of the line, so the arrow that points at it is the\n // one that opens it: the left one in a right-to-left page.\n const toward = isRtl(itemEl.value) ? 'ArrowLeft' : 'ArrowRight'\n if (![toward, 'Enter', ' '].includes(event.key)) return\n // The button's native activation has to be stopped: it would fire a click of its\n // own, and that click would toggle the panel shut again right behind the opening\n // below.\n event.preventDefault()\n subPanel.value?.show(itemEl.value ?? undefined)\n subPanel.value?.focusFirst()\n}\n\n// A submenu opens on hover, but only once the pointer has stayed long enough to show\n// it was meant — otherwise every panel crossed on the way to another one would flash\n// open. ONE timer serves both directions, since opening and closing are mutually\n// exclusive: arming either always cancels the other.\nconst hoverTimer = useTimer()\n\n// @a11y @core\n// TRAP — a TOUCH tap sends `pointerenter` and `pointerleave` before its `click`, so the\n// leave below armed the close and the submenu the tap opened shut 150 ms later. Hover\n// intent means nothing to a finger: only a pointer that can hover takes this route, the\n// tap opening the submenu natively through the click.\nfunction onPointerEnter(event: PointerEvent) {\n if (props.disabled || event.pointerType === 'touch') return\n // Hovering also moves the focus, so that the mouse and the keyboard never highlight\n // two different items at once — in a menu there is only ever one current item.\n itemEl.value?.focus({ preventScroll: true })\n if (!hasSubmenu()) return\n hoverTimer.start(() => subPanel.value?.show(itemEl.value ?? undefined), SUBMENU_HOVER_DELAY)\n}\n\n// @a11y @core\n// The test on where the focus currently is, is the accessibility half:\n// a pointer drifting off the item must not close a submenu a keyboard user is\n// standing inside.\nfunction onPointerLeave(event: PointerEvent) {\n if (!hasSubmenu() || event.pointerType === 'touch') return\n hoverTimer.start(() => {\n if (subPanel.value?.el?.contains(document.activeElement)) return\n subPanel.value?.close()\n }, SUBMENU_HOVER_DELAY)\n}\n</script>\n\n<template>\n <component\n :is=\"tag()\"\n ref=\"itemEl\"\n :aria-disabled=\"link.isInertLink.value ? 'true' : undefined\"\n :aria-current=\"selected ? 'true' : undefined\"\n :aria-haspopup=\"hasSubmenu() ? 'menu' : undefined\"\n :aria-expanded=\"hasSubmenu() ? subOpen : undefined\"\n :aria-controls=\"hasSubmenu() ? subId : undefined\"\n v-bind=\"link.attrs.value\"\n role=\"menuitem\"\n tabindex=\"-1\"\n class=\"v-menu-item\"\n :type=\"tag() === 'button' ? 'button' : undefined\"\n :disabled=\"tag() === 'button' ? disabled : undefined\"\n :href=\"link.linkHref.value\"\n :data-tone=\"tone\"\n :data-selected=\"selected ? '' : undefined\"\n :popovertarget=\"hasSubmenu() ? subId : undefined\"\n :popovertargetaction=\"hasSubmenu() ? 'show' : undefined\"\n @click=\"onClick\"\n @keydown=\"onKeydown\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"start\">\n <VIcon v-if=\"iconStart\" class=\"v-menu-item-icon\" v-bind=\"iconProps(iconStart)\" />\n </slot>\n <span class=\"v-menu-item-content\">\n <span class=\"v-menu-item-label\"\n ><slot>{{ label }}</slot></span\n >\n <span v-if=\"sublabel !== undefined || $slots.sublabel\" class=\"v-menu-item-sublabel\">\n <slot name=\"sublabel\">{{ sublabel }}</slot>\n </span>\n </span>\n <!-- An item opening a submenu always shows the chevron announcing it, and never\n the end icon: the sideways opening is what the reader needs to be told -->\n <VIcon v-if=\"hasSubmenu()\" :name=\"chevronRightIcon\" class=\"v-menu-item-chevron\" mirrored />\n <slot v-else name=\"end\">\n <VIcon v-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" />\n </slot>\n </component>\n <VMenuPanel\n v-if=\"hasSubmenu()\"\n :id=\"subId\"\n ref=\"subPanel\"\n placement=\"right-start\"\n submenu\n @toggle=\"subOpen = $event\"\n @pointerenter=\"hoverTimer.cancel()\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"submenu\" />\n </VMenuPanel>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-item {\n /*\n * Every dimension comes from the `--control-*` variables the root panel sets and\n * this row inherits — one size table for the whole design system. The icons follow\n * with nothing written for them, their own variables belonging to that same block.\n *\n * The type is the one composite part: the SIZE comes from the scale, but the line\n * height stays that of body text — a unitless ratio, so it still follows the size\n * — and the weight stays regular. The full `control` type role would mean a medium\n * weight and lines set tight against each other, and a row may carry a second line\n * under its label.\n *\n * The label and that second line TRUNCATE rather than wrap, the VSideNavigationItem\n * recipe: the panel has a ceiling, and a command that grew a line would break the\n * rhythm of the list. `width` on VMenu is the way to give long labels more room.\n *\n * The corner is VSideNavigationItem's: the control radius capped at half a control\n * height, so a pill override paints every row of a menu alike, a row carrying a\n * sublabel included, instead of rounding each one to half of its own height.\n */\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n inline-size: 100%;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n border: none;\n background: transparent;\n color: var(--vectis-color-text);\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-family: inherit;\n font-size: var(--control-font-size);\n line-height: var(--vectis-text-body-md-leading);\n text-align: start;\n text-decoration: none;\n cursor: pointer;\n }\n\n .v-menu-item-content {\n flex: 1;\n min-inline-size: 0;\n display: flex;\n flex-direction: column;\n }\n\n .v-menu-item-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .v-menu-item-sublabel {\n overflow: hidden;\n font-size: var(--vectis-text-caption-size);\n color: var(--vectis-color-text-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* The start icon is muted, as on every row of the design system (VSideNavigationItem,\n VAccordionItem): the label carries the command, the icon only helps find it. */\n .v-menu-item-icon,\n .v-menu-item-chevron {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n /* In a menu the focus IS the highlight, so it is drawn on `:focus` and not on\n `:focus-visible`: the focus is moved from code — by the arrows, and by hovering —\n and the browser would not call that a keyboard focus worth showing. While a submenu is\n open its parent item keeps the highlight too, so the path followed through the levels\n stays visible. */\n .v-menu-item:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n background: var(--vectis-color-surface-muted);\n outline: none;\n }\n\n .v-menu-item[data-selected] {\n background: var(--vectis-color-accent-surface);\n color: var(--vectis-color-accent-text);\n }\n\n .v-menu-item[data-selected]:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-selected]:focus,\n .v-menu-item[data-selected][aria-expanded='true'] {\n /* The selected row is already tinted, so its hover deepens that tint rather than\n replacing it with the neutral highlight. */\n background: color-mix(\n in oklab,\n var(--vectis-color-accent-surface),\n var(--vectis-color-accent-text) 8%\n );\n }\n\n .v-menu-item[data-tone='danger'] {\n color: var(--vectis-color-danger-text);\n }\n\n .v-menu-item[data-tone='danger']:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-tone='danger']:focus,\n .v-menu-item[data-tone='danger'][aria-expanded='true'] {\n background: var(--vectis-color-danger-surface);\n }\n\n /* Both forms have to be matched: `:disabled` only ever applies to a `<button>`, and\n an item rendered as a link is made inert through `aria-disabled` instead. */\n .v-menu-item:disabled,\n .v-menu-item[aria-disabled='true'] {\n background: transparent;\n color: var(--vectis-color-text-subtle);\n cursor: not-allowed;\n }\n\n /* On a row painted in a colour of its own, the start icon and the second line take the\n label's colour rather than the muted grey: muted would clash with the selected and the\n danger tints, and on a disabled row it is DARKER than the subtle grey the label takes,\n so they would outshine it. Each `:is()` counts for its most specific member, which\n keeps the rule at (0,3,0), above the two (0,1,0) base rules. */\n .v-menu-item:is([data-selected], [data-tone='danger'], :disabled, [aria-disabled='true'])\n :is(.v-menu-item-icon, .v-menu-item-sublabel) {\n color: inherit;\n }\n\n /* Windows forced colors. The highlight is a background, which the mode forces to Canvas:\n the row under the focus would look exactly like the others, and a keyboard user would\n lose their place (WCAG 2.4.7). An outline inside the row takes its place. The selected\n row, also drawn by its tint alone, takes the system selection pair, the rule the\n selected controls of the actions family follow; the class is doubled to stay above\n its (0,3,0) hover and focus rules. */\n @media (forced-colors: active) {\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n outline: var(--vectis-focus-ring-width) solid Highlight;\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\n }\n\n .v-menu-item.v-menu-item[data-selected] {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n }\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VMenuItem.js","names":[],"sources":["../../../src/components/VMenu/VMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Menu commands use native buttons or links. JavaScript handles inert anchors and submenu hover\n * intent; the panel owns roving focus and dismissal.\n */\n\nimport { inject, ref, useAttrs, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { useInertLink } from '../../composables/useInertLink'\nimport { isRtl } from '../../utils/direction'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport type { IconSource } from '../VIcon/types'\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuKey, SUBMENU_HOVER_DELAY } from './context'\n\nimport { useTimer } from '../../composables/useTimer'\n\n/**\n * What a command means, in colour. It is the subset of VButton's tones a menu row can\n * carry: an ordinary command, or a destructive one.\n */\nexport type MenuItemTone = 'neutral' | 'danger'\n\ninterface MenuItemProps {\n /** What the command says. The default slot replaces it. */\n label?: string\n /** A second line under the label, for a shortcut or a short explanation. */\n sublabel?: string\n /**\n * An icon before the label: an icon name, an image address, or an explicit render.\n * The `#start` slot replaces it.\n */\n iconStart?: IconSource\n /** An icon after the label, in the same forms. The `#end` slot replaces it. */\n iconEnd?: IconSource\n /**\n * Marks this item as the one currently in effect: the chosen sort order, the active\n * view. It is coloured and announced as such.\n */\n selected?: boolean\n /**\n * What the command means, in colour. `danger` marks it destructive (deleting something\n * belongs there), and `neutral`, the default, is every other command.\n */\n tone?: MenuItemTone\n disabled?: boolean\n /**\n * Turns the item into a link pointing at this address, for a menu that navigates\n * rather than acts. A disabled link is made inert by hand.\n */\n href?: string\n}\n\n// With a submenu this component renders two elements; the item and the nested panel; so there\n// is no single root for Vue to put the consumer's attributes on. They are placed on the item\n// explicitly.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<MenuItemProps>(), {\n label: undefined,\n sublabel: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n selected: false,\n tone: 'neutral',\n disabled: false,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * The command was chosen, by click or by keyboard. It is emitted before the menu\n * closes, so the handler runs while the menu is still open.\n */\n select: []\n}>()\n\nconst slots = defineSlots<{\n /** The label, replacing the `label` prop. */\n default?(): unknown\n /** The second line, replacing the `sublabel` prop. */\n sublabel?(): unknown\n /** Free content before the label, which takes the place of `iconStart`. */\n start?(): unknown\n /** Free content after the label, which takes the place of `iconEnd`. */\n end?(): unknown\n /**\n * The contents of a submenu: items, groups and separators, this component included,\n * so menus may nest as deep as needed.\n */\n submenu?(): unknown\n}>()\n\n// A function read by the template, never a `computed`: `slots` is not reactive, so a computed\n// would keep its first answer while a slot behind a `v-if` comes and goes.\nfunction hasSubmenu() {\n return !!slots.submenu\n}\nfunction tag() {\n return !hasSubmenu() && props.href !== undefined ? ('a' as const) : ('button' as const)\n}\n\nconst attrs = useAttrs()\nconst link = useInertLink({\n href: () => (hasSubmenu() ? undefined : props.href),\n inert: () => props.disabled,\n attrs: () => attrs,\n})\n\nconst menu = inject(menuKey, null)\n\nfunction onClick() {\n if (props.disabled || hasSubmenu()) return\n emit('select')\n menu?.closeAll()\n}\n\nconst subId = useId()\nconst subOpen = ref(false)\nconst subPanel = ref<InstanceType<typeof VMenuPanel> | null>(null)\n// When a submenu is opened from code rather than by a click, the item must be handed to the\n// browser as the source of that opening. The implicit anchor is only established natively, on\n// click; without it the panel has nothing to position itself against and lands at the corner of\n// the viewport, with no error anywhere.\nconst itemEl = ref<HTMLElement | null>(null)\n\n// @keyboard\n// Opening a submenu from the keyboard, which the browser's own toggle does not cover; it only\n// reacts to a click.\nfunction onKeydown(event: KeyboardEvent) {\n if (event.altKey || event.ctrlKey || event.metaKey) return\n // @keyboard @a11y\n // A menu item is activated by Space as well as Enter, and a LINK answers Enter alone:\n // Space would scroll the page instead.\n if (!hasSubmenu()) {\n if (event.key === ' ' && tag() === 'a') {\n event.preventDefault()\n itemEl.value?.click()\n }\n return\n }\n if (props.disabled) return\n // The submenu opens towards the end of the line, so the arrow that points at it is the\n // one that opens it: the left one in a right-to-left page.\n const toward = isRtl(itemEl.value) ? 'ArrowLeft' : 'ArrowRight'\n if (![toward, 'Enter', ' '].includes(event.key)) return\n // The button's native activation has to be stopped: it would fire a click of its\n // own, and that click would toggle the panel shut again right behind the opening\n // below.\n event.preventDefault()\n subPanel.value?.show(itemEl.value ?? undefined)\n subPanel.value?.focusFirst()\n}\n\n// A submenu opens on hover, but only once the pointer has stayed long enough to show it was\n// meant; otherwise every panel crossed on the way to another one would flash open. ONE timer\n// serves both directions, since opening and closing are mutually exclusive: arming either\n// always cancels the other.\nconst hoverTimer = useTimer()\n\n// @a11y @core\n// A TOUCH tap sends `pointerenter` and `pointerleave` before its `click`, so the leave below\n// armed the close and the submenu the tap opened shut 150 ms later. Hover intent means nothing\n// to a finger: only a pointer that can hover takes this route, the tap opening the submenu\n// natively through the click.\nfunction onPointerEnter(event: PointerEvent) {\n if (props.disabled || event.pointerType === 'touch') return\n // Hovering also moves the focus, so that the mouse and the keyboard never highlight two\n // different items at once; in a menu there is only ever one current item.\n itemEl.value?.focus({ preventScroll: true })\n if (!hasSubmenu()) return\n hoverTimer.start(() => subPanel.value?.show(itemEl.value ?? undefined), SUBMENU_HOVER_DELAY)\n}\n\n// @a11y @core\n// The test on where the focus currently is, is the accessibility half:\n// a pointer drifting off the item must not close a submenu a keyboard user is\n// standing inside.\nfunction onPointerLeave(event: PointerEvent) {\n if (!hasSubmenu() || event.pointerType === 'touch') return\n hoverTimer.start(() => {\n if (subPanel.value?.el?.contains(document.activeElement)) return\n subPanel.value?.close()\n }, SUBMENU_HOVER_DELAY)\n}\n</script>\n\n<template>\n <component\n :is=\"tag()\"\n ref=\"itemEl\"\n :aria-disabled=\"link.isInertLink.value ? 'true' : undefined\"\n :aria-current=\"selected ? 'true' : undefined\"\n :aria-haspopup=\"hasSubmenu() ? 'menu' : undefined\"\n :aria-expanded=\"hasSubmenu() ? subOpen : undefined\"\n :aria-controls=\"hasSubmenu() ? subId : undefined\"\n v-bind=\"link.attrs.value\"\n role=\"menuitem\"\n tabindex=\"-1\"\n class=\"v-menu-item\"\n :type=\"tag() === 'button' ? 'button' : undefined\"\n :disabled=\"tag() === 'button' ? disabled : undefined\"\n :href=\"link.linkHref.value\"\n :data-tone=\"tone\"\n :data-selected=\"selected ? '' : undefined\"\n :popovertarget=\"hasSubmenu() ? subId : undefined\"\n :popovertargetaction=\"hasSubmenu() ? 'show' : undefined\"\n @click=\"onClick\"\n @keydown=\"onKeydown\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"start\">\n <VIcon v-if=\"iconStart\" class=\"v-menu-item-icon\" v-bind=\"iconProps(iconStart)\" />\n </slot>\n <span class=\"v-menu-item-content\">\n <span class=\"v-menu-item-label\"\n ><slot>{{ label }}</slot></span\n >\n <span v-if=\"sublabel !== undefined || $slots.sublabel\" class=\"v-menu-item-sublabel\">\n <slot name=\"sublabel\">{{ sublabel }}</slot>\n </span>\n </span>\n <!-- An item opening a submenu always shows the chevron announcing it, and never\n the end icon: the sideways opening is what the reader needs to be told -->\n <VIcon v-if=\"hasSubmenu()\" :name=\"chevronRightIcon\" class=\"v-menu-item-chevron\" mirrored />\n <slot v-else name=\"end\">\n <VIcon v-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" />\n </slot>\n </component>\n <VMenuPanel\n v-if=\"hasSubmenu()\"\n :id=\"subId\"\n ref=\"subPanel\"\n placement=\"right-start\"\n submenu\n @toggle=\"subOpen = $event\"\n @pointerenter=\"hoverTimer.cancel()\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"submenu\" />\n </VMenuPanel>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-item {\n /*\n * Every dimension comes from the `--control-*` variables the root panel sets and this row\n * inherits; one size table for the whole design system. The icons follow with nothing\n * written for them, their own variables belonging to that same block.\n */\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n inline-size: 100%;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n border: none;\n background: transparent;\n color: var(--vectis-color-text);\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-family: inherit;\n font-size: var(--control-font-size);\n line-height: var(--vectis-text-body-md-leading);\n text-align: start;\n text-decoration: none;\n cursor: pointer;\n }\n\n .v-menu-item-content {\n flex: 1;\n min-inline-size: 0;\n display: flex;\n flex-direction: column;\n }\n\n .v-menu-item-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .v-menu-item-sublabel {\n overflow: hidden;\n font-size: var(--vectis-text-caption-size);\n color: var(--vectis-color-text-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* The start icon is muted, as on every row of the design system (VSideNavigationItem,\n VAccordionItem): the label carries the command, the icon only helps find it. */\n .v-menu-item-icon,\n .v-menu-item-chevron {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n /*\n * In a menu the focus IS the highlight, so it is drawn on `:focus` and not on\n * `:focus-visible`: the focus is moved from code; by the arrows, and by hovering; and the\n * browser would not call that a keyboard focus worth showing. While a submenu is open its\n * parent item keeps the highlight too, so the path followed through the levels stays visible.\n */\n .v-menu-item:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n background: var(--vectis-color-surface-muted);\n outline: none;\n }\n\n .v-menu-item[data-selected] {\n background: var(--vectis-color-accent-surface);\n color: var(--vectis-color-accent-text);\n }\n\n .v-menu-item[data-selected]:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-selected]:focus,\n .v-menu-item[data-selected][aria-expanded='true'] {\n /* The selected row is already tinted, so its hover deepens that tint rather than\n replacing it with the neutral highlight. */\n background: color-mix(\n in oklab,\n var(--vectis-color-accent-surface),\n var(--vectis-color-accent-text) 8%\n );\n }\n\n .v-menu-item[data-tone='danger'] {\n color: var(--vectis-color-danger-text);\n }\n\n .v-menu-item[data-tone='danger']:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-tone='danger']:focus,\n .v-menu-item[data-tone='danger'][aria-expanded='true'] {\n background: var(--vectis-color-danger-surface);\n }\n\n /* Both forms have to be matched: `:disabled` only ever applies to a `<button>`, and\n an item rendered as a link is made inert through `aria-disabled` instead. */\n .v-menu-item:disabled,\n .v-menu-item[aria-disabled='true'] {\n background: transparent;\n color: var(--vectis-color-text-subtle);\n cursor: not-allowed;\n }\n\n /* On a row painted in a colour of its own, the start icon and the second line take the\n label's colour rather than the muted grey: muted would clash with the selected and the\n danger tints, and on a disabled row it is DARKER than the subtle grey the label takes,\n so they would outshine it. Each `:is()` counts for its most specific member, which\n keeps the rule at (0,3,0), above the two (0,1,0) base rules. */\n .v-menu-item:is([data-selected], [data-tone='danger'], :disabled, [aria-disabled='true'])\n :is(.v-menu-item-icon, .v-menu-item-sublabel) {\n color: inherit;\n }\n\n /*\n * The highlight is a background, which the mode forces to Canvas: the row under the focus\n * would look exactly like the others, and a keyboard user would lose their place (WCAG\n * 2.4.7). An outline inside the row takes its place.\n */\n @media (forced-colors: active) {\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n outline: var(--vectis-focus-ring-width) solid Highlight;\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\n }\n\n .v-menu-item.v-menu-item[data-selected] {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n }\n }\n}\n</style>\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"VMenuItem.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VMenu/VMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * One command in a menu. The panel moves the focus between items, and choosing one closes\n * the whole menu, submenus included.\n *\n * With an `href` it becomes a link, made inert through `useInertLink` when disabled: a\n * link has no `disabled` attribute, so the address is removed, `aria-disabled` says why\n * and the consumer's click listeners are dropped (an `<a>` without `href` still clicks).\n *\n * With the `#submenu` slot it becomes the trigger of a nested panel rendered INSIDE the\n * parent one. That nesting is what buys the submenu behaviour from the browser: the panels\n * form a native stack, a click outside closes all of them, and opening one branch closes its\n * sibling.\n *\n * The JS covers only the two openings the browser does not: the keyboard, and a hover held\n * long enough to show it was meant. A click already opens it natively, and ONLY opens it\n * (`popovertargetaction=\"show\"`): the hover has usually opened it a moment before the\n * click lands, and the default toggle would shut what the reader was reaching for.\n */\n\nimport { inject, ref, useAttrs, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { useInertLink } from '../../composables/useInertLink'\nimport { isRtl } from '../../utils/direction'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport type { IconSource } from '../VIcon/types'\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuKey, SUBMENU_HOVER_DELAY } from './context'\n\nimport { useTimer } from '../../composables/useTimer'\n\n/**\n * What a command means, in colour. It is the subset of VButton's tones a menu row can\n * carry: an ordinary command, or a destructive one.\n */\nexport type MenuItemTone = 'neutral' | 'danger'\n\ninterface MenuItemProps {\n /** What the command says. The default slot replaces it. */\n label?: string\n /** A second line under the label, for a shortcut or a short explanation. */\n sublabel?: string\n /**\n * An icon before the label: an icon name, an image address, or an explicit render.\n * The `#start` slot replaces it.\n */\n iconStart?: IconSource\n /** An icon after the label, in the same forms. The `#end` slot replaces it. */\n iconEnd?: IconSource\n /**\n * Marks this item as the one currently in effect: the chosen sort order, the active\n * view. It is coloured and announced as such.\n */\n selected?: boolean\n /**\n * What the command means, in colour. `danger` marks it destructive (deleting something\n * belongs there), and `neutral`, the default, is every other command.\n *\n * A row is an action, so it takes the vocabulary of one: the same word on the same\n * prop as a VButton, rather than a boolean of its own. There is no `accent` here,\n * a menu having no primary command among its rows.\n */\n tone?: MenuItemTone\n /** Makes the item unusable: it no longer responds and the arrows skip over it. */\n disabled?: boolean\n /**\n * Turns the item into a link pointing at this address, for a menu that navigates\n * rather than acts. A disabled link is made inert by hand.\n */\n href?: string\n}\n\n// With a submenu this component renders two elements — the item and the nested panel\n// — so there is no single root for Vue to put the consumer's attributes on. They are\n// placed on the item explicitly.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<MenuItemProps>(), {\n label: undefined,\n sublabel: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n selected: false,\n tone: 'neutral',\n disabled: false,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * The command was chosen, by click or by keyboard. It is emitted before the menu\n * closes, so the handler runs while the menu is still open.\n */\n select: []\n}>()\n\nconst slots = defineSlots<{\n /** The label, replacing the `label` prop. */\n default?(): unknown\n /** The second line, replacing the `sublabel` prop. */\n sublabel?(): unknown\n /** Free content before the label, which takes the place of `iconStart`. */\n start?(): unknown\n /** Free content after the label, which takes the place of `iconEnd`. */\n end?(): unknown\n /**\n * The contents of a submenu: items, groups and separators, this component included,\n * so menus may nest as deep as needed.\n */\n submenu?(): unknown\n}>()\n\n// TRAP — a function read by the template, never a `computed`: `slots` is not reactive, so a\n// computed would keep its first answer while a slot behind a `v-if` comes and goes.\nfunction hasSubmenu() {\n return !!slots.submenu\n}\nfunction tag() {\n return !hasSubmenu() && props.href !== undefined ? ('a' as const) : ('button' as const)\n}\n\nconst attrs = useAttrs()\nconst link = useInertLink({\n href: () => (hasSubmenu() ? undefined : props.href),\n inert: () => props.disabled,\n attrs: () => attrs,\n})\n\nconst menu = inject(menuKey, null)\n\nfunction onClick() {\n if (props.disabled || hasSubmenu()) return\n emit('select')\n menu?.closeAll()\n}\n\nconst subId = useId()\nconst subOpen = ref(false)\nconst subPanel = ref<InstanceType<typeof VMenuPanel> | null>(null)\n// TRAP — when a submenu is opened from code rather than by a click, the item must be\n// handed to the browser as the source of that opening. The implicit anchor is only\n// established natively, on click; without it the panel has nothing to position itself\n// against and lands at the corner of the viewport, with no error anywhere.\nconst itemEl = ref<HTMLElement | null>(null)\n\n// @keyboard\n// Opening a submenu from the keyboard, which the browser's own toggle does not cover\n// — it only reacts to a click.\nfunction onKeydown(event: KeyboardEvent) {\n // A key held with a modifier is the browser's: Alt+Right is Forward.\n if (event.altKey || event.ctrlKey || event.metaKey) return\n // @keyboard @a11y\n // A menu item is activated by Space as well as Enter, and a LINK answers Enter alone:\n // Space would scroll the page instead.\n if (!hasSubmenu()) {\n if (event.key === ' ' && tag() === 'a') {\n event.preventDefault()\n itemEl.value?.click()\n }\n return\n }\n if (props.disabled) return\n // The submenu opens towards the end of the line, so the arrow that points at it is the\n // one that opens it: the left one in a right-to-left page.\n const toward = isRtl(itemEl.value) ? 'ArrowLeft' : 'ArrowRight'\n if (![toward, 'Enter', ' '].includes(event.key)) return\n // The button's native activation has to be stopped: it would fire a click of its\n // own, and that click would toggle the panel shut again right behind the opening\n // below.\n event.preventDefault()\n subPanel.value?.show(itemEl.value ?? undefined)\n subPanel.value?.focusFirst()\n}\n\n// A submenu opens on hover, but only once the pointer has stayed long enough to show\n// it was meant — otherwise every panel crossed on the way to another one would flash\n// open. ONE timer serves both directions, since opening and closing are mutually\n// exclusive: arming either always cancels the other.\nconst hoverTimer = useTimer()\n\n// @a11y @core\n// TRAP — a TOUCH tap sends `pointerenter` and `pointerleave` before its `click`, so the\n// leave below armed the close and the submenu the tap opened shut 150 ms later. Hover\n// intent means nothing to a finger: only a pointer that can hover takes this route, the\n// tap opening the submenu natively through the click.\nfunction onPointerEnter(event: PointerEvent) {\n if (props.disabled || event.pointerType === 'touch') return\n // Hovering also moves the focus, so that the mouse and the keyboard never highlight\n // two different items at once — in a menu there is only ever one current item.\n itemEl.value?.focus({ preventScroll: true })\n if (!hasSubmenu()) return\n hoverTimer.start(() => subPanel.value?.show(itemEl.value ?? undefined), SUBMENU_HOVER_DELAY)\n}\n\n// @a11y @core\n// The test on where the focus currently is, is the accessibility half:\n// a pointer drifting off the item must not close a submenu a keyboard user is\n// standing inside.\nfunction onPointerLeave(event: PointerEvent) {\n if (!hasSubmenu() || event.pointerType === 'touch') return\n hoverTimer.start(() => {\n if (subPanel.value?.el?.contains(document.activeElement)) return\n subPanel.value?.close()\n }, SUBMENU_HOVER_DELAY)\n}\n</script>\n\n<template>\n <component\n :is=\"tag()\"\n ref=\"itemEl\"\n :aria-disabled=\"link.isInertLink.value ? 'true' : undefined\"\n :aria-current=\"selected ? 'true' : undefined\"\n :aria-haspopup=\"hasSubmenu() ? 'menu' : undefined\"\n :aria-expanded=\"hasSubmenu() ? subOpen : undefined\"\n :aria-controls=\"hasSubmenu() ? subId : undefined\"\n v-bind=\"link.attrs.value\"\n role=\"menuitem\"\n tabindex=\"-1\"\n class=\"v-menu-item\"\n :type=\"tag() === 'button' ? 'button' : undefined\"\n :disabled=\"tag() === 'button' ? disabled : undefined\"\n :href=\"link.linkHref.value\"\n :data-tone=\"tone\"\n :data-selected=\"selected ? '' : undefined\"\n :popovertarget=\"hasSubmenu() ? subId : undefined\"\n :popovertargetaction=\"hasSubmenu() ? 'show' : undefined\"\n @click=\"onClick\"\n @keydown=\"onKeydown\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"start\">\n <VIcon v-if=\"iconStart\" class=\"v-menu-item-icon\" v-bind=\"iconProps(iconStart)\" />\n </slot>\n <span class=\"v-menu-item-content\">\n <span class=\"v-menu-item-label\"\n ><slot>{{ label }}</slot></span\n >\n <span v-if=\"sublabel !== undefined || $slots.sublabel\" class=\"v-menu-item-sublabel\">\n <slot name=\"sublabel\">{{ sublabel }}</slot>\n </span>\n </span>\n <!-- An item opening a submenu always shows the chevron announcing it, and never\n the end icon: the sideways opening is what the reader needs to be told -->\n <VIcon v-if=\"hasSubmenu()\" :name=\"chevronRightIcon\" class=\"v-menu-item-chevron\" mirrored />\n <slot v-else name=\"end\">\n <VIcon v-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" />\n </slot>\n </component>\n <VMenuPanel\n v-if=\"hasSubmenu()\"\n :id=\"subId\"\n ref=\"subPanel\"\n placement=\"right-start\"\n submenu\n @toggle=\"subOpen = $event\"\n @pointerenter=\"hoverTimer.cancel()\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"submenu\" />\n </VMenuPanel>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-item {\n /*\n * Every dimension comes from the `--control-*` variables the root panel sets and\n * this row inherits — one size table for the whole design system. The icons follow\n * with nothing written for them, their own variables belonging to that same block.\n *\n * The type is the one composite part: the SIZE comes from the scale, but the line\n * height stays that of body text — a unitless ratio, so it still follows the size\n * — and the weight stays regular. The full `control` type role would mean a medium\n * weight and lines set tight against each other, and a row may carry a second line\n * under its label.\n *\n * The label and that second line TRUNCATE rather than wrap, the VSideNavigationItem\n * recipe: the panel has a ceiling, and a command that grew a line would break the\n * rhythm of the list. `width` on VMenu is the way to give long labels more room.\n *\n * The corner is VSideNavigationItem's: the control radius capped at half a control\n * height, so a pill override paints every row of a menu alike, a row carrying a\n * sublabel included, instead of rounding each one to half of its own height.\n */\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n inline-size: 100%;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n border: none;\n background: transparent;\n color: var(--vectis-color-text);\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-family: inherit;\n font-size: var(--control-font-size);\n line-height: var(--vectis-text-body-md-leading);\n text-align: start;\n text-decoration: none;\n cursor: pointer;\n }\n\n .v-menu-item-content {\n flex: 1;\n min-inline-size: 0;\n display: flex;\n flex-direction: column;\n }\n\n .v-menu-item-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .v-menu-item-sublabel {\n overflow: hidden;\n font-size: var(--vectis-text-caption-size);\n color: var(--vectis-color-text-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* The start icon is muted, as on every row of the design system (VSideNavigationItem,\n VAccordionItem): the label carries the command, the icon only helps find it. */\n .v-menu-item-icon,\n .v-menu-item-chevron {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n /* In a menu the focus IS the highlight, so it is drawn on `:focus` and not on\n `:focus-visible`: the focus is moved from code — by the arrows, and by hovering —\n and the browser would not call that a keyboard focus worth showing. While a submenu is\n open its parent item keeps the highlight too, so the path followed through the levels\n stays visible. */\n .v-menu-item:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n background: var(--vectis-color-surface-muted);\n outline: none;\n }\n\n .v-menu-item[data-selected] {\n background: var(--vectis-color-accent-surface);\n color: var(--vectis-color-accent-text);\n }\n\n .v-menu-item[data-selected]:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-selected]:focus,\n .v-menu-item[data-selected][aria-expanded='true'] {\n /* The selected row is already tinted, so its hover deepens that tint rather than\n replacing it with the neutral highlight. */\n background: color-mix(\n in oklab,\n var(--vectis-color-accent-surface),\n var(--vectis-color-accent-text) 8%\n );\n }\n\n .v-menu-item[data-tone='danger'] {\n color: var(--vectis-color-danger-text);\n }\n\n .v-menu-item[data-tone='danger']:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-tone='danger']:focus,\n .v-menu-item[data-tone='danger'][aria-expanded='true'] {\n background: var(--vectis-color-danger-surface);\n }\n\n /* Both forms have to be matched: `:disabled` only ever applies to a `<button>`, and\n an item rendered as a link is made inert through `aria-disabled` instead. */\n .v-menu-item:disabled,\n .v-menu-item[aria-disabled='true'] {\n background: transparent;\n color: var(--vectis-color-text-subtle);\n cursor: not-allowed;\n }\n\n /* On a row painted in a colour of its own, the start icon and the second line take the\n label's colour rather than the muted grey: muted would clash with the selected and the\n danger tints, and on a disabled row it is DARKER than the subtle grey the label takes,\n so they would outshine it. Each `:is()` counts for its most specific member, which\n keeps the rule at (0,3,0), above the two (0,1,0) base rules. */\n .v-menu-item:is([data-selected], [data-tone='danger'], :disabled, [aria-disabled='true'])\n :is(.v-menu-item-icon, .v-menu-item-sublabel) {\n color: inherit;\n }\n\n /* Windows forced colors. The highlight is a background, which the mode forces to Canvas:\n the row under the focus would look exactly like the others, and a keyboard user would\n lose their place (WCAG 2.4.7). An outline inside the row takes its place. The selected\n row, also drawn by its tint alone, takes the system selection pair, the rule the\n selected controls of the actions family follow; the class is doubled to stay above\n its (0,3,0) hover and focus rules. */\n @media (forced-colors: active) {\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n outline: var(--vectis-focus-ring-width) solid Highlight;\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\n }\n\n .v-menu-item.v-menu-item[data-selected] {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"VMenuItem.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VMenu/VMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Menu commands use native buttons or links. JavaScript handles inert anchors and submenu hover\n * intent; the panel owns roving focus and dismissal.\n */\n\nimport { inject, ref, useAttrs, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { useInertLink } from '../../composables/useInertLink'\nimport { isRtl } from '../../utils/direction'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport type { IconSource } from '../VIcon/types'\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuKey, SUBMENU_HOVER_DELAY } from './context'\n\nimport { useTimer } from '../../composables/useTimer'\n\n/**\n * What a command means, in colour. It is the subset of VButton's tones a menu row can\n * carry: an ordinary command, or a destructive one.\n */\nexport type MenuItemTone = 'neutral' | 'danger'\n\ninterface MenuItemProps {\n /** What the command says. The default slot replaces it. */\n label?: string\n /** A second line under the label, for a shortcut or a short explanation. */\n sublabel?: string\n /**\n * An icon before the label: an icon name, an image address, or an explicit render.\n * The `#start` slot replaces it.\n */\n iconStart?: IconSource\n /** An icon after the label, in the same forms. The `#end` slot replaces it. */\n iconEnd?: IconSource\n /**\n * Marks this item as the one currently in effect: the chosen sort order, the active\n * view. It is coloured and announced as such.\n */\n selected?: boolean\n /**\n * What the command means, in colour. `danger` marks it destructive (deleting something\n * belongs there), and `neutral`, the default, is every other command.\n */\n tone?: MenuItemTone\n disabled?: boolean\n /**\n * Turns the item into a link pointing at this address, for a menu that navigates\n * rather than acts. A disabled link is made inert by hand.\n */\n href?: string\n}\n\n// With a submenu this component renders two elements; the item and the nested panel; so there\n// is no single root for Vue to put the consumer's attributes on. They are placed on the item\n// explicitly.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<MenuItemProps>(), {\n label: undefined,\n sublabel: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n selected: false,\n tone: 'neutral',\n disabled: false,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * The command was chosen, by click or by keyboard. It is emitted before the menu\n * closes, so the handler runs while the menu is still open.\n */\n select: []\n}>()\n\nconst slots = defineSlots<{\n /** The label, replacing the `label` prop. */\n default?(): unknown\n /** The second line, replacing the `sublabel` prop. */\n sublabel?(): unknown\n /** Free content before the label, which takes the place of `iconStart`. */\n start?(): unknown\n /** Free content after the label, which takes the place of `iconEnd`. */\n end?(): unknown\n /**\n * The contents of a submenu: items, groups and separators, this component included,\n * so menus may nest as deep as needed.\n */\n submenu?(): unknown\n}>()\n\n// A function read by the template, never a `computed`: `slots` is not reactive, so a computed\n// would keep its first answer while a slot behind a `v-if` comes and goes.\nfunction hasSubmenu() {\n return !!slots.submenu\n}\nfunction tag() {\n return !hasSubmenu() && props.href !== undefined ? ('a' as const) : ('button' as const)\n}\n\nconst attrs = useAttrs()\nconst link = useInertLink({\n href: () => (hasSubmenu() ? undefined : props.href),\n inert: () => props.disabled,\n attrs: () => attrs,\n})\n\nconst menu = inject(menuKey, null)\n\nfunction onClick() {\n if (props.disabled || hasSubmenu()) return\n emit('select')\n menu?.closeAll()\n}\n\nconst subId = useId()\nconst subOpen = ref(false)\nconst subPanel = ref<InstanceType<typeof VMenuPanel> | null>(null)\n// When a submenu is opened from code rather than by a click, the item must be handed to the\n// browser as the source of that opening. The implicit anchor is only established natively, on\n// click; without it the panel has nothing to position itself against and lands at the corner of\n// the viewport, with no error anywhere.\nconst itemEl = ref<HTMLElement | null>(null)\n\n// @keyboard\n// Opening a submenu from the keyboard, which the browser's own toggle does not cover; it only\n// reacts to a click.\nfunction onKeydown(event: KeyboardEvent) {\n if (event.altKey || event.ctrlKey || event.metaKey) return\n // @keyboard @a11y\n // A menu item is activated by Space as well as Enter, and a LINK answers Enter alone:\n // Space would scroll the page instead.\n if (!hasSubmenu()) {\n if (event.key === ' ' && tag() === 'a') {\n event.preventDefault()\n itemEl.value?.click()\n }\n return\n }\n if (props.disabled) return\n // The submenu opens towards the end of the line, so the arrow that points at it is the\n // one that opens it: the left one in a right-to-left page.\n const toward = isRtl(itemEl.value) ? 'ArrowLeft' : 'ArrowRight'\n if (![toward, 'Enter', ' '].includes(event.key)) return\n // The button's native activation has to be stopped: it would fire a click of its\n // own, and that click would toggle the panel shut again right behind the opening\n // below.\n event.preventDefault()\n subPanel.value?.show(itemEl.value ?? undefined)\n subPanel.value?.focusFirst()\n}\n\n// A submenu opens on hover, but only once the pointer has stayed long enough to show it was\n// meant; otherwise every panel crossed on the way to another one would flash open. ONE timer\n// serves both directions, since opening and closing are mutually exclusive: arming either\n// always cancels the other.\nconst hoverTimer = useTimer()\n\n// @a11y @core\n// A TOUCH tap sends `pointerenter` and `pointerleave` before its `click`, so the leave below\n// armed the close and the submenu the tap opened shut 150 ms later. Hover intent means nothing\n// to a finger: only a pointer that can hover takes this route, the tap opening the submenu\n// natively through the click.\nfunction onPointerEnter(event: PointerEvent) {\n if (props.disabled || event.pointerType === 'touch') return\n // Hovering also moves the focus, so that the mouse and the keyboard never highlight two\n // different items at once; in a menu there is only ever one current item.\n itemEl.value?.focus({ preventScroll: true })\n if (!hasSubmenu()) return\n hoverTimer.start(() => subPanel.value?.show(itemEl.value ?? undefined), SUBMENU_HOVER_DELAY)\n}\n\n// @a11y @core\n// The test on where the focus currently is, is the accessibility half:\n// a pointer drifting off the item must not close a submenu a keyboard user is\n// standing inside.\nfunction onPointerLeave(event: PointerEvent) {\n if (!hasSubmenu() || event.pointerType === 'touch') return\n hoverTimer.start(() => {\n if (subPanel.value?.el?.contains(document.activeElement)) return\n subPanel.value?.close()\n }, SUBMENU_HOVER_DELAY)\n}\n</script>\n\n<template>\n <component\n :is=\"tag()\"\n ref=\"itemEl\"\n :aria-disabled=\"link.isInertLink.value ? 'true' : undefined\"\n :aria-current=\"selected ? 'true' : undefined\"\n :aria-haspopup=\"hasSubmenu() ? 'menu' : undefined\"\n :aria-expanded=\"hasSubmenu() ? subOpen : undefined\"\n :aria-controls=\"hasSubmenu() ? subId : undefined\"\n v-bind=\"link.attrs.value\"\n role=\"menuitem\"\n tabindex=\"-1\"\n class=\"v-menu-item\"\n :type=\"tag() === 'button' ? 'button' : undefined\"\n :disabled=\"tag() === 'button' ? disabled : undefined\"\n :href=\"link.linkHref.value\"\n :data-tone=\"tone\"\n :data-selected=\"selected ? '' : undefined\"\n :popovertarget=\"hasSubmenu() ? subId : undefined\"\n :popovertargetaction=\"hasSubmenu() ? 'show' : undefined\"\n @click=\"onClick\"\n @keydown=\"onKeydown\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"start\">\n <VIcon v-if=\"iconStart\" class=\"v-menu-item-icon\" v-bind=\"iconProps(iconStart)\" />\n </slot>\n <span class=\"v-menu-item-content\">\n <span class=\"v-menu-item-label\"\n ><slot>{{ label }}</slot></span\n >\n <span v-if=\"sublabel !== undefined || $slots.sublabel\" class=\"v-menu-item-sublabel\">\n <slot name=\"sublabel\">{{ sublabel }}</slot>\n </span>\n </span>\n <!-- An item opening a submenu always shows the chevron announcing it, and never\n the end icon: the sideways opening is what the reader needs to be told -->\n <VIcon v-if=\"hasSubmenu()\" :name=\"chevronRightIcon\" class=\"v-menu-item-chevron\" mirrored />\n <slot v-else name=\"end\">\n <VIcon v-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" />\n </slot>\n </component>\n <VMenuPanel\n v-if=\"hasSubmenu()\"\n :id=\"subId\"\n ref=\"subPanel\"\n placement=\"right-start\"\n submenu\n @toggle=\"subOpen = $event\"\n @pointerenter=\"hoverTimer.cancel()\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"submenu\" />\n </VMenuPanel>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-item {\n /*\n * Every dimension comes from the `--control-*` variables the root panel sets and this row\n * inherits; one size table for the whole design system. The icons follow with nothing\n * written for them, their own variables belonging to that same block.\n */\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n inline-size: 100%;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n border: none;\n background: transparent;\n color: var(--vectis-color-text);\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-family: inherit;\n font-size: var(--control-font-size);\n line-height: var(--vectis-text-body-md-leading);\n text-align: start;\n text-decoration: none;\n cursor: pointer;\n }\n\n .v-menu-item-content {\n flex: 1;\n min-inline-size: 0;\n display: flex;\n flex-direction: column;\n }\n\n .v-menu-item-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .v-menu-item-sublabel {\n overflow: hidden;\n font-size: var(--vectis-text-caption-size);\n color: var(--vectis-color-text-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* The start icon is muted, as on every row of the design system (VSideNavigationItem,\n VAccordionItem): the label carries the command, the icon only helps find it. */\n .v-menu-item-icon,\n .v-menu-item-chevron {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n /*\n * In a menu the focus IS the highlight, so it is drawn on `:focus` and not on\n * `:focus-visible`: the focus is moved from code; by the arrows, and by hovering; and the\n * browser would not call that a keyboard focus worth showing. While a submenu is open its\n * parent item keeps the highlight too, so the path followed through the levels stays visible.\n */\n .v-menu-item:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n background: var(--vectis-color-surface-muted);\n outline: none;\n }\n\n .v-menu-item[data-selected] {\n background: var(--vectis-color-accent-surface);\n color: var(--vectis-color-accent-text);\n }\n\n .v-menu-item[data-selected]:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-selected]:focus,\n .v-menu-item[data-selected][aria-expanded='true'] {\n /* The selected row is already tinted, so its hover deepens that tint rather than\n replacing it with the neutral highlight. */\n background: color-mix(\n in oklab,\n var(--vectis-color-accent-surface),\n var(--vectis-color-accent-text) 8%\n );\n }\n\n .v-menu-item[data-tone='danger'] {\n color: var(--vectis-color-danger-text);\n }\n\n .v-menu-item[data-tone='danger']:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-tone='danger']:focus,\n .v-menu-item[data-tone='danger'][aria-expanded='true'] {\n background: var(--vectis-color-danger-surface);\n }\n\n /* Both forms have to be matched: `:disabled` only ever applies to a `<button>`, and\n an item rendered as a link is made inert through `aria-disabled` instead. */\n .v-menu-item:disabled,\n .v-menu-item[aria-disabled='true'] {\n background: transparent;\n color: var(--vectis-color-text-subtle);\n cursor: not-allowed;\n }\n\n /* On a row painted in a colour of its own, the start icon and the second line take the\n label's colour rather than the muted grey: muted would clash with the selected and the\n danger tints, and on a disabled row it is DARKER than the subtle grey the label takes,\n so they would outshine it. Each `:is()` counts for its most specific member, which\n keeps the rule at (0,3,0), above the two (0,1,0) base rules. */\n .v-menu-item:is([data-selected], [data-tone='danger'], :disabled, [aria-disabled='true'])\n :is(.v-menu-item-icon, .v-menu-item-sublabel) {\n color: inherit;\n }\n\n /*\n * The highlight is a background, which the mode forces to Canvas: the row under the focus\n * would look exactly like the others, and a keyboard user would lose their place (WCAG\n * 2.4.7). An outline inside the row takes its place.\n */\n @media (forced-colors: active) {\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n outline: var(--vectis-focus-ring-width) solid Highlight;\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\n }\n\n .v-menu-item.v-menu-item[data-selected] {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
@@ -33,12 +33,7 @@ type __VLS_Slots = {
33
33
  };
34
34
  /** Puts the focus on the first item the keyboard can reach, when the menu opens. */
35
35
  declare function focusFirst(): void;
36
- /**
37
- * Puts the focus on the panel itself, singling out no command. This is where a menu
38
- * opened with a pointer lands: the reader has not asked for a first choice, but the
39
- * keyboard still has to work from the very next keystroke — and every key below is
40
- * listened for on this element, so leaving the focus outside would silence all of them.
41
- */
36
+ /** Puts the focus on the panel itself, singling out no command. */
42
37
  declare function focusPanel(): void;
43
38
  declare const __VLS_base: import('vue').DefineComponent<MenuPanelProps, {
44
39
  /** Opens the panel. Pass the invoker, or the popover has no anchor to position against. */
@@ -1 +1 @@
1
- {"version":3,"file":"VMenuPanel.js","names":[],"sources":["../../../src/components/VMenu/VMenuPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @keyboard @core\n/**\n * The panel a menu is drawn in. Internal to VMenu, and the same component serves both\n * levels — the root menu and every submenu.\n *\n * It rests on `popover=\"auto\"`, which gives it light dismiss, positioning against its\n * invoker, and — a submenu being rendered inside its parent panel — a native stack the\n * browser closes from the outside in. Its look comes from the shared `.v-panel`.\n *\n * It carries `tabindex=\"-1\"` so it can hold the focus itself, which it does whenever the\n * menu was opened by POINTER: no command is singled out, but the keyboard still has\n * somewhere to arrive, everything below being listened for on this element.\n *\n * The JS is the ARIA menu keyboard, which the platform does not provide:\n *\n * - arrows and Home/End move focus item to item, CONFINED to the panel the focus is in — a\n * keystroke inside a submenu also bubbles through every panel containing it, hence the\n * guard on where the event came from;\n * - Escape closes the CURRENT LEVEL only, as does ArrowLeft in a submenu, handing focus\n * back to the item that opened it;\n * - Tab closes the whole menu, a menu not being something one tabs through.\n */\n\nimport { computed, inject, ref } from 'vue'\n\nimport { usePopover } from '../../composables/usePopover'\nimport { menuInvoker, menuKey } from './context'\nimport type { MenuPanelPlacement, MenuSize } from './context'\nimport { arrowNavigate } from '../../utils/arrowNav'\nimport { isRtl } from '../../utils/direction'\nimport { cssSize } from '../../utils/css'\n\ninterface MenuPanelProps {\n /** The panel's id, set by whoever owns it. It is what the trigger points at. */\n id: string\n /** Where the panel opens relative to whatever opened it. */\n placement?: MenuPanelPlacement\n /**\n * Marks this panel as a submenu, which enables the left arrow as a way back and keeps\n * the row-height settings off it, so that it inherits its parent's instead.\n */\n submenu?: boolean\n /**\n * The row height, set by the ROOT panel only. Submenus receive no value and inherit\n * it through CSS instead.\n */\n size?: MenuSize\n /** The reduced density, again set by the ROOT panel only and inherited through CSS. */\n compact?: boolean\n /**\n * An explicit width, set by the ROOT panel only. Submenus render no width attribute\n * and keep the default one, so an inherited value cannot reach them.\n */\n width?: number | string\n /**\n * Stops the panel from being narrower than its trigger, set by the ROOT panel only:\n * a submenu's trigger is a menu item, not a control worth matching.\n */\n matchTrigger?: boolean\n}\n\nconst props = withDefaults(defineProps<MenuPanelProps>(), {\n placement: 'bottom-start',\n submenu: false,\n size: undefined,\n compact: false,\n width: undefined,\n matchTrigger: false,\n})\n\nconst emit = defineEmits<{\n /**\n * The panel opened or closed. It is reported by the browser, so it also covers a\n * dismissal nobody asked for in code.\n */\n toggle: [open: boolean]\n}>()\n\ndefineSlots<{\n /** The contents of the panel: VMenuItem, VMenuGroup and VMenuSeparator. */\n default(): unknown\n}>()\n\nconst panelEl = ref<HTMLElement | null>(null)\n// The open state, the guards keeping the calls idempotent, and the option that gives\n// a programmatically opened submenu its anchor, all live in usePopover.\nconst { shown, syncShown, show, hide } = usePopover(panelEl)\n\n// Never absent: this panel is internal, rendered only by VMenu and by the VMenuItems inside\n// one, and VMenu provides the key before either exists.\nconst menu = inject(menuKey)!\n\nfunction onToggle(event: Event) {\n syncShown(event)\n emit('toggle', shown.value)\n}\n\n// @a11y @keyboard\n/**\n * The items the keyboard may move to. Two exclusions matter: `:disabled` only ever\n * matches a `<button>`, so an inert link is recognized by its `aria-disabled`\n * instead; and the final filter keeps only the items of THIS panel, since an open\n * submenu's items are DOM descendants of it and would otherwise join the list.\n */\nfunction items(): HTMLElement[] {\n const panel = panelEl.value\n if (!panel) return []\n return [\n ...panel.querySelectorAll<HTMLElement>(\n '[role=\"menuitem\"]:not(:disabled):not([aria-disabled=\"true\"])',\n ),\n ].filter((el) => el.closest('[role=\"menu\"]') === panel)\n}\n\n// @a11y\n/** Puts the focus on the first item the keyboard can reach, when the menu opens. */\nfunction focusFirst() {\n items()[0]?.focus()\n}\n\n// @a11y\n/**\n * Puts the focus on the panel itself, singling out no command. This is where a menu\n * opened with a pointer lands: the reader has not asked for a first choice, but the\n * keyboard still has to work from the very next keystroke — and every key below is\n * listened for on this element, so leaving the focus outside would silence all of them.\n */\nfunction focusPanel() {\n panelEl.value?.focus()\n}\n\n// @keyboard @a11y\nfunction onKeydown(event: KeyboardEvent) {\n const panel = panelEl.value\n // A keystroke inside a submenu passes through every panel containing it on its way\n // up, so only the panel the focused item DIRECTLY belongs to acts on it.\n if (!panel || (event.target as Element).closest('[role=\"menu\"]') !== panel) return\n // A key held with a modifier is the browser's: Alt+Left is Back.\n if (event.altKey || event.ctrlKey || event.metaKey) return\n\n if (event.key === 'Tab') {\n menu.closeAll()\n return\n }\n // The arrow pointing BACK at the parent closes a submenu: the right one in a\n // right-to-left page, where the submenu opened on the left.\n const back = isRtl(panel) ? 'ArrowRight' : 'ArrowLeft'\n if (event.key === 'Escape' || (props.submenu && event.key === back)) {\n // Escape closes THIS level and no more. The `preventDefault` matters: left to\n // itself the browser would close the popover on its own, without handing the\n // focus back to the item that opened it — and our own hide() would already have\n // closed it anyway. At the top level, VMenu is what returns the focus to the\n // trigger.\n event.preventDefault()\n hide()\n if (props.submenu) menuInvoker(props.id)?.focus()\n return\n }\n // Coming from the panel itself — a menu opened with a pointer, where no command is\n // singled out — the up arrow goes to the LAST item, entering the list from its far\n // end the way a menu is expected to. `utils/arrowNav` starts any arrow at the\n // beginning when nothing in the list has the focus, which is the right answer for the\n // tabs, the pages and the toggle group it also serves: it is this menu that has a\n // convention of its own, so it is handled here rather than in the shared helper.\n // The case only exists BECAUSE of the pointer opening: reached from an item, the\n // arrow never lands here.\n if (event.key === 'ArrowUp' && document.activeElement === panel) {\n const list = items()\n if (list.length > 0) {\n event.preventDefault()\n list[list.length - 1]?.focus()\n return\n }\n }\n // The movement itself is the shared one from `utils/arrowNav`, but it is handed OUR\n // list rather than letting it discover the items: ours also drops the disabled\n // links and stays inside this panel, neither of which the generic selector does.\n arrowNavigate(event, panel, items, { vertical: true })\n}\n\n// The only thing set inline is an explicit width, when the prop asks for one.\nconst resolvedWidth = computed(() => cssSize(props.width) || undefined)\nconst panelStyle = computed(() =>\n resolvedWidth.value ? { '--menu-width': resolvedWidth.value } : undefined,\n)\n\n// Internal to VMenu, which drives the panel from outside: opening has to be synchronous,\n// and where the focus lands depends on whether a pointer or the keyboard asked.\ndefineExpose({\n /** Opens the panel. Pass the invoker, or the popover has no anchor to position against. */\n show,\n /** Closes it. */\n close: hide,\n /** Whether it is open, as the browser last reported it. VMenu's model is checked against it. */\n shown,\n /** Puts the focus on the first reachable item: the keyboard's way in. */\n focusFirst,\n /** Puts the focus on the panel itself: the pointer's way in, singling out no command. */\n focusPanel,\n /** The panel element, which is the popover. */\n el: panelEl,\n})\n</script>\n\n<template>\n <div\n :id=\"id\"\n ref=\"panelEl\"\n popover=\"auto\"\n role=\"menu\"\n tabindex=\"-1\"\n class=\"v-overlay v-panel v-menu v-floating\"\n :class=\"{\n /*\n * The size class goes on the ROOT panel only. It sets the whole `--control-*`\n * family — height, paddings, gap, type, icon context — and the submenus, being\n * DOM descendants, inherit every one of them.\n *\n * Adding it to the submenus as well would BREAK them: the class recomputes the\n * height from the base one without knowing about the compact attribute, which\n * submenus do not carry, so a compact menu's submenus would silently snap back\n * to full height.\n */\n 'v-control': !submenu,\n }\"\n :data-placement=\"placement\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-width=\"resolvedWidth ? '' : undefined\"\n :data-match-trigger=\"matchTrigger ? '' : undefined\"\n :style=\"panelStyle\"\n @beforetoggle=\"syncShown\"\n @toggle=\"onToggle\"\n @keydown=\"onKeydown\"\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /* The panel's surface, border, shadow and inner rhythm come from the shared\n `.v-panel` class, and its dimensions from the `v-control` class the template sets\n on the root panel — there is no size table here, the rows reading the inherited\n variables directly. Only what is specific to a dropdown menu stays below. */\n .v-menu {\n min-inline-size: var(--vectis-control-size-menu-min);\n max-inline-size: min(var(--vectis-control-size-menu-max), calc(100dvi - var(--vectis-space-8)));\n }\n\n /* The panel is focusable, but only ever from code — its `tabindex` is -1, so the Tab\n key never brings anyone here. It holds the focus when the menu was opened with a\n pointer, and a ring drawn around the whole panel would then read as the reader\n having landed on something, which is the opposite of what that state means: no\n command is singled out. The rows keep their own highlight, `.v-menu-item:focus`. */\n .v-menu:focus {\n outline: none;\n }\n\n /* Pulls a submenu up so that its first item lines up with the item that opened it,\n by exactly the panel's own padding plus its border. */\n .v-menu .v-menu[data-placement='right-start'] {\n margin-block-start: calc(-1 * (var(--vectis-space-1) + 1px));\n }\n\n /* An explicit width, which only the ROOT panel ever renders: a submenu sets no such\n attribute, so the variable it inherits stays inert there. The ceiling that keeps\n a panel inside the viewport is untouched. */\n .v-menu[data-width] {\n min-inline-size: 0;\n inline-size: var(--menu-width);\n }\n\n /* The panel may not be narrower than its trigger. This works with no anchor name of\n any kind, because the button that opened the panel is already its implicit anchor\n — VCombobox and VTimeInput, anchored to a text input, have to name theirs. Like\n the width above, only the ROOT panel renders it.\n\n It is placed AFTER the width rule, at equal specificity, so that when both are\n given the floor wins. A trigger wider than the ceiling therefore widens the panel\n past it, a minimum width beating a maximum one — which is intended. */\n .v-menu[data-match-trigger] {\n min-inline-size: anchor-size(width);\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VMenuPanel.js","names":[],"sources":["../../../src/components/VMenu/VMenuPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @keyboard @core\n/**\n * Native auto popovers form the submenu stack. JavaScript maintains open models, roving focus\n * and explicit source invokers when opening imperatively.\n */\n\nimport { computed, inject, ref } from 'vue'\n\nimport { usePopover } from '../../composables/usePopover'\nimport { menuInvoker, menuKey } from './context'\nimport type { MenuPanelPlacement, MenuSize } from './context'\nimport { arrowNavigate } from '../../utils/arrowNav'\nimport { isRtl } from '../../utils/direction'\nimport { cssSize } from '../../utils/css'\n\ninterface MenuPanelProps {\n /** The panel's id, set by whoever owns it. It is what the trigger points at. */\n id: string\n /** Where the panel opens relative to whatever opened it. */\n placement?: MenuPanelPlacement\n /**\n * Marks this panel as a submenu, which enables the left arrow as a way back and keeps\n * the row-height settings off it, so that it inherits its parent's instead.\n */\n submenu?: boolean\n /**\n * The row height, set by the ROOT panel only. Submenus receive no value and inherit\n * it through CSS instead.\n */\n size?: MenuSize\n /** The reduced density, again set by the ROOT panel only and inherited through CSS. */\n compact?: boolean\n /**\n * An explicit width, set by the ROOT panel only. Submenus render no width attribute\n * and keep the default one, so an inherited value cannot reach them.\n */\n width?: number | string\n /**\n * Stops the panel from being narrower than its trigger, set by the ROOT panel only:\n * a submenu's trigger is a menu item, not a control worth matching.\n */\n matchTrigger?: boolean\n}\n\nconst props = withDefaults(defineProps<MenuPanelProps>(), {\n placement: 'bottom-start',\n submenu: false,\n size: undefined,\n compact: false,\n width: undefined,\n matchTrigger: false,\n})\n\nconst emit = defineEmits<{\n /**\n * The panel opened or closed. It is reported by the browser, so it also covers a\n * dismissal nobody asked for in code.\n */\n toggle: [open: boolean]\n}>()\n\ndefineSlots<{\n /** The contents of the panel: VMenuItem, VMenuGroup and VMenuSeparator. */\n default(): unknown\n}>()\n\nconst panelEl = ref<HTMLElement | null>(null)\n// The open state, the guards keeping the calls idempotent, and the option that gives\n// a programmatically opened submenu its anchor, all live in usePopover.\nconst { shown, syncShown, show, hide } = usePopover(panelEl)\n\n// Never absent: this panel is internal, rendered only by VMenu and by the VMenuItems inside\n// one, and VMenu provides the key before either exists.\nconst menu = inject(menuKey)!\n\nfunction onToggle(event: Event) {\n syncShown(event)\n emit('toggle', shown.value)\n}\n\n// @a11y @keyboard\n/**\n * The items the keyboard may move to. Two exclusions matter: `:disabled` only ever matches a\n * `<button>`, so an inert link is recognized by its `aria-disabled` instead; and the final\n * filter keeps only the items of this panel, since an open submenu's items are DOM descendants\n * of it and would otherwise join the list.\n */\nfunction items(): HTMLElement[] {\n const panel = panelEl.value\n if (!panel) return []\n return [\n ...panel.querySelectorAll<HTMLElement>(\n '[role=\"menuitem\"]:not(:disabled):not([aria-disabled=\"true\"])',\n ),\n ].filter((el) => el.closest('[role=\"menu\"]') === panel)\n}\n\n// @a11y\n/** Puts the focus on the first item the keyboard can reach, when the menu opens. */\nfunction focusFirst() {\n items()[0]?.focus()\n}\n\n// @a11y\n/** Puts the focus on the panel itself, singling out no command. */\nfunction focusPanel() {\n panelEl.value?.focus()\n}\n\n// @keyboard @a11y\nfunction onKeydown(event: KeyboardEvent) {\n const panel = panelEl.value\n // A keystroke inside a submenu passes through every panel containing it on its way\n // up, so only the panel the focused item DIRECTLY belongs to acts on it.\n if (!panel || (event.target as Element).closest('[role=\"menu\"]') !== panel) return\n if (event.altKey || event.ctrlKey || event.metaKey) return\n\n if (event.key === 'Tab') {\n menu.closeAll()\n return\n }\n // The arrow pointing BACK at the parent closes a submenu: the right one in a\n // right-to-left page, where the submenu opened on the left.\n const back = isRtl(panel) ? 'ArrowRight' : 'ArrowLeft'\n if (event.key === 'Escape' || (props.submenu && event.key === back)) {\n // The `preventDefault` matters: left to itself the browser would close the popover on its\n // own, without handing the focus back to the item that opened it; and our own hide() would\n // already have closed it anyway. At the top level, VMenu is what returns the focus to the\n // trigger.\n event.preventDefault()\n hide()\n if (props.submenu) menuInvoker(props.id)?.focus()\n return\n }\n // `utils/arrowNav` starts any arrow at the beginning when nothing in the list has the focus,\n // which is the right answer for the tabs, the pages and the toggle group it also serves: it\n // is this menu that has a convention of its own, so it is handled here rather than in the\n // shared helper. The case only exists BECAUSE of the pointer opening: reached from an item,\n // the arrow never lands here.\n if (event.key === 'ArrowUp' && document.activeElement === panel) {\n const list = items()\n if (list.length > 0) {\n event.preventDefault()\n list[list.length - 1]?.focus()\n return\n }\n }\n // The movement itself is the shared one from `utils/arrowNav`, but it is handed OUR\n // list rather than letting it discover the items: ours also drops the disabled\n // links and stays inside this panel, neither of which the generic selector does.\n arrowNavigate(event, panel, items, { vertical: true })\n}\n\nconst resolvedWidth = computed(() => cssSize(props.width))\n\n// Internal to VMenu, which drives the panel from outside: opening has to be synchronous,\n// and where the focus lands depends on whether a pointer or the keyboard asked.\ndefineExpose({\n /** Opens the panel. Pass the invoker, or the popover has no anchor to position against. */\n show,\n /** Closes it. */\n close: hide,\n /** Whether it is open, as the browser last reported it. VMenu's model is checked against it. */\n shown,\n /** Puts the focus on the first reachable item: the keyboard's way in. */\n focusFirst,\n /** Puts the focus on the panel itself: the pointer's way in, singling out no command. */\n focusPanel,\n /** The panel element, which is the popover. */\n el: panelEl,\n})\n</script>\n\n<template>\n <div\n :id=\"id\"\n ref=\"panelEl\"\n popover=\"auto\"\n role=\"menu\"\n tabindex=\"-1\"\n class=\"v-overlay v-panel v-menu v-floating\"\n :class=\"{\n /*\n * It sets the whole `--control-*` family; height, paddings, gap, type, icon context; and\n * the submenus, being DOM descendants, inherit every one of them. Adding it to the\n * submenus as well would BREAK them: the class recomputes the height from the base one\n * without knowing about the compact attribute, which submenus do not carry, so a compact\n * menu's submenus would silently snap back to full height.\n */\n 'v-control': !submenu,\n }\"\n :data-placement=\"placement\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-width=\"resolvedWidth ? '' : undefined\"\n :data-match-trigger=\"matchTrigger ? '' : undefined\"\n :style=\"{ '--menu-width': resolvedWidth }\"\n @beforetoggle=\"syncShown\"\n @toggle=\"onToggle\"\n @keydown=\"onKeydown\"\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * The panel's surface, border, shadow and inner rhythm come from the shared `.v-panel` class,\n * and its dimensions from the `v-control` class the template sets on the root panel; there is\n * no size table here, the rows reading the inherited variables directly. Only what is\n * specific to a dropdown menu stays below.\n */\n .v-menu {\n min-inline-size: var(--vectis-control-size-menu-min);\n max-inline-size: min(var(--vectis-control-size-menu-max), calc(100dvi - var(--vectis-space-8)));\n }\n\n /*\n * The panel is focusable, but only ever from code; its `tabindex` is -1, so the Tab key never\n * brings anyone here. It holds the focus when the menu was opened with a pointer, and a ring\n * drawn around the whole panel would then read as the reader having landed on something,\n * which is the opposite of what that state means: no command is singled out.\n */\n .v-menu:focus {\n outline: none;\n }\n\n /* Pulls a submenu up so that its first item lines up with the item that opened it,\n by exactly the panel's own padding plus its border. */\n .v-menu .v-menu[data-placement='right-start'] {\n margin-block-start: calc(-1 * (var(--vectis-space-1) + 1px));\n }\n\n /* An explicit width, which only the ROOT panel ever renders: a submenu sets no such\n attribute, so the variable it inherits stays inert there. The ceiling that keeps\n a panel inside the viewport is untouched. */\n .v-menu[data-width] {\n min-inline-size: 0;\n inline-size: var(--menu-width);\n }\n\n /*\n * This works with no anchor name of any kind, because the button that opened the panel is\n * already its implicit anchor; VCombobox and VTimeInput, anchored to a text input, have to\n * name theirs. Like the width above, only the ROOT panel renders it.\n */\n .v-menu[data-match-trigger] {\n min-inline-size: anchor-size(width);\n }\n}\n</style>\n"],"mappings":""}
@@ -69,7 +69,7 @@ var g = [
69
69
  }
70
70
  r(e, t, O, { vertical: !0 });
71
71
  }
72
- let M = o(() => e(y.width) || void 0), N = o(() => M.value ? { "--menu-width": M.value } : void 0);
72
+ let M = o(() => e(y.width));
73
73
  return _({
74
74
  show: w,
75
75
  close: T,
@@ -90,7 +90,7 @@ var g = [
90
90
  "data-compact": c.compact ? "" : void 0,
91
91
  "data-width": M.value ? "" : void 0,
92
92
  "data-match-trigger": c.matchTrigger ? "" : void 0,
93
- style: d(N.value),
93
+ style: d({ "--menu-width": M.value }),
94
94
  onBeforetoggle: t[0] ||= (...e) => h(C) && h(C)(...e),
95
95
  onToggle: D,
96
96
  onKeydown: j