@morscherlab/mint-sdk 1.2.0-beta.7 → 1.2.0-beta.9

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 (307) hide show
  1. package/README.md +30 -0
  2. package/dist/{ExperimentPopover-CfOdh5Rm.js → ExperimentPopover-oL0fg_A0.js} +5 -3
  3. package/dist/ExperimentPopover-oL0fg_A0.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-DvIagrxY.js → ExperimentSelectorModal-BeEOomw_.js} +4 -4
  5. package/dist/ExperimentSelectorModal-BeEOomw_.js.map +1 -0
  6. package/dist/{SettingsModal-W3TYfnP9.js → SettingsModal-Dphh3RrI.js} +4 -4
  7. package/dist/SettingsModal-Dphh3RrI.js.map +1 -0
  8. package/dist/{EmptyState-NBqf2_8z.js → Skeleton-DxtwJ5gt.js} +110 -92
  9. package/dist/Skeleton-DxtwJ5gt.js.map +1 -0
  10. package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
  11. package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
  12. package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
  13. package/dist/components/ActionMenu.vue.d.ts +1 -1
  14. package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
  15. package/dist/components/AuditTrail.vue.d.ts +13 -0
  16. package/dist/components/BatchProgressList.vue.d.ts +17 -0
  17. package/dist/components/DataFrame.vue.d.ts +14 -2
  18. package/dist/components/DatePicker.vue.d.ts +1 -1
  19. package/dist/components/DateTimePicker.vue.d.ts +2 -0
  20. package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
  21. package/dist/components/ExperimentPopover.vue.d.ts +1 -0
  22. package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
  23. package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
  24. package/dist/components/ProgressBar.vue.d.ts +1 -1
  25. package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
  26. package/dist/components/ReagentList.vue.d.ts +29 -1
  27. package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
  28. package/dist/components/SampleSelector.drag.d.ts +13 -0
  29. package/dist/components/SampleSelector.tree.d.ts +2 -0
  30. package/dist/components/SampleSelector.vue.d.ts +34 -1
  31. package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
  32. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -0
  33. package/dist/components/TimePicker.vue.d.ts +1 -0
  34. package/dist/components/WellPlate.geometry.d.ts +15 -0
  35. package/dist/components/WellPlate.interaction.d.ts +13 -0
  36. package/dist/components/WellPlate.vue.d.ts +29 -0
  37. package/dist/components/index.js +6 -6
  38. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
  39. package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
  40. package/dist/{components-B80gV0P6.js → components-CgAUzF8F.js} +1930 -1688
  41. package/dist/components-CgAUzF8F.js.map +1 -0
  42. package/dist/composables/controlSchemaTypes.d.ts +34 -0
  43. package/dist/composables/index.d.ts +1 -0
  44. package/dist/composables/index.js +9 -9
  45. package/dist/composables/useDropdownState.d.ts +18 -2
  46. package/dist/composables/useEventStream.d.ts +9 -0
  47. package/dist/composables/useListReorder.d.ts +90 -0
  48. package/dist/composables/useMenuKeyboard.d.ts +2 -2
  49. package/dist/composables/usePluginConfig.d.ts +3 -1
  50. package/dist/composables/useRequestSyncState.d.ts +42 -2
  51. package/dist/composables/useScheduleDrag.d.ts +1 -0
  52. package/dist/{composables-C8mmD5ta.js → composables-B_pia3hO.js} +13 -69
  53. package/dist/composables-B_pia3hO.js.map +1 -0
  54. package/dist/{useApi-CDBq6z23.js → errors-_W0-LbA6.js} +102 -3
  55. package/dist/errors-_W0-LbA6.js.map +1 -0
  56. package/dist/{experiment-Y9Iz7KYi.js → experiment-Cwl9AYxw.js} +3 -8
  57. package/dist/experiment-Cwl9AYxw.js.map +1 -0
  58. package/dist/experiment-utils-DV3DrvE_.js.map +1 -1
  59. package/dist/index.js +14 -14
  60. package/dist/install.js +2 -7
  61. package/dist/install.js.map +1 -1
  62. package/dist/permissions.d.ts +42 -1
  63. package/dist/permissions.js +20 -10
  64. package/dist/permissions.js.map +1 -1
  65. package/dist/stores/index.d.ts +1 -1
  66. package/dist/stores/index.js +3 -3
  67. package/dist/stores/settings.d.ts +31 -1
  68. package/dist/styles.css +9572 -27877
  69. package/dist/templates/index.js +2 -4
  70. package/dist/{templates-CK72aKO1.js → templates-Qq113pyZ.js} +291 -85
  71. package/dist/templates-Qq113pyZ.js.map +1 -0
  72. package/dist/{useControlSchema-_OVhavKq.js → useControlSchema-aMZlb4Sc.js} +197 -1089
  73. package/dist/useControlSchema-aMZlb4Sc.js.map +1 -0
  74. package/dist/{useDropdownState-D5G4UvWd.js → useDropdownState-DitCtrTT.js} +21 -5
  75. package/dist/useDropdownState-DitCtrTT.js.map +1 -0
  76. package/dist/{useExperimentSelector-DSEckf_g.js → useExperimentSelector-I8HC8hUu.js} +3 -3
  77. package/dist/{useExperimentSelector-DSEckf_g.js.map → useExperimentSelector-I8HC8hUu.js.map} +1 -1
  78. package/dist/{useFormBuilder-Bd714C7y.js → useFormBuilder-TCxNnqeb.js} +2 -2
  79. package/dist/{useFormBuilder-Bd714C7y.js.map → useFormBuilder-TCxNnqeb.js.map} +1 -1
  80. package/dist/{useProtocolTemplates-Dq5Pzwyd.js → useProtocolTemplates-BmX_iVBR.js} +185 -157
  81. package/dist/useProtocolTemplates-BmX_iVBR.js.map +1 -0
  82. package/dist/utils/color.d.ts +13 -0
  83. package/dist/utils/errors.d.ts +9 -0
  84. package/package.json +1 -1
  85. package/src/__tests__/components/ActionMenu.test.ts +94 -0
  86. package/src/__tests__/components/DataFrame.test.ts +183 -11
  87. package/src/__tests__/components/DataStates.test.ts +176 -0
  88. package/src/__tests__/components/DatePicker.test.ts +76 -0
  89. package/src/__tests__/components/DateTimePicker.test.ts +36 -0
  90. package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
  91. package/src/__tests__/components/GroupAssigner.test.ts +53 -0
  92. package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
  93. package/src/__tests__/components/RackEditor.test.ts +35 -0
  94. package/src/__tests__/components/ReagentList.test.ts +64 -2
  95. package/src/__tests__/components/RetiredStyles.test.ts +31 -0
  96. package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
  97. package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
  98. package/src/__tests__/components/SegmentedControl.test.ts +88 -0
  99. package/src/__tests__/components/TimePicker.test.ts +61 -0
  100. package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
  101. package/src/__tests__/composables/useDropdownState.test.ts +58 -1
  102. package/src/__tests__/composables/useListReorder.test.ts +145 -0
  103. package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
  104. package/src/__tests__/stores/settings.test.ts +71 -1
  105. package/src/__tests__/styles/cssTokens.test.ts +131 -0
  106. package/src/__tests__/utils/permissions.test.ts +68 -0
  107. package/src/components/ActionMenu.vue +45 -88
  108. package/src/components/AlertBox.vue +0 -4
  109. package/src/components/AppAvatarMenu.vue +2 -14
  110. package/src/components/AppLayout.vue +0 -4
  111. package/src/components/AppPluginSwitcher.story.vue +87 -0
  112. package/src/components/AppPluginSwitcher.vue +2 -5
  113. package/src/components/AppSidebar.vue +0 -4
  114. package/src/components/AppToastContainer.vue +6 -8
  115. package/src/components/AppTopBar.vue +0 -4
  116. package/src/components/ArtifactSaveDialog.vue +0 -4
  117. package/src/components/ArtifactSelectorModal.vue +0 -4
  118. package/src/components/AuditTrail.story.vue +18 -0
  119. package/src/components/AuditTrail.vue +27 -7
  120. package/src/components/AutoGroupModal.vue +0 -4
  121. package/src/components/Avatar.vue +0 -4
  122. package/src/components/BaseButton.vue +0 -4
  123. package/src/components/BaseCheckbox.vue +0 -4
  124. package/src/components/BaseInput.vue +0 -4
  125. package/src/components/BaseModal.vue +0 -4
  126. package/src/components/BasePill.vue +0 -4
  127. package/src/components/BaseRadioGroup.vue +0 -4
  128. package/src/components/BaseSelect.vue +0 -4
  129. package/src/components/BaseSlider.vue +0 -4
  130. package/src/components/BaseTabs.vue +0 -4
  131. package/src/components/BaseTextarea.vue +0 -4
  132. package/src/components/BaseToggle.vue +0 -4
  133. package/src/components/BatchProgressList.story.vue +18 -0
  134. package/src/components/BatchProgressList.vue +32 -4
  135. package/src/components/Breadcrumb.vue +0 -4
  136. package/src/components/Calendar.vue +0 -4
  137. package/src/components/ChartContainer.story.vue +1 -1
  138. package/src/components/ChartContainer.vue +0 -4
  139. package/src/components/ChemicalFormula.vue +0 -4
  140. package/src/components/CollapsibleCard.vue +0 -4
  141. package/src/components/ColorSlider.vue +0 -4
  142. package/src/components/ConcentrationInput.vue +0 -4
  143. package/src/components/ConfirmDialog.vue +0 -4
  144. package/src/components/DataFrame.story.vue +14 -0
  145. package/src/components/DataFrame.vue +97 -23
  146. package/src/components/DatePicker.vue +73 -27
  147. package/src/components/DateTimePicker.vue +45 -17
  148. package/src/components/Divider.vue +0 -4
  149. package/src/components/DoseCalculator.vue +0 -4
  150. package/src/components/DropdownButton.vue +2 -14
  151. package/src/components/EmptyState.vue +0 -4
  152. package/src/components/ExperimentCodeBadge.vue +0 -4
  153. package/src/components/ExperimentDataViewer.story.vue +3 -0
  154. package/src/components/ExperimentDataViewer.vue +33 -13
  155. package/src/components/ExperimentPopover.vue +2 -5
  156. package/src/components/ExperimentSelectorModal.vue +0 -4
  157. package/src/components/ExperimentTimeline.story.vue +18 -0
  158. package/src/components/ExperimentTimeline.vue +81 -75
  159. package/src/components/FileUploader.vue +0 -4
  160. package/src/components/FitPanel.vue +0 -4
  161. package/src/components/FormActions.story.vue +96 -0
  162. package/src/components/FormActions.vue +0 -4
  163. package/src/components/FormBuilder.vue +0 -4
  164. package/src/components/FormField.vue +0 -4
  165. package/src/components/FormulaInput.vue +0 -4
  166. package/src/components/GeneratedPathInput.story.vue +68 -0
  167. package/src/components/GeneratedPlotlyResult.story.vue +96 -0
  168. package/src/components/GeneratedPlotlyResult.vue +3 -3
  169. package/src/components/GeneratedPluginView.story.vue +145 -0
  170. package/src/components/GeneratedPluginView.vue +10 -10
  171. package/src/components/GeneratedResultRenderer.story.vue +117 -0
  172. package/src/components/GeneratedResultRenderer.vue +4 -4
  173. package/src/components/GroupAssigner.vue +67 -5
  174. package/src/components/IconButton.vue +0 -4
  175. package/src/components/InstrumentAlertLog.vue +0 -4
  176. package/src/components/InstrumentStateBadge.vue +0 -4
  177. package/src/components/InstrumentStatusCard.vue +0 -4
  178. package/src/components/JobsStatusTray.vue +0 -4
  179. package/src/components/LcmsSequenceTable.vue +81 -50
  180. package/src/components/LoadingSpinner.vue +0 -4
  181. package/src/components/MobileSupportGate.vue +0 -4
  182. package/src/components/MoleculeInput.vue +0 -4
  183. package/src/components/MultiSelect.vue +0 -4
  184. package/src/components/NumberInput.vue +0 -4
  185. package/src/components/PlateMapEditor.vue +3 -14
  186. package/src/components/PluginIcon.vue +0 -4
  187. package/src/components/ProgressBar.vue +0 -4
  188. package/src/components/ProtocolStepEditor.vue +2 -4
  189. package/src/components/RackEditor.vue +51 -70
  190. package/src/components/ReagentEditor.vue +0 -4
  191. package/src/components/ReagentList.story.vue +18 -0
  192. package/src/components/ReagentList.vue +127 -82
  193. package/src/components/ResourceCard.vue +0 -4
  194. package/src/components/RuntimeUpdateNotice.story.vue +49 -0
  195. package/src/components/RuntimeUpdateNotice.vue +4 -4
  196. package/src/components/SampleHierarchyTree.story.vue +18 -0
  197. package/src/components/SampleHierarchyTree.vue +32 -9
  198. package/src/components/SampleLegend.vue +2 -8
  199. package/src/components/SampleSelector.drag.ts +51 -0
  200. package/src/components/SampleSelector.story.vue +6 -0
  201. package/src/components/SampleSelector.tree.ts +2 -0
  202. package/src/components/SampleSelector.vue +49 -10
  203. package/src/components/SampleSelectorGroupNode.vue +19 -0
  204. package/src/components/ScheduleCalendar.story.vue +6 -0
  205. package/src/components/ScheduleCalendar.vue +82 -7
  206. package/src/components/ScientificNumber.vue +0 -4
  207. package/src/components/SectionCard.vue +0 -4
  208. package/src/components/SegmentedControl.vue +48 -5
  209. package/src/components/SequenceInput.vue +0 -4
  210. package/src/components/SequenceProgressBar.story.vue +110 -0
  211. package/src/components/SequenceProgressBar.vue +0 -4
  212. package/src/components/SettingsModal.vue +0 -4
  213. package/src/components/Skeleton.vue +0 -4
  214. package/src/components/SmartGroupFieldRecipe.vue +8 -6
  215. package/src/components/SmartGroupManual.vue +0 -4
  216. package/src/components/StatusIndicator.vue +0 -4
  217. package/src/components/StepWizard.vue +0 -4
  218. package/src/components/TagsInput.vue +4 -6
  219. package/src/components/ThemeToggle.vue +0 -4
  220. package/src/components/TimePicker.vue +36 -7
  221. package/src/components/TimeRangeInput.vue +0 -4
  222. package/src/components/Tooltip.story.vue +3 -3
  223. package/src/components/Tooltip.vue +0 -4
  224. package/src/components/UnitInput.vue +0 -4
  225. package/src/components/WellPlate.geometry.ts +60 -1
  226. package/src/components/WellPlate.interaction.ts +44 -0
  227. package/src/components/WellPlate.story.vue +8 -0
  228. package/src/components/WellPlate.vue +53 -7
  229. package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
  230. package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
  231. package/src/components/internal/DataStateInternal.vue +74 -0
  232. package/src/components/internal/FormSectionRenderer.vue +0 -4
  233. package/src/components/internal/WellEditPopupInternal.vue +0 -4
  234. package/src/composables/controlSchemaTypes.ts +34 -0
  235. package/src/composables/index.ts +10 -0
  236. package/src/composables/useDropdownState.ts +35 -5
  237. package/src/composables/useEventStream.ts +14 -2
  238. package/src/composables/useFileImport.ts +2 -7
  239. package/src/composables/useGeneratedAnalysis.ts +8 -9
  240. package/src/composables/useListReorder.story.vue +143 -0
  241. package/src/composables/useListReorder.ts +242 -0
  242. package/src/composables/useMenuKeyboard.ts +2 -2
  243. package/src/composables/useOptimisticMutation.ts +1 -15
  244. package/src/composables/usePluginConfig.ts +3 -1
  245. package/src/composables/useRackEditor.ts +2 -11
  246. package/src/composables/useRequestSyncState.ts +98 -28
  247. package/src/composables/useResourceCrud.ts +1 -15
  248. package/src/composables/useScheduleDrag.ts +37 -0
  249. package/src/composables/useWellPainting.ts +5 -29
  250. package/src/composables/useWellPlateEditor.ts +1 -11
  251. package/src/permissions.ts +48 -9
  252. package/src/stores/experiment.ts +2 -7
  253. package/src/stores/index.ts +9 -1
  254. package/src/stores/settings.ts +93 -1
  255. package/src/styles/components/audit-trail.css +5 -5
  256. package/src/styles/components/batch-progress-list.css +7 -0
  257. package/src/styles/components/color-slider.css +2 -2
  258. package/src/styles/components/data-state.css +30 -0
  259. package/src/styles/components/dataframe.css +29 -35
  260. package/src/styles/components/experiment-data-viewer.css +4 -4
  261. package/src/styles/components/experiment-timeline.css +3 -30
  262. package/src/styles/components/form-builder.css +5 -5
  263. package/src/styles/components/lcms-sequence-table.css +16 -1
  264. package/src/styles/components/multi-select.css +1 -1
  265. package/src/styles/components/reagent-list.css +38 -31
  266. package/src/styles/components/sample-hierarchy-tree.css +5 -19
  267. package/src/styles/components/sample-selector.css +9 -2
  268. package/src/styles/components/schedule-calendar.css +8 -0
  269. package/src/styles/components/scientific-number.css +1 -1
  270. package/src/styles/components/well-plate.css +8 -0
  271. package/src/styles/index.css +8 -0
  272. package/src/styles/variables.css +29 -0
  273. package/src/templates/builderPresetControls.ts +1 -1
  274. package/src/templates/componentPlateHelpers.ts +3 -1
  275. package/src/templates/doseResponseAdapters.ts +2 -13
  276. package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
  277. package/src/templates/plateMapBuilder.ts +2 -2
  278. package/src/templates/qpcrPlateBuilder.ts +1 -1
  279. package/src/templates/templateValidators.ts +1 -1
  280. package/src/templates/timeCourseBuilder.ts +2 -2
  281. package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
  282. package/src/utils/color.ts +51 -0
  283. package/src/utils/errors.ts +29 -0
  284. package/dist/EmptyState-NBqf2_8z.js.map +0 -1
  285. package/dist/ExperimentPopover-CfOdh5Rm.js.map +0 -1
  286. package/dist/ExperimentSelectorModal-DvIagrxY.js.map +0 -1
  287. package/dist/SettingsModal-W3TYfnP9.js.map +0 -1
  288. package/dist/components-B80gV0P6.js.map +0 -1
  289. package/dist/composables/autoGroupConstants.d.ts +0 -1
  290. package/dist/composables/autoGroupGrouping.d.ts +0 -3
  291. package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
  292. package/dist/composables-C8mmD5ta.js.map +0 -1
  293. package/dist/experiment-Y9Iz7KYi.js.map +0 -1
  294. package/dist/templates/builderDefaults.d.ts +0 -1
  295. package/dist/templates/plateGeometry.d.ts +0 -4
  296. package/dist/templates-CK72aKO1.js.map +0 -1
  297. package/dist/useApi-CDBq6z23.js.map +0 -1
  298. package/dist/useControlSchema-_OVhavKq.js.map +0 -1
  299. package/dist/useDropdownState-D5G4UvWd.js.map +0 -1
  300. package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +0 -1
  301. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
  302. package/src/composables/autoGroupConstants.ts +0 -4
  303. package/src/composables/autoGroupGrouping.ts +0 -148
  304. package/src/composables/useAutoGroupInputSources.ts +0 -147
  305. package/src/templates/builderDefaults.ts +0 -11
  306. package/src/templates/plateGeometry.ts +0 -62
  307. /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → components/DataStates.test.d.ts} +0 -0
@@ -45,4 +45,40 @@ describe('DateTimePicker', () => {
45
45
 
46
46
  wrapper.unmount()
47
47
  })
48
+
49
+ it('opens from the keyboard, focuses the calendar, and restores focus on close', async () => {
50
+ const wrapper = mount(DateTimePicker, {
51
+ props: { modelValue: '2026-04-22T14:30' },
52
+ attachTo: document.body,
53
+ })
54
+ const input = wrapper.get<HTMLInputElement>('input').element
55
+
56
+ await wrapper.get('input').trigger('keydown', { key: 'ArrowDown' })
57
+ await nextTick()
58
+ await nextTick()
59
+ await nextTick()
60
+
61
+ expect(wrapper.find('.mint-datetime-picker__dropdown').exists()).toBe(true)
62
+ expect(document.activeElement).toBe(wrapper.get('.mint-date-picker__day--selected').element)
63
+
64
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
65
+ await nextTick()
66
+ await nextTick()
67
+
68
+ expect(document.activeElement).toBe(input)
69
+
70
+ wrapper.unmount()
71
+ })
72
+
73
+ it('marks the panel as a non-modal dialog', async () => {
74
+ const wrapper = mount(DateTimePicker, { attachTo: document.body })
75
+
76
+ await wrapper.get('input').trigger('click')
77
+
78
+ const panel = wrapper.get('.mint-datetime-picker__dropdown')
79
+ expect(panel.attributes('role')).toBe('dialog')
80
+ expect(panel.attributes('aria-modal')).toBeUndefined()
81
+
82
+ wrapper.unmount()
83
+ })
48
84
  })
@@ -0,0 +1,63 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { describe, expect, it } from 'vitest'
3
+ import ExperimentTimeline from '../../components/ExperimentTimeline.vue'
4
+ import type { ProtocolStep } from '../../types'
5
+
6
+ function makeSteps(): ProtocolStep[] {
7
+ return [
8
+ { id: 's1', name: 'Thaw', type: 'incubation', status: 'pending', order: 0 },
9
+ { id: 's2', name: 'Quench', type: 'incubation', status: 'pending', order: 1 },
10
+ { id: 's3', name: 'Extract', type: 'incubation', status: 'pending', order: 2 },
11
+ ]
12
+ }
13
+
14
+ describe('ExperimentTimeline', () => {
15
+ it('renders steps in order', () => {
16
+ const wrapper = mount(ExperimentTimeline, { props: { modelValue: makeSteps() } })
17
+
18
+ expect(wrapper.findAll('.mint-timeline__title').map(title => title.text()))
19
+ .toEqual(['Thaw', 'Quench', 'Extract'])
20
+ })
21
+
22
+ it('reorders a step with Alt and the arrow keys while editable', async () => {
23
+ const wrapper = mount(ExperimentTimeline, {
24
+ props: { modelValue: makeSteps(), editable: true },
25
+ })
26
+
27
+ const first = wrapper.findAll('.mint-timeline__step')[0]
28
+ expect(first.attributes('tabindex')).toBe('0')
29
+ expect(first.attributes('aria-label')).toContain('Press Alt with the arrow keys')
30
+
31
+ await first.trigger('keydown', { key: 'ArrowDown', altKey: true })
32
+
33
+ expect(wrapper.emitted('step-reorder')?.[0]).toEqual(['s1', 1])
34
+ const steps = wrapper.emitted('update:modelValue')?.[0]?.[0] as ProtocolStep[]
35
+ expect(steps.map(step => step.id)).toEqual(['s2', 's1', 's3'])
36
+ expect(steps.map(step => step.order)).toEqual([0, 1, 2])
37
+ })
38
+
39
+ it('announces the move politely', async () => {
40
+ const wrapper = mount(ExperimentTimeline, {
41
+ props: { modelValue: makeSteps(), editable: true },
42
+ })
43
+
44
+ await wrapper.findAll('.mint-timeline__step')[2]
45
+ .trigger('keydown', { key: 'ArrowUp', altKey: true })
46
+ await wrapper.vm.$nextTick()
47
+
48
+ const liveRegion = wrapper.get('[role="status"]')
49
+ expect(liveRegion.attributes('aria-live')).toBe('polite')
50
+ expect(liveRegion.text()).toBe('Extract moved to position 2 of 3.')
51
+ })
52
+
53
+ it('is not keyboard reorderable when not editable', async () => {
54
+ const wrapper = mount(ExperimentTimeline, { props: { modelValue: makeSteps() } })
55
+
56
+ const first = wrapper.findAll('.mint-timeline__step')[0]
57
+ expect(first.attributes('tabindex')).toBeUndefined()
58
+
59
+ await first.trigger('keydown', { key: 'ArrowDown', altKey: true })
60
+
61
+ expect(wrapper.emitted('step-reorder')).toBeUndefined()
62
+ })
63
+ })
@@ -45,4 +45,57 @@ describe('GroupAssigner', () => {
45
45
  wrapper.find('.mint-group-assigner__zone').trigger('dragover', { dataTransfer: null }),
46
46
  ).resolves.toBeUndefined()
47
47
  })
48
+
49
+ it('assigns and unassigns groups with Alt and the arrow keys', async () => {
50
+ const wrapper = mount(GroupAssigner, {
51
+ props: { groups, group1: [], group2: [] },
52
+ })
53
+
54
+ const pill = wrapper.get('.mint-group-assigner__pill--unassigned')
55
+ expect(pill.attributes('tabindex')).toBe('0')
56
+ expect(pill.attributes('aria-label')).toContain('Available Groups')
57
+
58
+ await pill.trigger('keydown', { key: 'ArrowLeft', altKey: true })
59
+ expect(wrapper.emitted('update:group1')?.[0]).toEqual([['control-a']])
60
+
61
+ await wrapper.get('.mint-group-assigner__pill--unassigned')
62
+ .trigger('keydown', { key: 'ArrowRight', altKey: true })
63
+ expect(wrapper.emitted('update:group2')?.at(-1)).toEqual([['control-a']])
64
+ })
65
+
66
+ it('moves an assigned group back to the unassigned pool', async () => {
67
+ const wrapper = mount(GroupAssigner, {
68
+ props: { groups, group1: ['control-a'], group2: [] },
69
+ })
70
+
71
+ await wrapper.get('.mint-group-assigner__zone--1 .mint-group-assigner__pill')
72
+ .trigger('keydown', { key: 'ArrowRight', altKey: true })
73
+
74
+ expect(wrapper.emitted('update:group1')?.[0]).toEqual([[]])
75
+ })
76
+
77
+ it('announces keyboard assignments politely', async () => {
78
+ const wrapper = mount(GroupAssigner, {
79
+ props: { groups, group1: [], group2: [] },
80
+ })
81
+
82
+ await wrapper.get('.mint-group-assigner__pill--unassigned')
83
+ .trigger('keydown', { key: 'ArrowLeft', altKey: true })
84
+ await wrapper.vm.$nextTick()
85
+
86
+ const liveRegion = wrapper.get('[role="status"]')
87
+ expect(liveRegion.attributes('aria-live')).toBe('polite')
88
+ expect(liveRegion.text()).toBe('control-a moved to Control.')
89
+ })
90
+
91
+ it('ignores arrow keys without Alt', async () => {
92
+ const wrapper = mount(GroupAssigner, {
93
+ props: { groups, group1: [], group2: [] },
94
+ })
95
+
96
+ await wrapper.get('.mint-group-assigner__pill--unassigned')
97
+ .trigger('keydown', { key: 'ArrowLeft' })
98
+
99
+ expect(wrapper.emitted('update:group1')).toBeUndefined()
100
+ })
48
101
  })
@@ -54,4 +54,26 @@ describe('LcmsSequenceTable', () => {
54
54
  expect(wrapper.emitted('duplicate')?.[0]).toEqual([0])
55
55
  expect(wrapper.emitted('remove')?.[0]).toEqual([0])
56
56
  })
57
+
58
+ it('reorders rows from the drag handle with Alt and the arrow keys', async () => {
59
+ const wrapper = mount(LcmsSequenceTable, {
60
+ props: { items: makeItems(), editable: true },
61
+ })
62
+
63
+ const handle = wrapper.findAll('.mint-lcms-sequence-table__drag-handle')[0]
64
+ expect(handle.element.tagName).toBe('BUTTON')
65
+ expect(handle.attributes('aria-label')).toContain('Reorder Batch_NEG_S1_001')
66
+
67
+ await handle.trigger('keydown', { key: 'ArrowDown', altKey: true })
68
+
69
+ expect(wrapper.emitted('reorder')?.[0]).toEqual([0, 1])
70
+ })
71
+
72
+ it('does not reorder from the keyboard when not editable', async () => {
73
+ const wrapper = mount(LcmsSequenceTable, {
74
+ props: { items: makeItems() },
75
+ })
76
+
77
+ expect(wrapper.find('.mint-lcms-sequence-table__drag-handle').exists()).toBe(false)
78
+ })
57
79
  })
@@ -122,4 +122,39 @@ describe('RackEditor', () => {
122
122
  }),
123
123
  ])
124
124
  })
125
+
126
+ it('reorders rack tabs with Alt and the arrow keys', async () => {
127
+ const racks = makeRacks()
128
+ racks.push({ ...racks[0], id: 'rack-2', name: 'Rack 2', slot: 'G', wells: {} })
129
+
130
+ const wrapper = mount(RackEditor, { props: { modelValue: racks } })
131
+
132
+ await wrapper.findAll('.mint-rack-editor__tab')[0].trigger('keydown', {
133
+ key: 'ArrowRight',
134
+ altKey: true,
135
+ })
136
+ await nextTick()
137
+
138
+ expect(wrapper.emitted('rack-reorder')?.at(-1)?.[0]).toEqual(['rack-2', 'rack-1'])
139
+ expect(wrapper.findAll('.mint-rack-editor__tab').map(tab => tab.text()))
140
+ .toEqual([expect.stringContaining('Rack 2'), expect.stringContaining('Rack 1')])
141
+ expect(wrapper.get('[role="status"]').attributes('aria-live')).toBe('polite')
142
+ })
143
+
144
+ it('does not reorder rack tabs from the keyboard when reordering is off', async () => {
145
+ const racks = makeRacks()
146
+ racks.push({ ...racks[0], id: 'rack-2', name: 'Rack 2', slot: 'G', wells: {} })
147
+
148
+ const wrapper = mount(RackEditor, {
149
+ props: { modelValue: racks, allowReorder: false },
150
+ })
151
+
152
+ await wrapper.findAll('.mint-rack-editor__tab')[0].trigger('keydown', {
153
+ key: 'ArrowRight',
154
+ altKey: true,
155
+ })
156
+ await nextTick()
157
+
158
+ expect(wrapper.emitted('rack-reorder')).toBeUndefined()
159
+ })
125
160
  })
@@ -63,7 +63,7 @@ describe('ReagentList', () => {
63
63
  },
64
64
  })
65
65
 
66
- await wrapper.findAll('th')[1].trigger('click')
66
+ await wrapper.findAll('th')[1].get('.mint-reagent-list__sort-btn').trigger('click')
67
67
 
68
68
  const rows = wrapper.findAll('tbody tr')
69
69
  expect(rows.map(row => row.text())).toEqual([
@@ -72,11 +72,73 @@ describe('ReagentList', () => {
72
72
  expect.stringContaining('Pyruvate'),
73
73
  ])
74
74
 
75
- await wrapper.findAll('th')[1].trigger('click')
75
+ await wrapper.findAll('th')[1].get('.mint-reagent-list__sort-btn').trigger('click')
76
76
  expect(wrapper.findAll('tbody tr').map(row => row.text())).toEqual([
77
77
  expect.stringContaining('Lactate'),
78
78
  expect.stringContaining('Glucose'),
79
79
  expect.stringContaining('Pyruvate'),
80
80
  ])
81
81
  })
82
+
83
+ it('exposes sortable headers as buttons that sort from the keyboard', async () => {
84
+ const wrapper = mount(ReagentList, {
85
+ props: { modelValue: reagents, readonly: true, columns: ['name', 'catalog'] },
86
+ })
87
+
88
+ const nameHeader = () => wrapper.findAll('th')[0]
89
+ expect(nameHeader().attributes('aria-sort')).toBe('none')
90
+ expect(nameHeader().get('.mint-reagent-list__sort-btn').element.tagName).toBe('BUTTON')
91
+
92
+ await nameHeader().get('.mint-reagent-list__sort-btn').trigger('keydown', { key: 'Enter' })
93
+ expect(nameHeader().attributes('aria-sort')).toBe('ascending')
94
+
95
+ await nameHeader().get('.mint-reagent-list__sort-btn').trigger('keydown', { key: ' ' })
96
+ expect(nameHeader().attributes('aria-sort')).toBe('descending')
97
+ })
98
+
99
+ it('omits aria-sort when sorting is disabled', () => {
100
+ const wrapper = mount(ReagentList, {
101
+ props: { modelValue: reagents, readonly: true, sortable: false, columns: ['name'] },
102
+ })
103
+
104
+ expect(wrapper.findAll('th')[0].attributes('aria-sort')).toBeUndefined()
105
+ })
106
+
107
+ it('reorders reagents from the drag handle with Alt and the arrow keys', async () => {
108
+ const wrapper = mount(ReagentList, {
109
+ props: { modelValue: reagents, columns: ['name'] },
110
+ })
111
+
112
+ const handle = wrapper.findAll('.mint-reagent-list__drag-handle-btn')[0]
113
+ expect(handle.attributes('aria-label')).toContain('Reorder Glucose')
114
+
115
+ await handle.trigger('keydown', { key: 'ArrowDown', altKey: true })
116
+
117
+ expect(wrapper.emitted('reorder')?.[0]).toEqual(['reagent-1', 0, 1])
118
+ expect(
119
+ (wrapper.emitted('update:modelValue')?.[0]?.[0] as Reagent[]).map(reagent => reagent.id),
120
+ ).toEqual(['reagent-2', 'reagent-1'])
121
+ })
122
+
123
+ it('does not reorder from the keyboard while readonly', async () => {
124
+ const wrapper = mount(ReagentList, {
125
+ props: { modelValue: reagents, readonly: true, columns: ['name'] },
126
+ })
127
+
128
+ expect(wrapper.find('.mint-reagent-list__drag-handle-btn').exists()).toBe(false)
129
+ })
130
+
131
+ it('announces a keyboard reorder in a polite live region', async () => {
132
+ const wrapper = mount(ReagentList, {
133
+ props: { modelValue: reagents, columns: ['name'] },
134
+ })
135
+
136
+ await wrapper.findAll('.mint-reagent-list__drag-handle-btn')[0]
137
+ .trigger('keydown', { key: 'ArrowDown', altKey: true })
138
+ await wrapper.vm.$nextTick()
139
+
140
+ const liveRegion = wrapper.get('[role="status"]')
141
+ expect(liveRegion.attributes('aria-live')).toBe('polite')
142
+ expect(liveRegion.text()).toBe('Glucose moved to position 2 of 2.')
143
+ })
82
144
  })
@@ -6,9 +6,21 @@ const retiredSelectors: Record<string, readonly string[]> = {
6
6
  'app-plugin-switcher.css': [
7
7
  'mint-plugin-switcher__swatch',
8
8
  ],
9
+ 'audit-trail.css': [
10
+ 'mint-audit-trail__empty',
11
+ ],
12
+ 'dataframe.css': [
13
+ 'mint-dataframe__empty',
14
+ 'mint-dataframe__empty-icon',
15
+ 'mint-dataframe__empty-text',
16
+ 'mint-dataframe__loading-spinner',
17
+ ],
9
18
  'dose-calculator.css': [
10
19
  'mint-dose-calculator__mw-input',
11
20
  ],
21
+ 'experiment-data-viewer.css': [
22
+ 'mint-data-viewer__empty',
23
+ ],
12
24
  'experiment-popover.css': [
13
25
  'mint-experiment-popover__check-icon',
14
26
  'mint-experiment-popover__divider',
@@ -24,13 +36,31 @@ const retiredSelectors: Record<string, readonly string[]> = {
24
36
  'mint-experiment-selector__filters',
25
37
  'mint-experiment-selector__status-filter',
26
38
  ],
39
+ 'experiment-timeline.css': [
40
+ 'mint-timeline__empty',
41
+ 'mint-timeline__empty-btn',
42
+ 'mint-timeline__empty-content',
43
+ 'mint-timeline__empty-text',
44
+ ],
45
+ 'lcms-sequence-table.css': [
46
+ 'mint-lcms-sequence-table__empty',
47
+ ],
27
48
  'protocol-step-editor.css': [
28
49
  'mint-protocol-editor__field-row',
29
50
  ],
30
51
  'reagent-editor.css': [
31
52
  'mint-reagent-editor__row',
32
53
  ],
54
+ 'reagent-list.css': [
55
+ 'mint-reagent-list__empty',
56
+ 'mint-reagent-list__empty-btn',
57
+ 'mint-reagent-list__empty-icon',
58
+ 'mint-reagent-list__empty-text',
59
+ ],
33
60
  'sample-hierarchy-tree.css': [
61
+ 'mint-sample-tree__empty',
62
+ 'mint-sample-tree__empty-icon',
63
+ 'mint-sample-tree__empty-text',
34
64
  'mint-sample-tree__node--selected',
35
65
  ],
36
66
  'sample-selector.css': [
@@ -38,6 +68,7 @@ const retiredSelectors: Record<string, readonly string[]> = {
38
68
  'mint-popover-enter-from',
39
69
  'mint-popover-leave-active',
40
70
  'mint-popover-leave-to',
71
+ 'mint-sample-selector__empty',
41
72
  'mint-sample-selector__level-badge',
42
73
  'mint-sample-selector__level-btn',
43
74
  'mint-sample-selector__level-desc',
@@ -1,4 +1,4 @@
1
- import { ref } from 'vue'
1
+ import { nextTick, ref } from 'vue'
2
2
  import { describe, expect, it } from 'vitest'
3
3
  import type { SampleGroup } from '../../types'
4
4
  import { useSampleSelectorDrag } from '../../components/SampleSelector.drag'
@@ -9,6 +9,10 @@ type TestDragEvent = Omit<DragEvent, 'dataTransfer'> & {
9
9
  defaultPrevented: boolean
10
10
  }
11
11
 
12
+ function keyboardEvent(key: string, altKey = true): KeyboardEvent {
13
+ return { key, altKey, preventDefault: () => {} } as unknown as KeyboardEvent
14
+ }
15
+
12
16
  function createDragEvent(options: { clientY?: number; top?: number; height?: number } = {}) {
13
17
  const data: Record<string, string> = {}
14
18
  const transfer = {
@@ -123,4 +127,50 @@ describe('SampleSelector drag helpers', () => {
123
127
 
124
128
  expect(drag.reorderTarget.value).toBe('Liver / High / 10min')
125
129
  })
130
+
131
+ it('reorders sibling groups with Alt and the arrow keys', async () => {
132
+ const state = ref<SampleGroup[]>([
133
+ { name: 'Liver / High / 5min', color: '#111111', samples: ['L1'] },
134
+ { name: 'Liver / High / 10min', color: '#222222', samples: ['L2'] },
135
+ { name: 'Liver / High / 20min', color: '#333333', samples: ['L3'] },
136
+ ])
137
+ const drag = useSampleSelectorDrag(state)
138
+
139
+ drag.handleGroupReorderKeydown('Liver / High / 5min', 'sub', keyboardEvent('ArrowDown'))
140
+
141
+ expect(state.value.map(group => group.name)).toEqual([
142
+ 'Liver / High / 10min',
143
+ 'Liver / High / 5min',
144
+ 'Liver / High / 20min',
145
+ ])
146
+
147
+ await nextTick()
148
+ expect(drag.announcement.value).toBe('Liver / High / 5min moved down to position 2 of 3.')
149
+ })
150
+
151
+ it('keeps a group inside its own family and ignores arrows without Alt', () => {
152
+ const state = ref<SampleGroup[]>([
153
+ { name: 'Liver / High / 5min', color: '#111111', samples: ['L1'] },
154
+ { name: 'Muscle / High / 5min', color: '#222222', samples: ['M1'] },
155
+ ])
156
+ const drag = useSampleSelectorDrag(state)
157
+ const before = state.value
158
+
159
+ drag.handleGroupReorderKeydown('Liver / High / 5min', 'sub', keyboardEvent('ArrowDown'))
160
+ drag.handleGroupReorderKeydown('Liver / High / 5min', 'sub', keyboardEvent('ArrowDown', false))
161
+
162
+ expect(state.value).toBe(before)
163
+ })
164
+
165
+ it('reorders whole major families with Alt and the arrow keys', () => {
166
+ const state = ref<SampleGroup[]>([
167
+ { name: 'Liver / High', color: '#111111', samples: ['L1'] },
168
+ { name: 'Muscle / High', color: '#222222', samples: ['M1'] },
169
+ ])
170
+ const drag = useSampleSelectorDrag(state)
171
+
172
+ drag.handleGroupReorderKeydown('Muscle', 'major', keyboardEvent('ArrowUp'))
173
+
174
+ expect(state.value.map(group => group.name)).toEqual(['Muscle / High', 'Liver / High'])
175
+ })
126
176
  })
@@ -41,4 +41,67 @@ describe('ScheduleCalendar', () => {
41
41
  expect(wrapper.findAll('.mint-schedule__month-cell')).toHaveLength(42)
42
42
  expect(wrapper.find('.mint-schedule__title').text()).toBe('May 2024')
43
43
  })
44
+
45
+ it('reschedules an event with Alt and the arrow keys', async () => {
46
+ const wrapper = mount(ScheduleCalendar, {
47
+ props: {
48
+ modelValue: new Date('2024-05-15T12:00:00'),
49
+ dayStartHour: 6,
50
+ dayEndHour: 10,
51
+ slotDuration: 30,
52
+ showNowIndicator: false,
53
+ events: [
54
+ {
55
+ id: 'run-1',
56
+ title: 'LCMS run',
57
+ start: '2024-05-15T07:00:00',
58
+ end: '2024-05-15T08:00:00',
59
+ },
60
+ ],
61
+ },
62
+ })
63
+
64
+ const event = wrapper.get('.mint-schedule__event')
65
+ expect(event.attributes('tabindex')).toBe('0')
66
+ expect(event.attributes('aria-label')).toContain('Press Alt with the up or down arrow keys')
67
+
68
+ await event.trigger('keydown', { key: 'ArrowDown', altKey: true })
69
+
70
+ const update = wrapper.emitted('event-update')?.[0]?.[0] as {
71
+ newStart: Date
72
+ newEnd: Date
73
+ }
74
+ expect(update.newStart.getHours()).toBe(7)
75
+ expect(update.newStart.getMinutes()).toBe(30)
76
+ expect(update.newEnd.getHours()).toBe(8)
77
+ expect(update.newEnd.getMinutes()).toBe(30)
78
+
79
+ await wrapper.vm.$nextTick()
80
+ expect(wrapper.get('[role="status"]').attributes('aria-live')).toBe('polite')
81
+ })
82
+
83
+ it('does not reschedule from the keyboard while readonly', async () => {
84
+ const wrapper = mount(ScheduleCalendar, {
85
+ props: {
86
+ modelValue: new Date('2024-05-15T12:00:00'),
87
+ dayStartHour: 6,
88
+ dayEndHour: 10,
89
+ slotDuration: 30,
90
+ showNowIndicator: false,
91
+ readonly: true,
92
+ events: [
93
+ {
94
+ id: 'run-1',
95
+ title: 'LCMS run',
96
+ start: '2024-05-15T07:00:00',
97
+ end: '2024-05-15T08:00:00',
98
+ },
99
+ ],
100
+ },
101
+ })
102
+
103
+ await wrapper.get('.mint-schedule__event').trigger('keydown', { key: 'ArrowDown', altKey: true })
104
+
105
+ expect(wrapper.emitted('event-update')).toBeUndefined()
106
+ })
44
107
  })
@@ -66,4 +66,92 @@ describe('SegmentedControl', () => {
66
66
 
67
67
  expect(wrapper.emitted('update:modelValue')).toBeUndefined()
68
68
  })
69
+
70
+ describe('roving tabindex', () => {
71
+ const options = [
72
+ { value: 'table', label: 'Table' },
73
+ { value: 'chart', label: 'Chart' },
74
+ { value: 'raw', label: 'Raw' },
75
+ ]
76
+
77
+ it('makes only the checked option tabbable', () => {
78
+ const wrapper = mount(SegmentedControl, {
79
+ props: { modelValue: 'chart', options },
80
+ })
81
+
82
+ expect(wrapper.findAll('button').map(button => button.attributes('tabindex')))
83
+ .toEqual(['-1', '0', '-1'])
84
+ })
85
+
86
+ it('falls back to the first selectable option when nothing matches', () => {
87
+ const wrapper = mount(SegmentedControl, {
88
+ props: { modelValue: 'missing', options },
89
+ })
90
+
91
+ expect(wrapper.findAll('button').map(button => button.attributes('tabindex')))
92
+ .toEqual(['0', '-1', '-1'])
93
+ })
94
+
95
+ it('activates the adjacent option with the arrow keys, wrapping at the ends', async () => {
96
+ const wrapper = mount(SegmentedControl, {
97
+ props: { modelValue: 'chart', options },
98
+ attachTo: document.body,
99
+ })
100
+
101
+ const buttons = wrapper.findAll('button')
102
+ await buttons[1].trigger('keydown', { key: 'ArrowRight' })
103
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['raw'])
104
+
105
+ await buttons[1].trigger('keydown', { key: 'ArrowLeft' })
106
+ expect(wrapper.emitted('update:modelValue')?.[1]).toEqual(['table'])
107
+
108
+ await wrapper.setProps({ modelValue: 'raw' })
109
+ await wrapper.findAll('button')[2].trigger('keydown', { key: 'ArrowRight' })
110
+ expect(wrapper.emitted('update:modelValue')?.[2]).toEqual(['table'])
111
+
112
+ wrapper.unmount()
113
+ })
114
+
115
+ it('jumps to the first and last option with Home and End', async () => {
116
+ const wrapper = mount(SegmentedControl, {
117
+ props: { modelValue: 'chart', options },
118
+ })
119
+
120
+ const buttons = wrapper.findAll('button')
121
+ await buttons[1].trigger('keydown', { key: 'Home' })
122
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['table'])
123
+
124
+ await buttons[1].trigger('keydown', { key: 'End' })
125
+ expect(wrapper.emitted('update:modelValue')?.[1]).toEqual(['raw'])
126
+ })
127
+
128
+ it('skips disabled options during arrow navigation', async () => {
129
+ const wrapper = mount(SegmentedControl, {
130
+ props: {
131
+ modelValue: 'table',
132
+ options,
133
+ disabledValues: ['chart'],
134
+ },
135
+ })
136
+
137
+ await wrapper.findAll('button')[0].trigger('keydown', { key: 'ArrowRight' })
138
+
139
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['raw'])
140
+ })
141
+
142
+ it('moves focus onto the newly activated option', async () => {
143
+ const wrapper = mount(SegmentedControl, {
144
+ props: { modelValue: 'table', options },
145
+ attachTo: document.body,
146
+ })
147
+
148
+ await wrapper.findAll('button')[0].trigger('keydown', { key: 'ArrowRight' })
149
+ await wrapper.setProps({ modelValue: 'chart' })
150
+ await wrapper.vm.$nextTick()
151
+
152
+ expect(document.activeElement).toBe(wrapper.findAll('button')[1].element)
153
+
154
+ wrapper.unmount()
155
+ })
156
+ })
69
157
  })
@@ -35,4 +35,65 @@ describe('TimePicker', () => {
35
35
 
36
36
  wrapper.unmount()
37
37
  })
38
+
39
+ it('publishes the highlighted slot through aria-activedescendant', async () => {
40
+ const wrapper = mount(TimePicker, {
41
+ props: { modelValue: '14:30' },
42
+ attachTo: document.body,
43
+ })
44
+
45
+ const input = wrapper.get('input')
46
+ expect(input.attributes('aria-activedescendant')).toBeUndefined()
47
+
48
+ await wrapper.get('.mint-time-picker').trigger('keydown', { key: 'ArrowDown' })
49
+ await wrapper.vm.$nextTick()
50
+
51
+ const listbox = wrapper.get('.mint-time-picker__list')
52
+ expect(input.attributes('aria-controls')).toBe(listbox.attributes('id'))
53
+
54
+ const activeId = input.attributes('aria-activedescendant')
55
+ expect(activeId).toBeDefined()
56
+ expect(wrapper.find(`#${activeId}`).exists()).toBe(true)
57
+
58
+ wrapper.unmount()
59
+ })
60
+
61
+ it('jumps to the first and last selectable slot with Home and End', async () => {
62
+ const wrapper = mount(TimePicker, {
63
+ props: { min: '09:00', max: '10:00', step: 30 },
64
+ attachTo: document.body,
65
+ })
66
+
67
+ const root = wrapper.get('.mint-time-picker')
68
+ await root.trigger('keydown', { key: 'ArrowDown' })
69
+ await root.trigger('keydown', { key: 'End' })
70
+ await wrapper.vm.$nextTick()
71
+
72
+ const slots = wrapper.findAll('.mint-time-picker__slot')
73
+ expect(slots.at(-1)?.classes()).toContain('mint-time-picker__slot--highlighted')
74
+
75
+ await root.trigger('keydown', { key: 'Home' })
76
+ await wrapper.vm.$nextTick()
77
+ expect(wrapper.findAll('.mint-time-picker__slot')[0].classes())
78
+ .toContain('mint-time-picker__slot--highlighted')
79
+
80
+ wrapper.unmount()
81
+ })
82
+
83
+ it('returns focus to the input when Escape closes the list', async () => {
84
+ const wrapper = mount(TimePicker, { attachTo: document.body })
85
+ const input = wrapper.get<HTMLInputElement>('input').element
86
+
87
+ await wrapper.get('.mint-time-picker').trigger('keydown', { key: 'ArrowDown' })
88
+ expect(wrapper.find('.mint-time-picker__dropdown').exists()).toBe(true)
89
+
90
+ await wrapper.get('.mint-time-picker').trigger('keydown', { key: 'Escape' })
91
+ await wrapper.vm.$nextTick()
92
+ await wrapper.vm.$nextTick()
93
+
94
+ expect(wrapper.find('.mint-time-picker__dropdown').exists()).toBe(false)
95
+ expect(document.activeElement).toBe(input)
96
+
97
+ wrapper.unmount()
98
+ })
38
99
  })