@morscherlab/mint-sdk 1.2.0-beta.8 → 1.2.0

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 (402) hide show
  1. package/README.md +60 -0
  2. package/dist/{ExperimentPopover-CfOdh5Rm.js → ExperimentPopover-CJdNlCZb.js} +24 -15
  3. package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-DvIagrxY.js → ExperimentSelectorModal-DCXmwKNS.js} +5 -5
  5. package/dist/ExperimentSelectorModal-DCXmwKNS.js.map +1 -0
  6. package/dist/{SettingsModal-W3TYfnP9.js → SettingsModal-BYR20I4c.js} +5 -5
  7. package/dist/SettingsModal-BYR20I4c.js.map +1 -0
  8. package/dist/{EmptyState-NBqf2_8z.js → Skeleton-BAF3CKY7.js} +111 -93
  9. package/dist/Skeleton-BAF3CKY7.js.map +1 -0
  10. package/dist/__tests__/components/DataStates.test.d.ts +1 -0
  11. package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
  12. package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
  13. package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
  14. package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
  15. package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
  16. package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
  17. package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
  18. package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
  19. package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
  20. package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
  21. package/dist/apiErrors.d.ts +26 -0
  22. package/dist/components/ActionMenu.vue.d.ts +1 -1
  23. package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
  24. package/dist/components/AuditTrail.vue.d.ts +13 -0
  25. package/dist/components/BaseInput.vue.d.ts +1 -1
  26. package/dist/components/BaseSelect.vue.d.ts +1 -1
  27. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  28. package/dist/components/BatchProgressList.vue.d.ts +17 -0
  29. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  30. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
  31. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  32. package/dist/components/DataFrame.vue.d.ts +14 -2
  33. package/dist/components/DatePicker.vue.d.ts +2 -2
  34. package/dist/components/DateTimePicker.vue.d.ts +3 -1
  35. package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
  36. package/dist/components/ExperimentPopover.vue.d.ts +1 -0
  37. package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
  38. package/dist/components/FileBrowserModal.vue.d.ts +99 -0
  39. package/dist/components/FilePicker.vue.d.ts +32 -0
  40. package/dist/components/FileUploader.vue.d.ts +5 -2
  41. package/dist/components/FormulaInput.vue.d.ts +1 -1
  42. package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
  43. package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
  44. package/dist/components/MobileSupportGate.vue.d.ts +1 -1
  45. package/dist/components/MoleculeInput.vue.d.ts +1 -1
  46. package/dist/components/NumberInput.vue.d.ts +1 -1
  47. package/dist/components/PlotlyChart.vue.d.ts +59 -0
  48. package/dist/components/ProgressBar.vue.d.ts +1 -1
  49. package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
  50. package/dist/components/ReagentList.vue.d.ts +29 -1
  51. package/dist/components/ResourceCard.vue.d.ts +1 -1
  52. package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
  53. package/dist/components/SampleSelector.drag.d.ts +13 -0
  54. package/dist/components/SampleSelector.tree.d.ts +2 -0
  55. package/dist/components/SampleSelector.vue.d.ts +34 -1
  56. package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
  57. package/dist/components/SecretInput.vue.d.ts +1 -1
  58. package/dist/components/SequenceInput.vue.d.ts +1 -1
  59. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -0
  60. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  61. package/dist/components/TagsInput.vue.d.ts +1 -1
  62. package/dist/components/TimePicker.vue.d.ts +2 -1
  63. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  64. package/dist/components/UnitInput.vue.d.ts +1 -1
  65. package/dist/components/WellPlate.geometry.d.ts +15 -0
  66. package/dist/components/WellPlate.interaction.d.ts +13 -0
  67. package/dist/components/WellPlate.vue.d.ts +30 -1
  68. package/dist/components/fileBrowserPresentation.d.ts +10 -0
  69. package/dist/components/index.d.ts +4 -0
  70. package/dist/components/index.js +8 -8
  71. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
  72. package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
  73. package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
  74. package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
  75. package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
  76. package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
  77. package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
  78. package/dist/{components-B80gV0P6.js → components-DIum5hkx.js} +5415 -2753
  79. package/dist/components-DIum5hkx.js.map +1 -0
  80. package/dist/composables/controlSchemaTypes.d.ts +34 -0
  81. package/dist/composables/filePicker/localFiles.d.ts +3 -0
  82. package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
  83. package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
  84. package/dist/composables/filePicker/validation.d.ts +5 -0
  85. package/dist/composables/index.d.ts +3 -0
  86. package/dist/composables/index.js +10 -10
  87. package/dist/composables/useApi.d.ts +14 -7
  88. package/dist/composables/useDropdownState.d.ts +18 -2
  89. package/dist/composables/useEventStream.d.ts +9 -0
  90. package/dist/composables/useFileBrowser.d.ts +55 -0
  91. package/dist/composables/useFilePicker.d.ts +189 -0
  92. package/dist/composables/useListReorder.d.ts +90 -0
  93. package/dist/composables/useMenuKeyboard.d.ts +2 -2
  94. package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
  95. package/dist/composables/usePluginConfig.d.ts +3 -1
  96. package/dist/composables/useRequestSyncState.d.ts +42 -2
  97. package/dist/composables/useScheduleDrag.d.ts +1 -0
  98. package/dist/{composables-C8mmD5ta.js → composables-mFXsTA4g.js} +307 -74
  99. package/dist/composables-mFXsTA4g.js.map +1 -0
  100. package/dist/{useApi-CDBq6z23.js → errors-BLuqMee5.js} +178 -16
  101. package/dist/errors-BLuqMee5.js.map +1 -0
  102. package/dist/{experiment-Y9Iz7KYi.js → experiment-CVDRAnqp.js} +3 -8
  103. package/dist/experiment-CVDRAnqp.js.map +1 -0
  104. package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
  105. package/dist/experiment-utils-CV2Wza3z.js.map +1 -0
  106. package/dist/index.d.ts +1 -0
  107. package/dist/index.js +15 -15
  108. package/dist/install.js +2 -7
  109. package/dist/install.js.map +1 -1
  110. package/dist/permissions.d.ts +42 -1
  111. package/dist/permissions.js +20 -10
  112. package/dist/permissions.js.map +1 -1
  113. package/dist/stores/index.d.ts +1 -1
  114. package/dist/stores/index.js +3 -3
  115. package/dist/stores/settings.d.ts +31 -1
  116. package/dist/styles.css +15162 -32475
  117. package/dist/templates/index.js +2 -4
  118. package/dist/{templates-CK72aKO1.js → templates-CmGfmBRA.js} +291 -85
  119. package/dist/templates-CmGfmBRA.js.map +1 -0
  120. package/dist/types/fileBrowserTypes.d.ts +77 -0
  121. package/dist/types/filePicker.d.ts +77 -0
  122. package/dist/types/index.d.ts +2 -0
  123. package/dist/{useControlSchema-_OVhavKq.js → useControlSchema-DldpxNOP.js} +234 -1121
  124. package/dist/useControlSchema-DldpxNOP.js.map +1 -0
  125. package/dist/{useDropdownState-D5G4UvWd.js → useDropdownState-C7hRiVQf.js} +22 -6
  126. package/dist/useDropdownState-C7hRiVQf.js.map +1 -0
  127. package/dist/{useExperimentSelector-DSEckf_g.js → useExperimentSelector-CEcIaueJ.js} +4 -4
  128. package/dist/{useExperimentSelector-DSEckf_g.js.map → useExperimentSelector-CEcIaueJ.js.map} +1 -1
  129. package/dist/{useFormBuilder-Bd714C7y.js → useFormBuilder-z5L_dUIy.js} +2 -2
  130. package/dist/{useFormBuilder-Bd714C7y.js.map → useFormBuilder-z5L_dUIy.js.map} +1 -1
  131. package/dist/utils/color.d.ts +13 -0
  132. package/dist/utils/errors.d.ts +9 -0
  133. package/dist/{useProtocolTemplates-Dq5Pzwyd.js → validation-uwoJGEb6.js} +231 -158
  134. package/dist/validation-uwoJGEb6.js.map +1 -0
  135. package/package.json +1 -1
  136. package/src/__tests__/apiErrors.test.ts +81 -0
  137. package/src/__tests__/components/ActionMenu.test.ts +94 -0
  138. package/src/__tests__/components/DataFrame.test.ts +183 -11
  139. package/src/__tests__/components/DataStates.test.ts +176 -0
  140. package/src/__tests__/components/DatePicker.test.ts +76 -0
  141. package/src/__tests__/components/DateTimePicker.test.ts +36 -0
  142. package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
  143. package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
  144. package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
  145. package/src/__tests__/components/FilePicker.state.test.ts +287 -0
  146. package/src/__tests__/components/FilePicker.test.ts +487 -0
  147. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
  148. package/src/__tests__/components/GroupAssigner.test.ts +53 -0
  149. package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
  150. package/src/__tests__/components/PlotlyChart.test.ts +305 -0
  151. package/src/__tests__/components/RackEditor.test.ts +35 -0
  152. package/src/__tests__/components/ReagentList.test.ts +64 -2
  153. package/src/__tests__/components/RetiredStyles.test.ts +31 -0
  154. package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
  155. package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
  156. package/src/__tests__/components/SegmentedControl.test.ts +107 -0
  157. package/src/__tests__/components/TimePicker.test.ts +61 -0
  158. package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
  159. package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
  160. package/src/__tests__/composables/useApi.test.ts +92 -9
  161. package/src/__tests__/composables/useDropdownState.test.ts +58 -1
  162. package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
  163. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
  164. package/src/__tests__/composables/useListReorder.test.ts +145 -0
  165. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
  166. package/src/__tests__/composables/usePluginClient.test.ts +3 -0
  167. package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
  168. package/src/__tests__/stores/settings.test.ts +71 -1
  169. package/src/__tests__/styles/cssTokens.test.ts +131 -0
  170. package/src/__tests__/utils/permissions.test.ts +68 -0
  171. package/src/apiErrors.ts +85 -0
  172. package/src/components/ActionMenu.vue +45 -88
  173. package/src/components/AlertBox.vue +0 -4
  174. package/src/components/AppAvatarMenu.vue +2 -14
  175. package/src/components/AppLayout.vue +0 -4
  176. package/src/components/AppPluginSwitcher.story.vue +87 -0
  177. package/src/components/AppPluginSwitcher.vue +2 -5
  178. package/src/components/AppSidebar.vue +0 -4
  179. package/src/components/AppToastContainer.vue +6 -8
  180. package/src/components/AppTopBar.vue +0 -4
  181. package/src/components/ArtifactSaveDialog.vue +0 -4
  182. package/src/components/ArtifactSelectorModal.vue +0 -4
  183. package/src/components/AuditTrail.story.vue +18 -0
  184. package/src/components/AuditTrail.vue +27 -7
  185. package/src/components/AutoGroupModal.vue +0 -4
  186. package/src/components/Avatar.vue +0 -4
  187. package/src/components/BaseButton.vue +0 -4
  188. package/src/components/BaseCheckbox.vue +0 -4
  189. package/src/components/BaseInput.vue +0 -4
  190. package/src/components/BaseModal.vue +0 -4
  191. package/src/components/BasePill.vue +0 -4
  192. package/src/components/BaseRadioGroup.vue +0 -4
  193. package/src/components/BaseSelect.vue +0 -4
  194. package/src/components/BaseSlider.vue +0 -4
  195. package/src/components/BaseTabs.vue +0 -4
  196. package/src/components/BaseTextarea.vue +0 -4
  197. package/src/components/BaseToggle.vue +0 -4
  198. package/src/components/BatchProgressList.story.vue +18 -0
  199. package/src/components/BatchProgressList.vue +32 -4
  200. package/src/components/Breadcrumb.vue +0 -4
  201. package/src/components/Calendar.vue +0 -4
  202. package/src/components/ChartContainer.story.vue +1 -1
  203. package/src/components/ChartContainer.vue +0 -4
  204. package/src/components/ChemicalFormula.vue +0 -4
  205. package/src/components/CollapsibleCard.vue +0 -4
  206. package/src/components/ColorSlider.vue +0 -4
  207. package/src/components/ConcentrationInput.vue +0 -4
  208. package/src/components/ConfirmDialog.vue +0 -4
  209. package/src/components/DataFrame.story.vue +14 -0
  210. package/src/components/DataFrame.vue +97 -23
  211. package/src/components/DatePicker.vue +73 -27
  212. package/src/components/DateTimePicker.vue +45 -17
  213. package/src/components/Divider.vue +0 -4
  214. package/src/components/DoseCalculator.vue +0 -4
  215. package/src/components/DropdownButton.vue +2 -14
  216. package/src/components/EmptyState.vue +0 -4
  217. package/src/components/ExperimentCodeBadge.vue +0 -4
  218. package/src/components/ExperimentDataViewer.story.vue +3 -0
  219. package/src/components/ExperimentDataViewer.vue +33 -13
  220. package/src/components/ExperimentPopover.vue +12 -7
  221. package/src/components/ExperimentSelectorModal.vue +0 -4
  222. package/src/components/ExperimentTimeline.story.vue +18 -0
  223. package/src/components/ExperimentTimeline.vue +81 -75
  224. package/src/components/FileBrowserModal.story.vue +203 -0
  225. package/src/components/FileBrowserModal.vue +384 -0
  226. package/src/components/FilePicker.story.vue +44 -0
  227. package/src/components/FilePicker.vue +284 -0
  228. package/src/components/FileUploader.vue +32 -18
  229. package/src/components/FitPanel.vue +0 -4
  230. package/src/components/FormActions.story.vue +96 -0
  231. package/src/components/FormActions.vue +0 -4
  232. package/src/components/FormBuilder.vue +0 -4
  233. package/src/components/FormField.vue +0 -4
  234. package/src/components/FormulaInput.vue +0 -4
  235. package/src/components/GeneratedPathInput.story.vue +68 -0
  236. package/src/components/GeneratedPlotlyResult.story.vue +96 -0
  237. package/src/components/GeneratedPlotlyResult.vue +34 -69
  238. package/src/components/GeneratedPluginView.story.vue +145 -0
  239. package/src/components/GeneratedPluginView.vue +10 -10
  240. package/src/components/GeneratedResultRenderer.story.vue +117 -0
  241. package/src/components/GeneratedResultRenderer.vue +4 -9
  242. package/src/components/GroupAssigner.vue +67 -5
  243. package/src/components/IconButton.vue +0 -4
  244. package/src/components/InstrumentAlertLog.vue +0 -4
  245. package/src/components/InstrumentStateBadge.vue +0 -4
  246. package/src/components/InstrumentStatusCard.vue +0 -4
  247. package/src/components/JobsStatusTray.vue +0 -4
  248. package/src/components/LcmsSequenceTable.vue +81 -50
  249. package/src/components/LoadingSpinner.vue +0 -4
  250. package/src/components/MobileSupportGate.vue +0 -4
  251. package/src/components/MoleculeInput.vue +0 -4
  252. package/src/components/MultiSelect.vue +0 -4
  253. package/src/components/NumberInput.vue +0 -4
  254. package/src/components/PlateMapEditor.vue +3 -14
  255. package/src/components/PlotlyChart.story.vue +61 -0
  256. package/src/components/PlotlyChart.vue +233 -0
  257. package/src/components/PluginIcon.vue +0 -4
  258. package/src/components/ProgressBar.vue +0 -4
  259. package/src/components/ProtocolStepEditor.vue +2 -4
  260. package/src/components/RackEditor.vue +51 -70
  261. package/src/components/ReagentEditor.vue +0 -4
  262. package/src/components/ReagentList.story.vue +18 -0
  263. package/src/components/ReagentList.vue +127 -82
  264. package/src/components/ResourceCard.vue +0 -4
  265. package/src/components/RuntimeUpdateNotice.story.vue +49 -0
  266. package/src/components/RuntimeUpdateNotice.vue +4 -4
  267. package/src/components/SampleHierarchyTree.story.vue +18 -0
  268. package/src/components/SampleHierarchyTree.vue +32 -9
  269. package/src/components/SampleLegend.vue +2 -8
  270. package/src/components/SampleSelector.drag.ts +51 -0
  271. package/src/components/SampleSelector.story.vue +6 -0
  272. package/src/components/SampleSelector.tree.ts +2 -0
  273. package/src/components/SampleSelector.vue +49 -10
  274. package/src/components/SampleSelectorGroupNode.vue +19 -0
  275. package/src/components/ScheduleCalendar.story.vue +6 -0
  276. package/src/components/ScheduleCalendar.vue +82 -7
  277. package/src/components/ScientificNumber.vue +0 -4
  278. package/src/components/SectionCard.vue +0 -4
  279. package/src/components/SegmentedControl.vue +50 -7
  280. package/src/components/SequenceInput.vue +0 -4
  281. package/src/components/SequenceProgressBar.story.vue +110 -0
  282. package/src/components/SequenceProgressBar.vue +0 -4
  283. package/src/components/SettingsModal.vue +0 -4
  284. package/src/components/Skeleton.vue +0 -4
  285. package/src/components/SmartGroupFieldRecipe.vue +8 -6
  286. package/src/components/SmartGroupManual.vue +0 -4
  287. package/src/components/StatusIndicator.vue +0 -4
  288. package/src/components/StepWizard.vue +0 -4
  289. package/src/components/TagsInput.vue +4 -6
  290. package/src/components/ThemeToggle.vue +0 -4
  291. package/src/components/TimePicker.vue +36 -7
  292. package/src/components/TimeRangeInput.vue +0 -4
  293. package/src/components/Tooltip.story.vue +3 -3
  294. package/src/components/Tooltip.vue +0 -4
  295. package/src/components/UnitInput.vue +0 -4
  296. package/src/components/WellPlate.geometry.ts +60 -1
  297. package/src/components/WellPlate.interaction.ts +44 -0
  298. package/src/components/WellPlate.story.vue +8 -0
  299. package/src/components/WellPlate.vue +53 -7
  300. package/src/components/fileBrowserPresentation.ts +39 -0
  301. package/src/components/index.ts +5 -0
  302. package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
  303. package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
  304. package/src/components/internal/DataStateInternal.vue +74 -0
  305. package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
  306. package/src/components/internal/FileBrowserListingInternal.vue +197 -0
  307. package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
  308. package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
  309. package/src/components/internal/FilePickerTreeInternal.vue +238 -0
  310. package/src/components/internal/FormSectionRenderer.vue +0 -4
  311. package/src/components/internal/WellEditPopupInternal.vue +0 -4
  312. package/src/composables/controlSchemaTypes.ts +34 -0
  313. package/src/composables/filePicker/localFiles.ts +58 -0
  314. package/src/composables/filePicker/usePickerNavigation.ts +220 -0
  315. package/src/composables/filePicker/usePickerSelection.ts +259 -0
  316. package/src/composables/filePicker/validation.ts +41 -0
  317. package/src/composables/index.ts +17 -0
  318. package/src/composables/useApi.ts +57 -13
  319. package/src/composables/useDropdownState.ts +35 -5
  320. package/src/composables/useEventStream.ts +14 -2
  321. package/src/composables/useFileBrowser.ts +314 -0
  322. package/src/composables/useFileImport.ts +2 -7
  323. package/src/composables/useFilePicker.ts +366 -0
  324. package/src/composables/useGeneratedAnalysis.ts +18 -26
  325. package/src/composables/useListReorder.story.vue +143 -0
  326. package/src/composables/useListReorder.ts +242 -0
  327. package/src/composables/useMenuKeyboard.ts +2 -2
  328. package/src/composables/useOptimisticMutation.ts +1 -15
  329. package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
  330. package/src/composables/usePluginClient.ts +3 -0
  331. package/src/composables/usePluginConfig.ts +3 -1
  332. package/src/composables/useRackEditor.ts +2 -11
  333. package/src/composables/useRequestSyncState.ts +98 -28
  334. package/src/composables/useResourceCrud.ts +1 -15
  335. package/src/composables/useScheduleDrag.ts +37 -0
  336. package/src/composables/useWellPainting.ts +5 -29
  337. package/src/composables/useWellPlateEditor.ts +1 -11
  338. package/src/index.ts +3 -0
  339. package/src/permissions.ts +48 -9
  340. package/src/stores/experiment.ts +2 -7
  341. package/src/stores/index.ts +9 -1
  342. package/src/stores/settings.ts +93 -1
  343. package/src/styles/components/audit-trail.css +5 -5
  344. package/src/styles/components/batch-progress-list.css +7 -0
  345. package/src/styles/components/color-slider.css +2 -2
  346. package/src/styles/components/data-state.css +30 -0
  347. package/src/styles/components/dataframe.css +29 -35
  348. package/src/styles/components/experiment-data-viewer.css +4 -4
  349. package/src/styles/components/experiment-timeline.css +3 -30
  350. package/src/styles/components/file-browser-modal.css +489 -0
  351. package/src/styles/components/file-picker.css +502 -0
  352. package/src/styles/components/form-builder.css +5 -5
  353. package/src/styles/components/icon-button.css +9 -0
  354. package/src/styles/components/lcms-sequence-table.css +16 -1
  355. package/src/styles/components/multi-select.css +1 -1
  356. package/src/styles/components/reagent-list.css +38 -31
  357. package/src/styles/components/sample-hierarchy-tree.css +5 -19
  358. package/src/styles/components/sample-selector.css +9 -2
  359. package/src/styles/components/schedule-calendar.css +8 -0
  360. package/src/styles/components/scientific-number.css +1 -1
  361. package/src/styles/components/well-plate.css +8 -0
  362. package/src/styles/index.css +11 -0
  363. package/src/styles/variables.css +29 -0
  364. package/src/templates/builderPresetControls.ts +1 -1
  365. package/src/templates/componentPlateHelpers.ts +3 -1
  366. package/src/templates/doseResponseAdapters.ts +2 -13
  367. package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
  368. package/src/templates/plateMapBuilder.ts +2 -2
  369. package/src/templates/qpcrPlateBuilder.ts +1 -1
  370. package/src/templates/templateValidators.ts +1 -1
  371. package/src/templates/timeCourseBuilder.ts +2 -2
  372. package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
  373. package/src/types/fileBrowserTypes.ts +85 -0
  374. package/src/types/filePicker.ts +72 -0
  375. package/src/types/index.ts +14 -0
  376. package/src/utils/color.ts +51 -0
  377. package/src/utils/errors.ts +29 -0
  378. package/dist/EmptyState-NBqf2_8z.js.map +0 -1
  379. package/dist/ExperimentPopover-CfOdh5Rm.js.map +0 -1
  380. package/dist/ExperimentSelectorModal-DvIagrxY.js.map +0 -1
  381. package/dist/SettingsModal-W3TYfnP9.js.map +0 -1
  382. package/dist/components-B80gV0P6.js.map +0 -1
  383. package/dist/composables/autoGroupConstants.d.ts +0 -1
  384. package/dist/composables/autoGroupGrouping.d.ts +0 -3
  385. package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
  386. package/dist/composables-C8mmD5ta.js.map +0 -1
  387. package/dist/experiment-Y9Iz7KYi.js.map +0 -1
  388. package/dist/experiment-utils-DV3DrvE_.js.map +0 -1
  389. package/dist/templates/builderDefaults.d.ts +0 -1
  390. package/dist/templates/plateGeometry.d.ts +0 -4
  391. package/dist/templates-CK72aKO1.js.map +0 -1
  392. package/dist/useApi-CDBq6z23.js.map +0 -1
  393. package/dist/useControlSchema-_OVhavKq.js.map +0 -1
  394. package/dist/useDropdownState-D5G4UvWd.js.map +0 -1
  395. package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +0 -1
  396. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
  397. package/src/composables/autoGroupConstants.ts +0 -4
  398. package/src/composables/autoGroupGrouping.ts +0 -148
  399. package/src/composables/useAutoGroupInputSources.ts +0 -147
  400. package/src/templates/builderDefaults.ts +0 -11
  401. package/src/templates/plateGeometry.ts +0 -62
  402. /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → apiErrors.test.d.ts} +0 -0
@@ -3,6 +3,25 @@ import { describe, expect, it } from 'vitest'
3
3
  import SegmentedControl from '../../components/SegmentedControl.vue'
4
4
 
5
5
  describe('SegmentedControl', () => {
6
+ it('defaults to card while allowing an explicit simple variant', async () => {
7
+ const wrapper = mount(SegmentedControl, {
8
+ props: {
9
+ modelValue: 'table',
10
+ options: [{ value: 'table', label: 'Table', description: 'View rows' }],
11
+ },
12
+ })
13
+
14
+ expect(wrapper.classes()).toContain('mint-segmented-control--card')
15
+ expect(wrapper.get('button').classes()).toContain('mint-segmented-control__option--card')
16
+ expect(wrapper.get('.mint-segmented-control__description').text()).toBe('View rows')
17
+
18
+ await wrapper.setProps({ variant: 'simple' })
19
+
20
+ expect(wrapper.classes()).toContain('mint-segmented-control--simple')
21
+ expect(wrapper.get('button').classes()).toContain('mint-segmented-control__option--simple')
22
+ expect(wrapper.find('.mint-segmented-control__description').exists()).toBe(false)
23
+ })
24
+
6
25
  it('accepts string and number shorthand options', async () => {
7
26
  const wrapper = mount(SegmentedControl, {
8
27
  props: {
@@ -66,4 +85,92 @@ describe('SegmentedControl', () => {
66
85
 
67
86
  expect(wrapper.emitted('update:modelValue')).toBeUndefined()
68
87
  })
88
+
89
+ describe('roving tabindex', () => {
90
+ const options = [
91
+ { value: 'table', label: 'Table' },
92
+ { value: 'chart', label: 'Chart' },
93
+ { value: 'raw', label: 'Raw' },
94
+ ]
95
+
96
+ it('makes only the checked option tabbable', () => {
97
+ const wrapper = mount(SegmentedControl, {
98
+ props: { modelValue: 'chart', options },
99
+ })
100
+
101
+ expect(wrapper.findAll('button').map(button => button.attributes('tabindex')))
102
+ .toEqual(['-1', '0', '-1'])
103
+ })
104
+
105
+ it('falls back to the first selectable option when nothing matches', () => {
106
+ const wrapper = mount(SegmentedControl, {
107
+ props: { modelValue: 'missing', options },
108
+ })
109
+
110
+ expect(wrapper.findAll('button').map(button => button.attributes('tabindex')))
111
+ .toEqual(['0', '-1', '-1'])
112
+ })
113
+
114
+ it('activates the adjacent option with the arrow keys, wrapping at the ends', async () => {
115
+ const wrapper = mount(SegmentedControl, {
116
+ props: { modelValue: 'chart', options },
117
+ attachTo: document.body,
118
+ })
119
+
120
+ const buttons = wrapper.findAll('button')
121
+ await buttons[1].trigger('keydown', { key: 'ArrowRight' })
122
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['raw'])
123
+
124
+ await buttons[1].trigger('keydown', { key: 'ArrowLeft' })
125
+ expect(wrapper.emitted('update:modelValue')?.[1]).toEqual(['table'])
126
+
127
+ await wrapper.setProps({ modelValue: 'raw' })
128
+ await wrapper.findAll('button')[2].trigger('keydown', { key: 'ArrowRight' })
129
+ expect(wrapper.emitted('update:modelValue')?.[2]).toEqual(['table'])
130
+
131
+ wrapper.unmount()
132
+ })
133
+
134
+ it('jumps to the first and last option with Home and End', async () => {
135
+ const wrapper = mount(SegmentedControl, {
136
+ props: { modelValue: 'chart', options },
137
+ })
138
+
139
+ const buttons = wrapper.findAll('button')
140
+ await buttons[1].trigger('keydown', { key: 'Home' })
141
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['table'])
142
+
143
+ await buttons[1].trigger('keydown', { key: 'End' })
144
+ expect(wrapper.emitted('update:modelValue')?.[1]).toEqual(['raw'])
145
+ })
146
+
147
+ it('skips disabled options during arrow navigation', async () => {
148
+ const wrapper = mount(SegmentedControl, {
149
+ props: {
150
+ modelValue: 'table',
151
+ options,
152
+ disabledValues: ['chart'],
153
+ },
154
+ })
155
+
156
+ await wrapper.findAll('button')[0].trigger('keydown', { key: 'ArrowRight' })
157
+
158
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['raw'])
159
+ })
160
+
161
+ it('moves focus onto the newly activated option', async () => {
162
+ const wrapper = mount(SegmentedControl, {
163
+ props: { modelValue: 'table', options },
164
+ attachTo: document.body,
165
+ })
166
+
167
+ await wrapper.findAll('button')[0].trigger('keydown', { key: 'ArrowRight' })
168
+ await wrapper.setProps({ modelValue: 'chart' })
169
+ await wrapper.vm.$nextTick()
170
+
171
+ expect(document.activeElement).toBe(wrapper.findAll('button')[1].element)
172
+
173
+ wrapper.unmount()
174
+ })
175
+ })
69
176
  })
@@ -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
  })
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it, vi } from 'vitest'
2
+ import { nextTick } from 'vue'
2
3
  import {
3
4
  nextClickedWellSelection,
4
5
  useWellPlateInteraction,
@@ -41,6 +42,14 @@ function dragEvent(data: Record<string, string> = {}): DragEvent {
41
42
  } as unknown as DragEvent
42
43
  }
43
44
 
45
+ function keyEvent(key: string, altKey = true): KeyboardEvent {
46
+ return {
47
+ key,
48
+ altKey,
49
+ preventDefault: vi.fn(),
50
+ } as unknown as KeyboardEvent
51
+ }
52
+
44
53
  function createInteraction(overrides: Partial<Parameters<typeof useWellPlateInteraction>[0]> = {}) {
45
54
  const emitted: Record<string, unknown[]> = {}
46
55
  const selected = { value: [] as string[] }
@@ -168,4 +177,54 @@ describe('WellPlate interaction helpers', () => {
168
177
  ['A1', 'A2', 'B1', 'B2'],
169
178
  ])
170
179
  })
180
+
181
+ it('moves a well with the keyboard pick-up/drop chord in drag mode', async () => {
182
+ const { emitted, interaction, selectionMode } = createInteraction()
183
+ selectionMode.value = 'drag'
184
+ const source = well({ id: 'A1', row: 0, col: 0, sampleType: 'sample' })
185
+ const target = well({ id: 'B2', row: 1, col: 1 })
186
+
187
+ interaction.handleWellMoveKeydown(source, keyEvent('Enter'))
188
+ expect(interaction.dragSourceWell.value).toBe('A1')
189
+
190
+ interaction.handleWellMoveKeydown(target, keyEvent('Enter'))
191
+
192
+ expect(emitted.move).toEqual([['A1', 'B2']])
193
+ expect(interaction.dragSourceWell.value).toBeNull()
194
+
195
+ await nextTick()
196
+ expect(interaction.moveAnnouncement.value).toBe('Well A1 moved to B2.')
197
+ })
198
+
199
+ it('cancels a keyboard pick-up with Escape and refuses empty wells', () => {
200
+ const { emitted, interaction, selectionMode } = createInteraction()
201
+ selectionMode.value = 'drag'
202
+
203
+ interaction.handleWellMoveKeydown(well({ id: 'A2', row: 0, col: 1 }), keyEvent('Enter'))
204
+ expect(interaction.dragSourceWell.value).toBeNull()
205
+
206
+ interaction.handleWellMoveKeydown(
207
+ well({ id: 'A1', row: 0, col: 0, sampleType: 'sample' }),
208
+ keyEvent('Enter'),
209
+ )
210
+ expect(interaction.dragSourceWell.value).toBe('A1')
211
+
212
+ interaction.handleWellMoveKeydown(
213
+ well({ id: 'A1', row: 0, col: 0, sampleType: 'sample' }),
214
+ keyEvent('Escape', false),
215
+ )
216
+ expect(interaction.dragSourceWell.value).toBeNull()
217
+ expect(emitted.move).toBeUndefined()
218
+ })
219
+
220
+ it('ignores the move chord outside drag selection mode', () => {
221
+ const { interaction } = createInteraction()
222
+
223
+ interaction.handleWellMoveKeydown(
224
+ well({ id: 'A1', row: 0, col: 0, sampleType: 'sample' }),
225
+ keyEvent('Enter'),
226
+ )
227
+
228
+ expect(interaction.dragSourceWell.value).toBeNull()
229
+ })
171
230
  })
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ // jsdom has no layout, so this pins the stylesheet contract instead: a bare
6
+ // <svg> passed to IconButton's slot must be sized by the icon box, otherwise it
7
+ // collapses to zero width as a flex item (FilePicker source toggles, beta.14).
8
+ describe('IconButton icon sizing', () => {
9
+ it('should size a bare slot svg to the icon box when the caller passes no dimensions', () => {
10
+ const css = readFileSync(
11
+ resolve(__dirname, '../../styles/components/icon-button.css'),
12
+ 'utf8',
13
+ )
14
+ expect(css).toMatch(
15
+ /\.mint-icon-button__icon--sm > svg,\s*\.mint-icon-button__icon--md > svg,\s*\.mint-icon-button__icon--lg > svg \{\s*width: 100%;\s*height: 100%;\s*\}/,
16
+ )
17
+ })
18
+ })
@@ -3,6 +3,7 @@ import { createPinia, setActivePinia } from 'pinia'
3
3
  import { AxiosError, type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
4
4
 
5
5
  import { AuthenticationRequiredError, useApi } from '../../composables/useApi'
6
+ import { MintApiError } from '../../apiErrors'
6
7
  import { useAuthStore } from '../../stores/auth'
7
8
 
8
9
  function readAuthorizationHeader(headers: InternalAxiosRequestConfig['headers']): unknown {
@@ -32,6 +33,7 @@ function unauthorizedAdapter(config: AxiosRequestConfig): Promise<never> {
32
33
  describe('useApi URL helpers', () => {
33
34
  beforeEach(() => {
34
35
  setActivePinia(createPinia())
36
+ useAuthStore().clearToken()
35
37
  })
36
38
 
37
39
  afterEach(() => {
@@ -98,25 +100,35 @@ describe('useApi URL helpers', () => {
98
100
  expect(authorization).toBeUndefined()
99
101
  })
100
102
 
101
- it('normalizes authenticated 401 responses and clears the rejected token', async () => {
103
+ it.each([false, true])('normalizes authenticated 401 responses and clears the rejected token (typedErrors=%s)', async (typedErrors) => {
102
104
  const authStore = useAuthStore()
103
105
  authStore.setToken('expired-token', 3600)
104
- const api = useApi({ baseUrl: '/api' })
106
+ const api = useApi({ baseUrl: '/api', typedErrors })
105
107
 
106
108
  const adapter = vi.fn(unauthorizedAdapter)
107
109
  const error = await api.get('/protected', { adapter }).catch((reason: unknown) => reason)
108
110
 
109
- expect({ attempts: adapter.mock.calls.length, errorType: error?.constructor, token: authStore.token }).toEqual({
111
+ expect({
112
+ attempts: adapter.mock.calls.length,
113
+ errorType: error?.constructor,
114
+ typed: error instanceof MintApiError,
115
+ status: (error as MintApiError).status,
116
+ code: (error as MintApiError).code,
117
+ token: authStore.token,
118
+ }).toEqual({
110
119
  attempts: 1,
111
120
  errorType: AuthenticationRequiredError,
121
+ typed: true,
122
+ status: 401,
123
+ code: 'AUTHENTICATION_REQUIRED',
112
124
  token: null,
113
125
  })
114
126
  })
115
127
 
116
- it('does not clear auth state for public requests that return 401', async () => {
128
+ it.each([false, true])('does not clear auth state for public requests that return 401 (typedErrors=%s)', async (typedErrors) => {
117
129
  const authStore = useAuthStore()
118
130
  authStore.setToken('still-valid-token', 3600)
119
- const api = useApi({ baseUrl: '/api', withAuth: false })
131
+ const api = useApi({ baseUrl: '/api', typedErrors, withAuth: false })
120
132
 
121
133
  const error = await api.get('/public', { adapter: unauthorizedAdapter }).catch((reason: unknown) => reason)
122
134
 
@@ -126,10 +138,10 @@ describe('useApi URL helpers', () => {
126
138
  })
127
139
  })
128
140
 
129
- it('does not clear a replacement token after an older request returns 401', async () => {
141
+ it.each([false, true])('does not clear a replacement token after an older request returns 401 (typedErrors=%s)', async (typedErrors) => {
130
142
  const authStore = useAuthStore()
131
143
  authStore.setToken('rejected-token', 3600)
132
- const api = useApi({ baseUrl: '/api' })
144
+ const api = useApi({ baseUrl: '/api', typedErrors })
133
145
 
134
146
  await api.get('/protected', {
135
147
  adapter: async (config) => {
@@ -141,9 +153,10 @@ describe('useApi URL helpers', () => {
141
153
  expect(authStore.token).toBe('replacement-token')
142
154
  })
143
155
 
144
- it('does not clear a token created while an unauthenticated request is in flight', async () => {
156
+ it.each([false, true])('does not clear a token created while an unauthenticated request is in flight (typedErrors=%s)', async (typedErrors) => {
145
157
  const authStore = useAuthStore()
146
- const api = useApi({ baseUrl: '/api' })
158
+ const api = useApi({ baseUrl: '/api', typedErrors })
159
+ expect(authStore.token).toBeNull()
147
160
 
148
161
  await api.get('/protected', {
149
162
  adapter: async (config) => {
@@ -154,4 +167,74 @@ describe('useApi URL helpers', () => {
154
167
 
155
168
  expect(authStore.token).toBe('new-login-token')
156
169
  })
170
+
171
+ it('normalizes a typed response when typed errors are enabled', async () => {
172
+ const api = useApi({ baseUrl: '/api', typedErrors: true })
173
+ const adapter = vi.fn(async (config: AxiosRequestConfig) => Promise.reject(new AxiosError(
174
+ 'Not Found',
175
+ AxiosError.ERR_BAD_REQUEST,
176
+ config as InternalAxiosRequestConfig,
177
+ undefined,
178
+ {
179
+ data: {
180
+ code: 'leaf.ARTIFACT_NOT_FOUND',
181
+ message: 'Artifact is not available',
182
+ status: 404,
183
+ request_id: 'req-generated',
184
+ details: { artifact_id: 67 },
185
+ detail: 'Artifact is not available',
186
+ },
187
+ status: 404,
188
+ statusText: 'Not Found',
189
+ headers: { 'x-request-id': 'req-generated' },
190
+ config: config as InternalAxiosRequestConfig,
191
+ },
192
+ )))
193
+
194
+ const error = await api.get('/artifacts/67', { adapter }).catch((reason: unknown) => reason)
195
+
196
+ const typedError = error as MintApiError
197
+ expect({ attempts: adapter.mock.calls.length, type: typedError.constructor, code: typedError.code }).toEqual({
198
+ attempts: 1,
199
+ type: MintApiError,
200
+ code: 'leaf.ARTIFACT_NOT_FOUND',
201
+ })
202
+ })
203
+
204
+ it.each([true, false])('normalizes Blob download errors (JSON=%s)', async (json) => {
205
+ const api = useApi({ typedErrors: true })
206
+ const body = { code: 'artifact.unavailable', message: 'No artifact', details: { id: 67 } }
207
+ const blob = new Blob([json ? JSON.stringify(body) : 'Bad gateway'], {
208
+ type: json ? 'application/json' : 'text/plain',
209
+ })
210
+ let cause: AxiosError | undefined
211
+ const error = await api.get('/download', {
212
+ responseType: 'blob',
213
+ adapter: async (config) => {
214
+ cause = new AxiosError('Bad gateway', AxiosError.ERR_BAD_RESPONSE, config, undefined, {
215
+ data: blob, status: 502, statusText: 'Bad gateway',
216
+ headers: { 'x-request-id': 'blob-request' }, config,
217
+ })
218
+ throw cause
219
+ },
220
+ }).catch((reason: unknown) => reason)
221
+
222
+ expect(error).toBeInstanceOf(MintApiError)
223
+ expect(error).toMatchObject({
224
+ code: json ? body.code : 'unknown',
225
+ message: json ? body.message : 'Bad gateway',
226
+ status: 502, requestId: 'blob-request', cause,
227
+ })
228
+ expect((error as MintApiError).body).toEqual(json ? body : blob)
229
+ })
230
+
231
+ it('leaves cancellations and network failures unchanged in typed mode', async () => {
232
+ const api = useApi({ typedErrors: true })
233
+ const cause = new AxiosError('Offline', AxiosError.ERR_NETWORK)
234
+ await expect(api.get('/offline', { adapter: async () => { throw cause } })).rejects.toBe(cause)
235
+ const controller = new AbortController()
236
+ controller.abort()
237
+ await expect(api.get('/cancelled', { signal: controller.signal })).rejects.toMatchObject({ code: 'ERR_CANCELED' })
238
+ })
239
+
157
240
  })
@@ -14,7 +14,7 @@ function mountHarness(options: UseDropdownStateOptions = {}) {
14
14
  state = useDropdownState(options)
15
15
  return state
16
16
  },
17
- template: '<div ref="rootRef" class="dropdown-root"><button class="inside">Inside</button></div>',
17
+ template: '<div ref="rootRef" class="dropdown-root"><button ref="triggerRef" class="trigger">Trigger</button><button class="inside">Inside</button></div>',
18
18
  }), { attachTo: document.body })
19
19
 
20
20
  if (!state) {
@@ -78,6 +78,63 @@ describe('useDropdownState', () => {
78
78
  wrapper.unmount()
79
79
  })
80
80
 
81
+ it('returns focus to the trigger only when asked', async () => {
82
+ const { wrapper, state } = mountHarness()
83
+ const trigger = wrapper.get<HTMLButtonElement>('.trigger').element
84
+ const inside = wrapper.get<HTMLButtonElement>('.inside').element
85
+
86
+ state.open()
87
+ inside.focus()
88
+ state.close()
89
+ await nextTick()
90
+ expect(document.activeElement).toBe(inside)
91
+
92
+ state.open()
93
+ inside.focus()
94
+ state.close(true)
95
+ await nextTick()
96
+ expect(document.activeElement).toBe(trigger)
97
+
98
+ wrapper.unmount()
99
+ })
100
+
101
+ it('returns focus to the trigger when Escape closes the dropdown', async () => {
102
+ const { wrapper, state } = mountHarness()
103
+ const trigger = wrapper.get<HTMLButtonElement>('.trigger').element
104
+
105
+ state.open()
106
+ wrapper.get<HTMLButtonElement>('.inside').element.focus()
107
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
108
+ await nextTick()
109
+ await nextTick()
110
+
111
+ expect(state.isOpen.value).toBe(false)
112
+ expect(document.activeElement).toBe(trigger)
113
+
114
+ wrapper.unmount()
115
+ })
116
+
117
+ it('closes on scroll and resize only when closeOnScroll is set', async () => {
118
+ const plain = mountHarness()
119
+ plain.state.open()
120
+ window.dispatchEvent(new Event('scroll'))
121
+ await nextTick()
122
+ expect(plain.state.isOpen.value).toBe(true)
123
+ plain.wrapper.unmount()
124
+
125
+ const scrolling = mountHarness({ closeOnScroll: true })
126
+ scrolling.state.open()
127
+ window.dispatchEvent(new Event('scroll'))
128
+ await nextTick()
129
+ expect(scrolling.state.isOpen.value).toBe(false)
130
+
131
+ scrolling.state.open()
132
+ window.dispatchEvent(new Event('resize'))
133
+ await nextTick()
134
+ expect(scrolling.state.isOpen.value).toBe(false)
135
+ scrolling.wrapper.unmount()
136
+ })
137
+
81
138
  it('can opt out of outside-click and Escape closing', async () => {
82
139
  const { wrapper, state } = mountHarness({
83
140
  closeOnOutsideClick: false,