@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
@@ -1,3 +1,16 @@
1
+ import { SlotPosition } from '../types/componentLabTypes';
2
+ /**
3
+ * Canonical categorical scale for sample types, plate maps, dose-response
4
+ * series and legends. Mirrors `--mint-sample-1` … `--mint-sample-9`.
5
+ */
6
+ export declare const SAMPLE_COLOR_SCALE: readonly string[];
7
+ /**
8
+ * Slot-position colors shared by `PlateMapEditor` and `RackEditor`.
9
+ * Mirrors `--mint-slot-r`, `--mint-slot-g`, `--mint-slot-b`, `--mint-slot-y`.
10
+ */
11
+ export declare const SLOT_COLORS: Record<SlotPosition, string>;
12
+ /** Slot positions in their canonical display order. */
13
+ export declare const SLOT_ORDER: readonly SlotPosition[];
1
14
  /**
2
15
  * Color utilities for deriving same-hue-family shades.
3
16
  *
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The one place a thrown value is turned into a developer-facing message.
3
+ *
4
+ * Five copies of this logic drifted apart across the composables and stores
5
+ * (some checked a bare `{ message }` object, some did not); this export is the
6
+ * union of what they all accepted, so every caller now reads the same message
7
+ * from the same value.
8
+ */
9
+ export declare function readErrorMessage(value: unknown, fallback?: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mint-sdk",
3
- "version": "1.2.0-beta.7",
3
+ "version": "1.2.0-beta.9",
4
4
  "description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -102,6 +102,100 @@ describe('ActionMenu', () => {
102
102
  expect(document.body.querySelector('.mint-action-menu__panel')).toBeNull()
103
103
  })
104
104
 
105
+ it('should open from the trigger keyboard shortcuts and focus the first item', async () => {
106
+ const wrapper = mount(ActionMenu, {
107
+ attachTo: document.body,
108
+ props: { label: 'More user actions', items },
109
+ })
110
+
111
+ await wrapper.get('button').trigger('keydown', { key: 'ArrowDown' })
112
+ await wrapper.vm.$nextTick()
113
+
114
+ const menuItems = document.body.querySelectorAll<HTMLElement>('.mint-action-menu__item')
115
+ expect(menuItems).toHaveLength(2)
116
+ expect(document.activeElement).toBe(menuItems[0])
117
+ })
118
+
119
+ it('should open focused on the last item with ArrowUp', async () => {
120
+ const wrapper = mount(ActionMenu, {
121
+ attachTo: document.body,
122
+ props: { label: 'More user actions', items },
123
+ })
124
+
125
+ await wrapper.get('button').trigger('keydown', { key: 'ArrowUp' })
126
+ await wrapper.vm.$nextTick()
127
+
128
+ const menuItems = document.body.querySelectorAll<HTMLElement>('.mint-action-menu__item')
129
+ expect(document.activeElement).toBe(menuItems[1])
130
+ })
131
+
132
+ it('should cycle items with the arrow keys and jump with Home/End', async () => {
133
+ const wrapper = mount(ActionMenu, {
134
+ attachTo: document.body,
135
+ props: { label: 'More user actions', items },
136
+ })
137
+ await wrapper.get('button').trigger('click')
138
+ await wrapper.vm.$nextTick()
139
+
140
+ const panel = document.body.querySelector<HTMLElement>('.mint-action-menu__panel')
141
+ const menuItems = document.body.querySelectorAll<HTMLElement>('.mint-action-menu__item')
142
+
143
+ function press(key: string) {
144
+ panel?.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
145
+ }
146
+
147
+ press('ArrowDown')
148
+ expect(document.activeElement).toBe(menuItems[1])
149
+ press('ArrowDown')
150
+ expect(document.activeElement).toBe(menuItems[0])
151
+ press('End')
152
+ expect(document.activeElement).toBe(menuItems[1])
153
+ press('Home')
154
+ expect(document.activeElement).toBe(menuItems[0])
155
+ })
156
+
157
+ it('should close on Tab without forcing focus back to the trigger', async () => {
158
+ const wrapper = mount(ActionMenu, {
159
+ attachTo: document.body,
160
+ props: { label: 'More user actions', items },
161
+ })
162
+ await wrapper.get('button').trigger('click')
163
+
164
+ document.body.querySelector<HTMLElement>('.mint-action-menu__panel')?.dispatchEvent(
165
+ new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }),
166
+ )
167
+ await wrapper.vm.$nextTick()
168
+
169
+ expect(document.body.querySelector('.mint-action-menu__panel')).toBeNull()
170
+ expect(document.activeElement).not.toBe(wrapper.get('button').element)
171
+ })
172
+
173
+ it('should not open from the keyboard while disabled', async () => {
174
+ const wrapper = mount(ActionMenu, {
175
+ attachTo: document.body,
176
+ props: { label: 'More user actions', items, disabled: true },
177
+ })
178
+
179
+ await wrapper.get('button').trigger('keydown', { key: 'ArrowDown' })
180
+ await wrapper.vm.$nextTick()
181
+
182
+ expect(document.body.querySelector('.mint-action-menu__panel')).toBeNull()
183
+ })
184
+
185
+ it('should close when a click lands outside the trigger and panel', async () => {
186
+ const wrapper = mount(ActionMenu, {
187
+ attachTo: document.body,
188
+ props: { label: 'More user actions', items },
189
+ })
190
+ await wrapper.get('button').trigger('click')
191
+ expect(document.body.querySelector('.mint-action-menu__panel')).not.toBeNull()
192
+
193
+ document.body.dispatchEvent(new MouseEvent('click', { bubbles: true }))
194
+ await wrapper.vm.$nextTick()
195
+
196
+ expect(document.body.querySelector('.mint-action-menu__panel')).toBeNull()
197
+ })
198
+
105
199
  it('should return focus to the trigger when Escape closes the menu', async () => {
106
200
  const wrapper = mount(ActionMenu, {
107
201
  attachTo: document.body,
@@ -1,6 +1,8 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { afterEach, describe, it, expect } from 'vitest'
2
2
  import { mount } from '@vue/test-utils'
3
+ import { createPinia, setActivePinia } from 'pinia'
3
4
  import DataFrame from '../../components/DataFrame.vue'
5
+ import { useSettingsStore } from '../../stores/settings'
4
6
  import type { DataFrameColumn, SortState, PaginationState } from '../../types'
5
7
 
6
8
  describe('DataFrame', () => {
@@ -167,7 +169,7 @@ describe('DataFrame', () => {
167
169
  props: { data: mockData, columns: sortableColumns },
168
170
  })
169
171
  const nameHeader = wrapper.findAll('.mint-dataframe__th')[0]
170
- await nameHeader.trigger('click')
172
+ await nameHeader.get('.mint-dataframe__th-button').trigger('click')
171
173
 
172
174
  expect(wrapper.emitted('update:sort')).toHaveLength(1)
173
175
  const sortState = wrapper.emitted('update:sort')?.[0]?.[0] as SortState
@@ -184,7 +186,7 @@ describe('DataFrame', () => {
184
186
  },
185
187
  })
186
188
  const nameHeader = wrapper.findAll('.mint-dataframe__th')[0]
187
- await nameHeader.trigger('click')
189
+ await nameHeader.get('.mint-dataframe__th-button').trigger('click')
188
190
 
189
191
  const sortState = wrapper.emitted('update:sort')?.[0]?.[0] as SortState
190
192
  expect(sortState.direction).toBe('desc')
@@ -199,7 +201,7 @@ describe('DataFrame', () => {
199
201
  },
200
202
  })
201
203
  const nameHeader = wrapper.findAll('.mint-dataframe__th')[0]
202
- await nameHeader.trigger('click')
204
+ await nameHeader.get('.mint-dataframe__th-button').trigger('click')
203
205
 
204
206
  const sortState = wrapper.emitted('update:sort')?.[0]?.[0]
205
207
  expect(sortState).toBeNull()
@@ -210,7 +212,7 @@ describe('DataFrame', () => {
210
212
  props: { data: mockData, columns: sortableColumns },
211
213
  })
212
214
  const nameHeader = wrapper.findAll('.mint-dataframe__th')[0]
213
- await nameHeader.trigger('click')
215
+ await nameHeader.get('.mint-dataframe__th-button').trigger('click')
214
216
 
215
217
  // After sorting, first row should have name starting with 'A'
216
218
  const firstRow = wrapper.findAll('.mint-dataframe__row')[0]
@@ -223,7 +225,7 @@ describe('DataFrame', () => {
223
225
  props: { data: mockData, columns: sortableColumns },
224
226
  })
225
227
  const ageHeader = wrapper.findAll('.mint-dataframe__th')[1]
226
- await ageHeader.trigger('click')
228
+ await ageHeader.get('.mint-dataframe__th-button').trigger('click')
227
229
 
228
230
  // After sorting ascending, first row should have age 25
229
231
  const firstRow = wrapper.findAll('.mint-dataframe__row')[0]
@@ -553,28 +555,38 @@ describe('DataFrame', () => {
553
555
  const wrapper = mount(DataFrame, {
554
556
  props: { data: [], columns: mockColumns },
555
557
  })
556
- expect(wrapper.find('.mint-dataframe__empty').exists()).toBe(true)
558
+ expect(wrapper.find('.mint-dataframe__state').exists()).toBe(true)
559
+ expect(wrapper.find('[data-state="empty"]').exists()).toBe(true)
557
560
  })
558
561
 
559
562
  it('should display empty text', () => {
560
563
  const wrapper = mount(DataFrame, {
561
564
  props: { data: [], columns: mockColumns },
562
565
  })
563
- expect(wrapper.find('.mint-dataframe__empty-text').text()).toBe('No data')
566
+ expect(wrapper.find('.mint-empty-state__title').text()).toBe('No data')
564
567
  })
565
568
 
566
569
  it('should use custom empty text', () => {
567
570
  const wrapper = mount(DataFrame, {
568
571
  props: { data: [], columns: mockColumns, emptyText: 'No records found' },
569
572
  })
570
- expect(wrapper.find('.mint-dataframe__empty-text').text()).toBe('No records found')
573
+ expect(wrapper.find('.mint-empty-state__title').text()).toBe('No records found')
571
574
  })
572
575
 
573
576
  it('should not show empty state when loading', () => {
574
577
  const wrapper = mount(DataFrame, {
575
578
  props: { data: [], columns: mockColumns, loading: true },
576
579
  })
577
- expect(wrapper.find('.mint-dataframe__empty').exists()).toBe(false)
580
+ expect(wrapper.find('[data-state="empty"]').exists()).toBe(false)
581
+ })
582
+
583
+ it('should render the empty slot instead of the default placeholder', () => {
584
+ const wrapper = mount(DataFrame, {
585
+ props: { data: [], columns: mockColumns },
586
+ slots: { empty: '<p class="custom-empty">Nothing here</p>' },
587
+ })
588
+ expect(wrapper.find('.custom-empty').exists()).toBe(true)
589
+ expect(wrapper.find('[data-state="empty"]').exists()).toBe(false)
578
590
  })
579
591
  })
580
592
 
@@ -590,7 +602,40 @@ describe('DataFrame', () => {
590
602
  const wrapper = mount(DataFrame, {
591
603
  props: { data: mockData, columns: mockColumns, loading: true },
592
604
  })
593
- expect(wrapper.find('.mint-dataframe__loading-spinner').exists()).toBe(true)
605
+ expect(wrapper.find('.mint-spinner').exists()).toBe(true)
606
+ })
607
+ })
608
+
609
+ describe('error state', () => {
610
+ it('should render nothing extra when error is not passed', () => {
611
+ const wrapper = mount(DataFrame, {
612
+ props: { data: mockData, columns: mockColumns },
613
+ })
614
+ expect(wrapper.find('[data-state="error"]').exists()).toBe(false)
615
+ })
616
+
617
+ it('should replace the empty state with an error state', () => {
618
+ const wrapper = mount(DataFrame, {
619
+ props: { data: [], columns: mockColumns, error: 'Request failed' },
620
+ })
621
+ expect(wrapper.find('[data-state="error"]').exists()).toBe(true)
622
+ expect(wrapper.find('[data-state="empty"]').exists()).toBe(false)
623
+ expect(wrapper.find('.mint-empty-state__title').text()).toBe('Request failed')
624
+ })
625
+
626
+ it('should take precedence over the empty state even with rows present', () => {
627
+ const wrapper = mount(DataFrame, {
628
+ props: { data: mockData, columns: mockColumns, error: 'Boom' },
629
+ })
630
+ expect(wrapper.find('[data-state="error"]').exists()).toBe(true)
631
+ })
632
+
633
+ it('should render the error slot with the message', () => {
634
+ const wrapper = mount(DataFrame, {
635
+ props: { data: [], columns: mockColumns, error: 'Boom' },
636
+ slots: { error: '<p class="custom-error">custom error</p>' },
637
+ })
638
+ expect(wrapper.find('.custom-error').exists()).toBe(true)
594
639
  })
595
640
  })
596
641
 
@@ -911,5 +956,132 @@ describe('DataFrame', () => {
911
956
  await wrapper.find('.mint-dataframe__resize-handle').trigger('click')
912
957
  expect(wrapper.emitted('update:sort')).toBeUndefined()
913
958
  })
959
+
960
+ it('should expose the resize handle as a slider with its current width', () => {
961
+ const wrapper = mount(DataFrame, {
962
+ props: {
963
+ data: mockData,
964
+ columns: [{ key: 'name', label: 'Name', minWidth: 80 }],
965
+ resizable: true,
966
+ columnWidths: { name: 240 },
967
+ },
968
+ })
969
+
970
+ const handle = wrapper.get('.mint-dataframe__resize-handle')
971
+ expect(handle.attributes('role')).toBe('slider')
972
+ expect(handle.attributes('tabindex')).toBe('0')
973
+ expect(handle.attributes('aria-label')).toBe('Resize Name column')
974
+ expect(handle.attributes('aria-valuenow')).toBe('240')
975
+ expect(handle.attributes('aria-valuemin')).toBe('80')
976
+ })
977
+
978
+ it('should resize a column with the arrow keys, clamped to the minimum width', async () => {
979
+ const wrapper = mount(DataFrame, {
980
+ props: {
981
+ data: mockData,
982
+ columns: [{ key: 'name', label: 'Name' }],
983
+ resizable: true,
984
+ columnWidths: { name: 120 },
985
+ },
986
+ })
987
+
988
+ const handle = wrapper.get('.mint-dataframe__resize-handle')
989
+ await handle.trigger('keydown', { key: 'ArrowRight' })
990
+ expect(wrapper.emitted('update:columnWidths')?.[0]).toEqual([{ name: 136 }])
991
+
992
+ await handle.trigger('keydown', { key: 'ArrowLeft' })
993
+ expect(wrapper.emitted('update:columnWidths')?.[1]).toEqual([{ name: 104 }])
994
+
995
+ await wrapper.setProps({ columnWidths: { name: 50 } })
996
+ await handle.trigger('keydown', { key: 'ArrowLeft' })
997
+ expect(wrapper.emitted('update:columnWidths')?.[2]).toEqual([{ name: 48 }])
998
+ })
999
+ })
1000
+
1001
+ describe('sortable header accessibility', () => {
1002
+ const sortableColumn = [{ key: 'name', label: 'Name', sortable: true }]
1003
+
1004
+ it('should render sortable headers as buttons and plain headers without one', () => {
1005
+ const wrapper = mount(DataFrame, {
1006
+ props: {
1007
+ data: mockData,
1008
+ columns: [...sortableColumn, { key: 'age', label: 'Age' }],
1009
+ },
1010
+ })
1011
+
1012
+ const headers = wrapper.findAll('.mint-dataframe__th')
1013
+ expect(headers[0].get('.mint-dataframe__th-button').element.tagName).toBe('BUTTON')
1014
+ expect(headers[1].get('.mint-dataframe__th-button').element.tagName).toBe('DIV')
1015
+ })
1016
+
1017
+ it('should reflect the sort direction with aria-sort', async () => {
1018
+ const wrapper = mount(DataFrame, {
1019
+ props: {
1020
+ data: mockData,
1021
+ columns: [...sortableColumn, { key: 'age', label: 'Age' }],
1022
+ },
1023
+ })
1024
+
1025
+ const nameHeader = () => wrapper.findAll('.mint-dataframe__th')[0]
1026
+ expect(nameHeader().attributes('aria-sort')).toBe('none')
1027
+ expect(wrapper.findAll('.mint-dataframe__th')[1].attributes('aria-sort')).toBeUndefined()
1028
+
1029
+ await nameHeader().get('.mint-dataframe__th-button').trigger('click')
1030
+ expect(nameHeader().attributes('aria-sort')).toBe('ascending')
1031
+
1032
+ await nameHeader().get('.mint-dataframe__th-button').trigger('click')
1033
+ expect(nameHeader().attributes('aria-sort')).toBe('descending')
1034
+
1035
+ await nameHeader().get('.mint-dataframe__th-button').trigger('click')
1036
+ expect(nameHeader().attributes('aria-sort')).toBe('none')
1037
+ })
1038
+
1039
+ it('should sort from the keyboard with Enter and Space', async () => {
1040
+ const wrapper = mount(DataFrame, {
1041
+ props: { data: mockData, columns: sortableColumn },
1042
+ })
1043
+
1044
+ const button = wrapper.get('.mint-dataframe__th-button')
1045
+ await button.trigger('keydown', { key: 'Enter' })
1046
+ expect(wrapper.emitted('update:sort')?.[0]?.[0]).toEqual({ key: 'name', direction: 'asc' })
1047
+
1048
+ await button.trigger('keydown', { key: ' ' })
1049
+ expect(wrapper.emitted('update:sort')?.[1]?.[0]).toEqual({ key: 'name', direction: 'desc' })
1050
+ })
1051
+ })
1052
+
1053
+ describe('table density default', () => {
1054
+ afterEach(() => {
1055
+ setActivePinia(undefined)
1056
+ })
1057
+
1058
+ it('falls back to md when no settings store is installed', () => {
1059
+ setActivePinia(undefined)
1060
+ const wrapper = mount(DataFrame, {
1061
+ props: { data: mockData, columns: mockColumns },
1062
+ })
1063
+ expect(wrapper.find('.mint-dataframe__td--md').exists()).toBe(true)
1064
+ })
1065
+
1066
+ it('follows the tableDensity setting when size is not passed', () => {
1067
+ setActivePinia(createPinia())
1068
+ useSettingsStore().tableDensity = 'compact'
1069
+
1070
+ const wrapper = mount(DataFrame, {
1071
+ props: { data: mockData, columns: mockColumns },
1072
+ })
1073
+ expect(wrapper.find('.mint-dataframe__td--sm').exists()).toBe(true)
1074
+ expect(wrapper.find('.mint-dataframe__td--md').exists()).toBe(false)
1075
+ })
1076
+
1077
+ it('lets an explicit size prop win over the density setting', () => {
1078
+ setActivePinia(createPinia())
1079
+ useSettingsStore().tableDensity = 'compact'
1080
+
1081
+ const wrapper = mount(DataFrame, {
1082
+ props: { data: mockData, columns: mockColumns, size: 'lg' },
1083
+ })
1084
+ expect(wrapper.find('.mint-dataframe__td--lg').exists()).toBe(true)
1085
+ })
914
1086
  })
915
1087
  })
@@ -0,0 +1,176 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import AuditTrail from '../../components/AuditTrail.vue'
4
+ import BatchProgressList from '../../components/BatchProgressList.vue'
5
+ import ExperimentTimeline from '../../components/ExperimentTimeline.vue'
6
+ import LcmsSequenceTable from '../../components/LcmsSequenceTable.vue'
7
+ import ReagentList from '../../components/ReagentList.vue'
8
+ import SampleHierarchyTree from '../../components/SampleHierarchyTree.vue'
9
+ import ScheduleCalendar from '../../components/ScheduleCalendar.vue'
10
+ import WellPlate from '../../components/WellPlate.vue'
11
+
12
+ /**
13
+ * The `loading` / `error` props and the `#empty` / `#error` / `#loading` slots
14
+ * are one contract shared by the data components, so they are asserted in one
15
+ * place. Each entry pairs a component with the props that leave it empty and a
16
+ * selector that only its populated rendering produces.
17
+ */
18
+ const cases = [
19
+ {
20
+ name: 'AuditTrail',
21
+ component: AuditTrail,
22
+ emptyProps: { entries: [] },
23
+ filledProps: {
24
+ entries: [{ id: 'a', type: 'created', action: 'Created', timestamp: '2026-01-01T00:00:00Z' }],
25
+ },
26
+ contentSelector: '.mint-audit-trail__list',
27
+ },
28
+ {
29
+ name: 'BatchProgressList',
30
+ component: BatchProgressList,
31
+ emptyProps: { items: [] },
32
+ filledProps: { items: [{ id: '1', label: 'One', status: 'pending' }] },
33
+ contentSelector: '.mint-batch-progress__list',
34
+ },
35
+ {
36
+ name: 'ExperimentTimeline',
37
+ component: ExperimentTimeline,
38
+ emptyProps: { modelValue: [] },
39
+ filledProps: { modelValue: [{ id: 's1', name: 'Step 1', order: 1, type: 'incubation' }] },
40
+ contentSelector: '.mint-timeline__step',
41
+ },
42
+ {
43
+ name: 'LcmsSequenceTable',
44
+ component: LcmsSequenceTable,
45
+ emptyProps: { items: [] },
46
+ filledProps: {
47
+ items: [{
48
+ file_name: 'a.raw',
49
+ position: 'A1',
50
+ sample_type: 'Unknown',
51
+ instrument_method: 'm',
52
+ injection_volume: 1,
53
+ }],
54
+ },
55
+ contentSelector: '.mint-lcms-sequence-table__scroller',
56
+ },
57
+ {
58
+ name: 'ReagentList',
59
+ component: ReagentList,
60
+ emptyProps: { modelValue: [] },
61
+ filledProps: { modelValue: [{ id: 'r1', name: 'Water' }] },
62
+ contentSelector: '.mint-reagent-list__table',
63
+ },
64
+ {
65
+ name: 'SampleHierarchyTree',
66
+ component: SampleHierarchyTree,
67
+ emptyProps: { nodes: [] },
68
+ filledProps: { nodes: [{ id: 'n1', label: 'Study', type: 'study' }] },
69
+ contentSelector: '.mint-sample-tree__node',
70
+ },
71
+ ] as const
72
+
73
+ describe('shared data component states', () => {
74
+ describe.each(cases)('$name', ({ component, emptyProps, filledProps, contentSelector }) => {
75
+ it('renders its content when neither loading nor error is passed', () => {
76
+ const wrapper = mount(component, { props: { ...filledProps } as never })
77
+
78
+ expect(wrapper.find(contentSelector).exists()).toBe(true)
79
+ expect(wrapper.find('[data-state]').exists()).toBe(false)
80
+ })
81
+
82
+ it('renders an empty state instead of content when there is nothing to show', () => {
83
+ const wrapper = mount(component, { props: { ...emptyProps } as never })
84
+
85
+ expect(wrapper.find('[data-state="empty"]').exists()).toBe(true)
86
+ expect(wrapper.find(contentSelector).exists()).toBe(false)
87
+ })
88
+
89
+ it('renders a loading state that replaces the content', () => {
90
+ const wrapper = mount(component, { props: { ...filledProps, loading: true } as never })
91
+
92
+ expect(wrapper.find('[data-state="loading"]').exists()).toBe(true)
93
+ expect(wrapper.find('.mint-spinner').exists()).toBe(true)
94
+ expect(wrapper.find(contentSelector).exists()).toBe(false)
95
+ })
96
+
97
+ it('renders an error state that wins over content and loading', () => {
98
+ const wrapper = mount(component, {
99
+ props: { ...filledProps, loading: true, error: 'Request failed' } as never,
100
+ })
101
+
102
+ expect(wrapper.find('[data-state="error"]').exists()).toBe(true)
103
+ expect(wrapper.find('[data-state="loading"]').exists()).toBe(false)
104
+ expect(wrapper.find('.mint-empty-state__title').text()).toBe('Request failed')
105
+ expect(wrapper.find(contentSelector).exists()).toBe(false)
106
+ })
107
+
108
+ it('lets the empty slot replace the default placeholder', () => {
109
+ const wrapper = mount(component, {
110
+ props: { ...emptyProps } as never,
111
+ slots: { empty: '<p class="slot-empty">custom</p>' },
112
+ })
113
+
114
+ expect(wrapper.find('.slot-empty').exists()).toBe(true)
115
+ expect(wrapper.find('[data-state="empty"]').exists()).toBe(false)
116
+ })
117
+
118
+ it('lets the error slot replace the default placeholder', () => {
119
+ const wrapper = mount(component, {
120
+ props: { ...filledProps, error: 'Boom' } as never,
121
+ slots: { error: '<p class="slot-error">custom</p>' },
122
+ })
123
+
124
+ expect(wrapper.find('.slot-error').exists()).toBe(true)
125
+ expect(wrapper.find('[data-state="error"]').exists()).toBe(false)
126
+ })
127
+ })
128
+
129
+ describe('opt-in empty states', () => {
130
+ it('keeps the WellPlate grid when no emptyMessage is given', () => {
131
+ const wrapper = mount(WellPlate, { props: { wells: {} } })
132
+
133
+ expect(wrapper.find('.mint-well-plate__scroll').exists()).toBe(true)
134
+ expect(wrapper.find('[data-state]').exists()).toBe(false)
135
+ })
136
+
137
+ it('replaces the WellPlate grid once an emptyMessage is given', () => {
138
+ const wrapper = mount(WellPlate, {
139
+ props: { wells: {}, emptyMessage: 'No plate loaded' },
140
+ })
141
+
142
+ expect(wrapper.find('[data-state="empty"]').exists()).toBe(true)
143
+ expect(wrapper.find('.mint-well-plate__scroll').exists()).toBe(false)
144
+ })
145
+
146
+ it('replaces the WellPlate grid on error', () => {
147
+ const wrapper = mount(WellPlate, { props: { error: 'Boom' } })
148
+
149
+ expect(wrapper.find('[data-state="error"]').exists()).toBe(true)
150
+ expect(wrapper.find('.mint-well-plate__scroll').exists()).toBe(false)
151
+ })
152
+
153
+ it('keeps the ScheduleCalendar grid when no emptyMessage is given', () => {
154
+ const wrapper = mount(ScheduleCalendar, { props: { events: [] } })
155
+
156
+ expect(wrapper.find('.mint-schedule__body').exists()).toBe(true)
157
+ expect(wrapper.find('[data-state]').exists()).toBe(false)
158
+ })
159
+
160
+ it('replaces the ScheduleCalendar grid once an emptyMessage is given', () => {
161
+ const wrapper = mount(ScheduleCalendar, {
162
+ props: { events: [], emptyMessage: 'Nothing scheduled' },
163
+ })
164
+
165
+ expect(wrapper.find('[data-state="empty"]').exists()).toBe(true)
166
+ expect(wrapper.find('.mint-schedule__body').exists()).toBe(false)
167
+ })
168
+
169
+ it('replaces the ScheduleCalendar grid while loading', () => {
170
+ const wrapper = mount(ScheduleCalendar, { props: { loading: true } })
171
+
172
+ expect(wrapper.find('[data-state="loading"]').exists()).toBe(true)
173
+ expect(wrapper.find('.mint-schedule__body').exists()).toBe(false)
174
+ })
175
+ })
176
+ })
@@ -42,4 +42,80 @@ describe('DatePicker', () => {
42
42
 
43
43
  wrapper.unmount()
44
44
  })
45
+
46
+ it('opens from the input with Enter, Space and ArrowDown', async () => {
47
+ const wrapper = mount(DatePicker, { attachTo: document.body })
48
+
49
+ for (const key of ['Enter', ' ', 'ArrowDown']) {
50
+ await wrapper.get('input').trigger('keydown', { key })
51
+ expect(dropdown()).not.toBeNull()
52
+
53
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
54
+ await nextTick()
55
+ expect(dropdown()).toBeNull()
56
+ }
57
+
58
+ wrapper.unmount()
59
+ })
60
+
61
+ it('moves focus into the calendar grid and back to the input on close', async () => {
62
+ const wrapper = mount(DatePicker, {
63
+ props: { modelValue: '2026-04-22' },
64
+ attachTo: document.body,
65
+ })
66
+ const input = wrapper.get<HTMLInputElement>('input').element
67
+
68
+ await wrapper.get('input').trigger('click')
69
+ await nextTick()
70
+ await nextTick()
71
+ await nextTick()
72
+
73
+ expect(document.activeElement).toBe(
74
+ document.body.querySelector('.mint-date-picker__day--selected'),
75
+ )
76
+
77
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
78
+ await nextTick()
79
+ await nextTick()
80
+
81
+ expect(document.activeElement).toBe(input)
82
+
83
+ wrapper.unmount()
84
+ })
85
+
86
+ it('marks the panel as a non-modal dialog', async () => {
87
+ const wrapper = mount(DatePicker, { attachTo: document.body })
88
+
89
+ await wrapper.get('input').trigger('click')
90
+
91
+ const panel = dropdown()
92
+ expect(panel?.getAttribute('role')).toBe('dialog')
93
+ expect(panel?.hasAttribute('aria-modal')).toBe(false)
94
+ expect(wrapper.get('input').attributes('aria-haspopup')).toBe('dialog')
95
+ expect(wrapper.get('input').attributes('aria-expanded')).toBe('true')
96
+
97
+ wrapper.unmount()
98
+ })
99
+
100
+ it('keeps the panel open while the page scrolls, repositioning instead', async () => {
101
+ const wrapper = mount(DatePicker, { attachTo: document.body })
102
+
103
+ await wrapper.get('input').trigger('click')
104
+ window.dispatchEvent(new Event('scroll'))
105
+ await nextTick()
106
+
107
+ expect(dropdown()).not.toBeNull()
108
+
109
+ wrapper.unmount()
110
+ })
111
+
112
+ it('does not open from the keyboard while disabled', async () => {
113
+ const wrapper = mount(DatePicker, { props: { disabled: true }, attachTo: document.body })
114
+
115
+ await wrapper.get('input').trigger('keydown', { key: 'Enter' })
116
+
117
+ expect(dropdown()).toBeNull()
118
+
119
+ wrapper.unmount()
120
+ })
45
121
  })