flexdesk 0.17.8 → 0.17.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 (281) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +0 -0
  3. package/README.md +0 -0
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/canvas.js +0 -0
  6. package/charts.js +0 -0
  7. package/core.js +0 -0
  8. package/css/base.css +21 -1
  9. package/css/flexdesk.css +21 -1
  10. package/css/overrides.css +0 -0
  11. package/css/reset.css +0 -0
  12. package/dist/canvas.js +0 -0
  13. package/dist/canvas.js.map +0 -0
  14. package/dist/charts.js +7 -6
  15. package/dist/charts.js.map +2 -2
  16. package/dist/{chunk-EIYNF4BD.js → chunk-2YTM3GJE.js} +2 -2
  17. package/dist/chunk-2YTM3GJE.js.map +7 -0
  18. package/dist/chunk-3PHCPZHT.js +0 -0
  19. package/dist/chunk-3PHCPZHT.js.map +0 -0
  20. package/dist/{chunk-UURZ37SD.js → chunk-7U4IB65H.js} +27 -9
  21. package/dist/chunk-7U4IB65H.js.map +7 -0
  22. package/dist/{chunk-D3COLZZ2.js → chunk-BE2PU3LL.js} +2 -2
  23. package/dist/{chunk-D3COLZZ2.js.map → chunk-BE2PU3LL.js.map} +0 -0
  24. package/dist/chunk-CVMCZZX2.js +0 -0
  25. package/dist/chunk-CVMCZZX2.js.map +0 -0
  26. package/dist/{chunk-UWO43BZU.js → chunk-F35XSQWA.js} +16 -20
  27. package/dist/chunk-F35XSQWA.js.map +7 -0
  28. package/dist/chunk-FOOS3T5L.js +0 -0
  29. package/dist/chunk-FOOS3T5L.js.map +0 -0
  30. package/dist/chunk-GOQLR5WR.js +0 -0
  31. package/dist/chunk-GOQLR5WR.js.map +0 -0
  32. package/dist/chunk-HNU6MM2B.js +0 -0
  33. package/dist/chunk-HNU6MM2B.js.map +0 -0
  34. package/dist/chunk-JKZLNKG6.js +0 -0
  35. package/dist/chunk-JKZLNKG6.js.map +0 -0
  36. package/dist/chunk-JYWURG5T.js +0 -0
  37. package/dist/chunk-JYWURG5T.js.map +0 -0
  38. package/dist/{chunk-RKNFBX5K.js → chunk-MIW7D6WC.js} +16 -4
  39. package/dist/{chunk-RKNFBX5K.js.map → chunk-MIW7D6WC.js.map} +2 -2
  40. package/dist/chunk-N7HC3ADV.js +0 -0
  41. package/dist/chunk-N7HC3ADV.js.map +0 -0
  42. package/dist/{chunk-LWV23YVE.js → chunk-NIJEJAZ7.js} +42 -18
  43. package/dist/chunk-NIJEJAZ7.js.map +7 -0
  44. package/dist/{chunk-HYJYYEPK.js → chunk-REJ5HLIM.js} +2 -2
  45. package/dist/{chunk-HYJYYEPK.js.map → chunk-REJ5HLIM.js.map} +0 -0
  46. package/dist/{chunk-WVFGV5FT.js → chunk-WUFLGETN.js} +39 -2
  47. package/dist/{chunk-WVFGV5FT.js.map → chunk-WUFLGETN.js.map} +3 -3
  48. package/dist/core.js +0 -0
  49. package/dist/core.js.map +0 -0
  50. package/dist/editor.js +2 -2
  51. package/dist/editor.js.map +0 -0
  52. package/dist/flexdesk.css +21 -1
  53. package/dist/flow.js +3 -3
  54. package/dist/flow.js.map +0 -0
  55. package/dist/host.js +0 -0
  56. package/dist/host.js.map +0 -0
  57. package/dist/reset.css +0 -0
  58. package/dist/tile_registry-6WZPTUZV.js +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  60. package/dist/tiles.js +63 -20
  61. package/dist/tiles.js.map +2 -2
  62. package/dist/widgets.js +6 -6
  63. package/dist/widgets.js.map +0 -0
  64. package/dist/wm.js +5 -5
  65. package/dist/wm.js.map +0 -0
  66. package/docs/README.md +0 -0
  67. package/docs/best-practices.md +0 -0
  68. package/docs/components/charts.md +29 -0
  69. package/docs/components/command-palette.md +0 -0
  70. package/docs/components/conversation.md +0 -0
  71. package/docs/components/data-table-grid.md +0 -0
  72. package/docs/components/data-table.md +0 -0
  73. package/docs/components/editor.md +0 -0
  74. package/docs/components/flow-canvas-editor.md +0 -0
  75. package/docs/components/flow-editors.md +0 -0
  76. package/docs/components/flow-lane-editor.md +0 -0
  77. package/docs/components/flow-outline-editor.md +0 -0
  78. package/docs/components/forms-and-dialogs.md +0 -0
  79. package/docs/components/input-fields.md +0 -0
  80. package/docs/components/keymap.md +0 -0
  81. package/docs/components/markdown-and-outline.md +0 -0
  82. package/docs/components/menus.md +41 -0
  83. package/docs/components/notifications.md +0 -0
  84. package/docs/components/panels-and-desktops.md +0 -0
  85. package/docs/components/record-list-and-fact-table.md +0 -0
  86. package/docs/components/settings-view.md +0 -0
  87. package/docs/components/shell.md +0 -0
  88. package/docs/components/slide-out-panel.md +0 -0
  89. package/docs/components/tiles.md +25 -0
  90. package/docs/components/wizard.md +0 -0
  91. package/docs/concepts/building-and-packaging.md +0 -0
  92. package/docs/concepts/domain-neutrality.md +0 -0
  93. package/docs/concepts/host-port.md +0 -0
  94. package/docs/concepts/packages-and-entry-points.md +0 -0
  95. package/docs/concepts/peer-dependencies.md +0 -0
  96. package/docs/concepts/state-guard.md +0 -0
  97. package/docs/concepts/styling.md +0 -0
  98. package/docs/quickstart.md +0 -0
  99. package/editor.js +0 -0
  100. package/flow.js +0 -0
  101. package/host.js +0 -0
  102. package/package.json +1 -1
  103. package/src/canvas/canvas_adapter.js +0 -0
  104. package/src/canvas/edge_router.js +0 -0
  105. package/src/canvas/graph/connector_router.js +0 -0
  106. package/src/canvas/graph/history_service.js +0 -0
  107. package/src/canvas/graph/node_platform.js +0 -0
  108. package/src/canvas/index.js +0 -0
  109. package/src/canvas/layered_layout.js +0 -0
  110. package/src/charts/chart_types.js +3 -2
  111. package/src/charts/data_series_plot_window.js +0 -0
  112. package/src/charts/downsample.js +0 -0
  113. package/src/charts/plot_config.js +0 -0
  114. package/src/charts/plot_config_editor.js +8 -2
  115. package/src/charts/plotly_wrapper.js +0 -0
  116. package/src/core/event_bus.js +0 -0
  117. package/src/core/logging.js +0 -0
  118. package/src/core/settings.js +0 -0
  119. package/src/core/state_guard.js +0 -0
  120. package/src/core/state_guard_config.js +0 -0
  121. package/src/core/state_machine.js +0 -0
  122. package/src/editor/editor_pane.js +0 -0
  123. package/src/editor/monaco_editor_factory.js +0 -0
  124. package/src/editor/monaco_loader.js +0 -0
  125. package/src/editor/notebook_search.js +0 -0
  126. package/src/editor/notebook_tab_bar.js +26 -7
  127. package/src/editor/notebook_undo_manager.js +0 -0
  128. package/src/editor/split_pane_container.js +0 -0
  129. package/src/flow/canvas_editor/connections.js +0 -0
  130. package/src/flow/canvas_editor/editor.js +0 -0
  131. package/src/flow/canvas_editor/index.js +0 -0
  132. package/src/flow/canvas_editor/layout.js +0 -0
  133. package/src/flow/canvas_editor/strings.js +0 -0
  134. package/src/flow/kit/always_before.js +0 -0
  135. package/src/flow/kit/catalogue.js +0 -0
  136. package/src/flow/kit/chip_input.js +0 -0
  137. package/src/flow/kit/dom.js +0 -0
  138. package/src/flow/kit/findings.js +0 -0
  139. package/src/flow/kit/history.js +0 -0
  140. package/src/flow/kit/index.js +0 -0
  141. package/src/flow/kit/keys.js +0 -0
  142. package/src/flow/kit/logic_graph.js +0 -0
  143. package/src/flow/kit/references.js +0 -0
  144. package/src/flow/kit/settings_panel.js +0 -0
  145. package/src/flow/kit/settings_schema.js +0 -0
  146. package/src/flow/kit/step_picker.js +0 -0
  147. package/src/flow/kit/strings.js +0 -0
  148. package/src/flow/kit/value_picker.js +0 -0
  149. package/src/flow/kit/widgets.js +0 -0
  150. package/src/flow/lanes/editor.js +0 -0
  151. package/src/flow/lanes/index.js +0 -0
  152. package/src/flow/lanes/layout.js +0 -0
  153. package/src/flow/lanes/pipeline.js +0 -0
  154. package/src/flow/lanes/preview.js +0 -0
  155. package/src/flow/lanes/strings.js +0 -0
  156. package/src/flow/outline/editor.js +0 -0
  157. package/src/flow/outline/index.js +0 -0
  158. package/src/flow/outline/mapping.js +0 -0
  159. package/src/flow/outline/menu.js +0 -0
  160. package/src/flow/outline/operations.js +0 -0
  161. package/src/flow/outline/recognise.js +0 -0
  162. package/src/flow/outline/strings.js +0 -0
  163. package/src/help/help_modal.js +0 -0
  164. package/src/help/help_registry.js +0 -0
  165. package/src/host/external_links.js +0 -0
  166. package/src/host/file_picker.js +0 -0
  167. package/src/host/host.js +0 -0
  168. package/src/host/photino_host.js +0 -0
  169. package/src/host/photino_host.md +0 -0
  170. package/src/host/pywebview_host.js +0 -0
  171. package/src/tiles/chart_tiles.js +0 -0
  172. package/src/tiles/config_schema.js +0 -0
  173. package/src/tiles/figure_tile.js +0 -0
  174. package/src/tiles/layout_persistence.js +0 -0
  175. package/src/tiles/list_tiles.js +0 -0
  176. package/src/tiles/scope_picker.js +0 -0
  177. package/src/tiles/tile_base.js +67 -16
  178. package/src/tiles/tile_config_form.js +0 -0
  179. package/src/tiles/tile_grid.js +0 -0
  180. package/src/tiles/tile_registry.js +0 -0
  181. package/src/tiling/bottom_panel_tabs.js +0 -0
  182. package/src/tiling/command_palette.js +0 -0
  183. package/src/tiling/content_registry.js +0 -0
  184. package/src/tiling/desktops.js +0 -0
  185. package/src/tiling/entity_sources.js +0 -0
  186. package/src/tiling/keymap.js +0 -0
  187. package/src/tiling/kind_taxonomy.js +0 -0
  188. package/src/tiling/landing_table.js +0 -0
  189. package/src/tiling/loading_overlay.js +0 -0
  190. package/src/tiling/nav_panel.js +0 -0
  191. package/src/tiling/page_factory.js +0 -0
  192. package/src/tiling/panel_availability.js +0 -0
  193. package/src/tiling/panel_keys.js +0 -0
  194. package/src/tiling/shell.js +0 -0
  195. package/src/tiling/tab_strip.js +0 -0
  196. package/src/tiling/tile_breadcrumb.js +0 -0
  197. package/src/tiling/tile_renderer.js +0 -0
  198. package/src/tiling/tile_tab_menu.js +0 -0
  199. package/src/tiling/tile_tree.js +0 -0
  200. package/src/tiling/wm.js +0 -0
  201. package/src/tiling/zoom.js +0 -0
  202. package/src/ui/base/component_base.js +0 -0
  203. package/src/ui/base/controller_base.js +0 -0
  204. package/src/ui/base/page_base.js +0 -0
  205. package/src/ui/components/about_dialog.js +0 -0
  206. package/src/ui/components/action_dropdown.js +19 -6
  207. package/src/ui/components/alert_log.js +0 -0
  208. package/src/ui/components/alerts_pane.js +0 -0
  209. package/src/ui/components/attribute_list_editor.js +0 -0
  210. package/src/ui/components/autocomplete_field.js +0 -0
  211. package/src/ui/components/cell_editor_host.js +0 -0
  212. package/src/ui/components/cell_editors.js +0 -0
  213. package/src/ui/components/cell_grid.js +0 -0
  214. package/src/ui/components/cell_paste.js +0 -0
  215. package/src/ui/components/cell_selection.js +0 -0
  216. package/src/ui/components/choice_controls.js +0 -0
  217. package/src/ui/components/color_field.js +0 -0
  218. package/src/ui/components/color_list_field.js +0 -0
  219. package/src/ui/components/column_fit.js +0 -0
  220. package/src/ui/components/column_groups.js +0 -0
  221. package/src/ui/components/column_order.js +0 -0
  222. package/src/ui/components/computing_status_window.js +0 -0
  223. package/src/ui/components/confirm_dialog.js +0 -0
  224. package/src/ui/components/context_menu.js +20 -20
  225. package/src/ui/components/conversation_view.js +0 -0
  226. package/src/ui/components/crosstab.js +0 -0
  227. package/src/ui/components/data_table.js +45 -19
  228. package/src/ui/components/detail_header.js +0 -0
  229. package/src/ui/components/document_outline.js +0 -0
  230. package/src/ui/components/drag_reorder.js +0 -0
  231. package/src/ui/components/fact_table.js +0 -0
  232. package/src/ui/components/fill_handle.js +0 -0
  233. package/src/ui/components/gallery_picker.js +0 -0
  234. package/src/ui/components/header_actions.js +0 -0
  235. package/src/ui/components/inline_renamer.js +0 -0
  236. package/src/ui/components/job_progress.js +0 -0
  237. package/src/ui/components/managed_window.js +0 -0
  238. package/src/ui/components/menu_bar.js +0 -0
  239. package/src/ui/components/modal.js +0 -0
  240. package/src/ui/components/notification_history.js +0 -0
  241. package/src/ui/components/progress.js +0 -0
  242. package/src/ui/components/record_list.js +0 -0
  243. package/src/ui/components/row_activation.js +0 -0
  244. package/src/ui/components/section_stack.js +0 -0
  245. package/src/ui/components/series.js +0 -0
  246. package/src/ui/components/settings_view.js +0 -0
  247. package/src/ui/components/slide_out_panel.js +0 -0
  248. package/src/ui/components/slider_field.js +0 -0
  249. package/src/ui/components/sortable_list.js +0 -0
  250. package/src/ui/components/staged_edits.js +0 -0
  251. package/src/ui/components/staged_history.js +0 -0
  252. package/src/ui/components/table_filter_chips.js +0 -0
  253. package/src/ui/components/table_state_store.js +0 -0
  254. package/src/ui/components/table_structure.js +0 -0
  255. package/src/ui/components/table_values.js +0 -0
  256. package/src/ui/components/tabs.js +0 -0
  257. package/src/ui/components/tag_input.js +0 -0
  258. package/src/ui/components/toast.js +0 -0
  259. package/src/ui/components/toggle_field.js +0 -0
  260. package/src/ui/components/tree_view.js +0 -0
  261. package/src/ui/components/tsv.js +0 -0
  262. package/src/ui/components/window_taskbar.js +0 -0
  263. package/src/ui/components/wizard.js +0 -0
  264. package/src/ui/controllers/window_chrome_controller.js +0 -0
  265. package/src/ui/notification_center.js +0 -0
  266. package/src/ui/utils/auto_scrollbars.js +0 -0
  267. package/src/ui/utils/markdown.js +0 -0
  268. package/src/ui/utils/menu_viewport.js +81 -0
  269. package/src/ui/utils/native_context_menu.js +0 -0
  270. package/src/ui/utils/overlay_scrollbar.js +0 -0
  271. package/src/ui/utils/raf_resize_observer.js +0 -0
  272. package/src/ui/utils/scroll_lock.js +0 -0
  273. package/src/ui/utils/strip_scroll.js +0 -0
  274. package/src/ui/utils/tooltip_service.js +0 -0
  275. package/tiles.js +0 -0
  276. package/widgets.js +0 -0
  277. package/wm.js +0 -0
  278. package/dist/chunk-EIYNF4BD.js.map +0 -7
  279. package/dist/chunk-LWV23YVE.js.map +0 -7
  280. package/dist/chunk-UURZ37SD.js.map +0 -7
  281. package/dist/chunk-UWO43BZU.js.map +0 -7
@@ -8,6 +8,8 @@
8
8
 
9
9
  import { openRawTracesWindow } from '../charts/plot_popout_window.js';
10
10
  import { formatVariableLabel } from './config_schema.js';
11
+ import { ActionDropdown } from '../ui/components/action_dropdown.js';
12
+ import { modalHost } from '../ui/components/modal.js';
11
13
  // The host is INJECTED, not fetched from a global.
12
14
  //
13
15
  // This imported `appHost()` from ui/js/ecoagent/app_host.js — a tile reaching into
@@ -403,6 +405,12 @@ export class TileBase {
403
405
  if (this._disposed) return;
404
406
  this._disposed = true;
405
407
 
408
+ // An open ⋯ menu lives on document.body; take it back first.
409
+ this._closeTileMenu();
410
+ this._menuButton = null;
411
+ this._menuDropdown = null;
412
+ this._menuHome = null;
413
+
406
414
  // Close expand window if open
407
415
  if (this._expandWindow?.isVisible) {
408
416
  this._expandWindow.close();
@@ -485,6 +493,13 @@ export class TileBase {
485
493
  * @private
486
494
  */
487
495
  _buildChrome() {
496
+ // A rebuilt header gets a new ⋯ menu; an open old one must not stay
497
+ // behind on document.body.
498
+ this._closeTileMenu();
499
+ this._menuButton = null;
500
+ this._menuDropdown = null;
501
+ this._menuHome = null;
502
+
488
503
  // Header with drag handle and controls
489
504
  const header = document.createElement('div');
490
505
  header.className = 'tile-header';
@@ -584,23 +599,13 @@ export class TileBase {
584
599
  const menuBtn = header.querySelector('.tile-menu-btn');
585
600
  const menuDropdown = header.querySelector('.tile-menu-dropdown');
586
601
  if (menuBtn && menuDropdown) {
602
+ this._menuButton = menuBtn;
603
+ this._menuDropdown = menuDropdown;
604
+ this._menuHome = menuDropdown.parentNode;
587
605
  menuBtn.addEventListener('click', (e) => {
588
606
  e.stopPropagation();
589
- const isOpen = !menuDropdown.hidden;
590
- menuDropdown.hidden = !menuDropdown.hidden;
591
- if (!isOpen) {
592
- // Close on outside click
593
- const closeMenu = (ev) => {
594
- if (!menuDropdown.contains(ev.target) && ev.target !== menuBtn) {
595
- menuDropdown.hidden = true;
596
- document.removeEventListener('pointerdown', closeMenu, true);
597
- }
598
- };
599
- // Delay to avoid immediate close from the same click
600
- requestAnimationFrame(() => {
601
- document.addEventListener('pointerdown', closeMenu, true);
602
- });
603
- }
607
+ if (menuDropdown.hidden) this._openTileMenu();
608
+ else this._closeTileMenu();
604
609
  });
605
610
  // One listener per rendered entry, dispatching by `data-action`
606
611
  // into `_onMenuAction`. Wiring each action by name here is what
@@ -609,7 +614,7 @@ export class TileBase {
609
614
  menuDropdown.querySelectorAll('.tile-menu-item').forEach((itemEl) => {
610
615
  itemEl.addEventListener('click', (e) => {
611
616
  e.stopPropagation();
612
- menuDropdown.hidden = true;
617
+ this._closeTileMenu();
613
618
  this._onMenuAction(itemEl.dataset.action);
614
619
  });
615
620
  });
@@ -654,6 +659,52 @@ export class TileBase {
654
659
  }
655
660
  }
656
661
 
662
+ /**
663
+ * Open the ⋯ menu. It is drawn on `document.body` (or the open modal's
664
+ * host, as `showContextMenu` is), not inside the tile: a tile clips its
665
+ * content, so a menu wider than a narrow tile lost its left part. It is
666
+ * placed with `ActionDropdown.position` — fixed, under the button (above
667
+ * when there is no room), right edges aligned, kept inside the window.
668
+ * @private
669
+ */
670
+ _openTileMenu() {
671
+ const btn = this._menuButton;
672
+ const menu = this._menuDropdown;
673
+ if (!btn || !menu || !menu.hidden) return;
674
+ (modalHost() || document.body).appendChild(menu);
675
+ menu.hidden = false;
676
+ ActionDropdown.position(btn, menu, { align: 'end' });
677
+ // Close on a press outside the menu and its button. Added a frame
678
+ // later so the press that opened it does not close it.
679
+ this._menuOutsideListener = (ev) => {
680
+ if (!menu.contains(ev.target) && !btn.contains(ev.target)) this._closeTileMenu();
681
+ };
682
+ const listener = this._menuOutsideListener;
683
+ requestAnimationFrame(() => {
684
+ if (this._menuOutsideListener === listener) {
685
+ document.addEventListener('pointerdown', listener, true);
686
+ }
687
+ });
688
+ }
689
+
690
+ /**
691
+ * Close the ⋯ menu and put it back in the tile's header.
692
+ * @private
693
+ */
694
+ _closeTileMenu() {
695
+ const menu = this._menuDropdown;
696
+ if (this._menuOutsideListener) {
697
+ document.removeEventListener('pointerdown', this._menuOutsideListener, true);
698
+ this._menuOutsideListener = null;
699
+ }
700
+ if (!menu) return;
701
+ menu.hidden = true;
702
+ for (const prop of ['position', 'top', 'bottom', 'left', 'right', 'maxHeight', 'overflowY']) {
703
+ menu.style[prop] = '';
704
+ }
705
+ if (this._menuHome && menu.parentNode !== this._menuHome) this._menuHome.appendChild(menu);
706
+ }
707
+
657
708
  /**
658
709
  * Handle config button click — highlight tile and emit config request.
659
710
  * @private
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/tiling/wm.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -379,8 +379,12 @@ export class ActionDropdown {
379
379
  *
380
380
  * @param {HTMLElement} trigger - The element the dropdown opens from
381
381
  * @param {HTMLElement} menu - The dropdown menu element to position
382
+ * @param {Object} [opts]
383
+ * @param {'start'|'end'} [opts.align='start'] - Which trigger edge the menu
384
+ * lines up with first: `'start'` its left edge, `'end'` its right edge.
385
+ * Either flips to the other edge when it would leave the window.
382
386
  */
383
- static position(trigger, menu) {
387
+ static position(trigger, menu, { align = 'start' } = {}) {
384
388
  if (!trigger || !menu) return;
385
389
 
386
390
  // Reset any prior inline position so natural size can be measured.
@@ -424,12 +428,21 @@ export class ActionDropdown {
424
428
  top = EDGE_MARGIN;
425
429
  }
426
430
 
427
- // ── Horizontal: align left-edge with trigger, flip if off-screen ──
428
- let left = triggerRect.left;
429
-
430
- if (left + menuRect.width > vw - EDGE_MARGIN) {
431
- // Align right edges instead.
431
+ // ── Horizontal: align with the trigger's start (left) or end (right)
432
+ // edge, flip to the other edge if off-screen ──
433
+ let left;
434
+ if (align === 'end') {
432
435
  left = triggerRect.right - menuRect.width;
436
+ if (left < EDGE_MARGIN) {
437
+ // Align left edges instead.
438
+ left = triggerRect.left;
439
+ }
440
+ } else {
441
+ left = triggerRect.left;
442
+ if (left + menuRect.width > vw - EDGE_MARGIN) {
443
+ // Align right edges instead.
444
+ left = triggerRect.right - menuRect.width;
445
+ }
433
446
  }
434
447
 
435
448
  // Still off-screen? Clamp to edges.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -24,9 +24,16 @@
24
24
  * Disabled items are skipped by the arrows rather than focusable-but-inert.
25
25
  * `<button>` rather than a div with `tabindex`: the browser then gives Enter and
26
26
  * Space for free and screen readers announce it without further help.
27
+ *
28
+ * ── It stays inside the window (BUG-0040) ─────────────────────────────
29
+ * The top was clamped at 0 but nothing capped the height, so a long menu ran
30
+ * off the bottom and could not scroll. `placeMenuInViewport` caps it at the
31
+ * window's height less a margin; it scrolls inside, the arrows scroll the
32
+ * focused item into view, and scrolling the menu itself does not close it.
27
33
  */
28
34
 
29
35
  import { modalHost } from './modal.js';
36
+ import { placeMenuInViewport, moveMenuFocus, focusMenuItem } from '../utils/menu_viewport.js';
30
37
 
31
38
  let _activeMenu = null;
32
39
  let _returnFocusTo = null;
@@ -92,23 +99,20 @@ export function showContextMenu(x, y, items, onAction) {
92
99
  menu.style.display = 'block';
93
100
  _activeMenu = menu;
94
101
 
95
- // Position with viewport clamping.
96
- const rect = menu.getBoundingClientRect();
97
- const left = Math.min(x, window.innerWidth - rect.width - 8);
98
- const top = Math.min(y, window.innerHeight - rect.height - 8);
99
- menu.style.left = `${Math.max(0, left)}px`;
100
- menu.style.top = `${Math.max(0, top)}px`;
102
+ // Position inside the window, scrolling when taller than it (BUG-0040).
103
+ placeMenuInViewport(menu, x, y);
101
104
 
102
- // Close on outside click / scroll / Escape.
105
+ // Close on outside click / scroll / Escape. A scroll INSIDE the menu is
106
+ // the menu being read, not the page moving under it.
103
107
  setTimeout(() => {
104
108
  document.addEventListener('mousedown', _outsideHandler, { once: true, capture: true });
105
109
  }, 0);
106
110
  document.addEventListener('keydown', _keyHandler);
107
- window.addEventListener('scroll', hideContextMenu, { once: true, capture: true });
111
+ window.addEventListener('scroll', _scrollHandler, { capture: true });
108
112
 
109
113
  // Focus the first item the keyboard can actually use. `preventScroll` so a
110
114
  // menu opened near the bottom of a long page does not jump it.
111
- _enabledItems(menu)[0]?.focus({ preventScroll: true });
115
+ focusMenuItem(_enabledItems(menu)[0]);
112
116
  }
113
117
 
114
118
  export function hideContextMenu() {
@@ -119,6 +123,7 @@ export function hideContextMenu() {
119
123
  _activeMenu = null;
120
124
  _returnFocusTo = null;
121
125
  document.removeEventListener('keydown', _keyHandler);
126
+ window.removeEventListener('scroll', _scrollHandler, { capture: true });
122
127
  // Only take focus back if the menu still had it. A click elsewhere has
123
128
  // already moved focus deliberately and must not be undone.
124
129
  if (held && returnTo?.isConnected) returnTo.focus?.({ preventScroll: true });
@@ -134,17 +139,12 @@ function _enabledItems(menu) {
134
139
  function _keyHandler(e) {
135
140
  if (!_activeMenu) return;
136
141
  if (e.key === 'Escape') { e.preventDefault(); hideContextMenu(); return; }
137
- const items = _enabledItems(_activeMenu);
138
- if (items.length === 0) return;
139
- const at = items.indexOf(document.activeElement);
140
- let next = null;
141
- if (e.key === 'ArrowDown') next = items[(at + 1 + items.length) % items.length];
142
- else if (e.key === 'ArrowUp') next = items[(at - 1 + items.length) % items.length];
143
- else if (e.key === 'Home') next = items[0];
144
- else if (e.key === 'End') next = items[items.length - 1];
145
- if (!next) return;
146
- e.preventDefault();
147
- next.focus({ preventScroll: true });
142
+ moveMenuFocus(e, _enabledItems(_activeMenu));
143
+ }
144
+
145
+ function _scrollHandler(e) {
146
+ if (_activeMenu && e.target?.nodeType && _activeMenu.contains(e.target)) return;
147
+ hideContextMenu();
148
148
  }
149
149
 
150
150
  function _outsideHandler(e) {
File without changes
File without changes
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { createRafResizeObserver } from '../utils/raf_resize_observer.js';
23
+ import { placeMenuInViewport, moveMenuFocus, focusMenuItem } from '../utils/menu_viewport.js';
23
24
  import {
24
25
  ISO_DATE, ISO_DATETIME, formatDate, formatDuration, matchDateFilter,
25
26
  matchDurationFilter, parseDateValue, parseDuration,
@@ -764,6 +765,8 @@ export class DataTable {
764
765
  // STORY-0010. The right-click the custom items on show were built for,
765
766
  // read again when one is chosen.
766
767
  this._contextMenuCtx = null;
768
+ // BUG-0040. What had focus when the menu opened, given it back on close.
769
+ this._contextMenuReturnFocus = null;
767
770
  // STORY-0010 `keyboardNav`: the row the arrows last moved to.
768
771
  this._navCursor = null;
769
772
  this._filterDropdownEl = null;
@@ -7281,6 +7284,7 @@ export class DataTable {
7281
7284
 
7282
7285
  const menu = document.createElement('div');
7283
7286
  menu.className = 'twm-context-menu data-context-menu';
7287
+ menu.setAttribute('role', 'menu');
7284
7288
  menu.style.display = 'none';
7285
7289
  menu.style.position = 'fixed';
7286
7290
  menu.style.zIndex = '10001';
@@ -7338,19 +7342,31 @@ export class DataTable {
7338
7342
  if (event?.target && menu?.contains(event.target)) return;
7339
7343
  this._hideContextMenu();
7340
7344
  };
7341
- const hideOnEscape = (event) => {
7342
- if (event.key === 'Escape') this._hideContextMenu();
7345
+ // BUG-0040. The keyboard reaches every item: Up/Down/Home/End move
7346
+ // focus (and scroll the item into view in a menu taller than the
7347
+ // window), Enter and Space choose, Escape closes.
7348
+ const onMenuKey = (event) => {
7349
+ if (event.key === 'Escape') { this._hideContextMenu(); return; }
7350
+ if (!menu || menu.style.display === 'none') return;
7351
+ const focused = document.activeElement;
7352
+ if ((event.key === 'Enter' || event.key === ' ')
7353
+ && focused?.classList?.contains('twm-context-menu-item') && menu.contains(focused)) {
7354
+ event.preventDefault();
7355
+ focused.click();
7356
+ return;
7357
+ }
7358
+ moveMenuFocus(event, this._contextMenuEnabledItems());
7343
7359
  };
7344
7360
  const hideOnResize = () => this._hideContextMenu();
7345
7361
 
7346
7362
  document.addEventListener('click', hideOnGlobal, true);
7347
7363
  document.addEventListener('scroll', hideOnGlobal, true);
7348
7364
  window.addEventListener('resize', hideOnResize);
7349
- document.addEventListener('keydown', hideOnEscape);
7365
+ document.addEventListener('keydown', onMenuKey);
7350
7366
 
7351
7367
  this._contextMenuGlobals.push(() => document.removeEventListener('click', hideOnGlobal, true));
7352
7368
  this._contextMenuGlobals.push(() => document.removeEventListener('scroll', hideOnGlobal, true));
7353
- this._contextMenuGlobals.push(() => document.removeEventListener('keydown', hideOnEscape));
7369
+ this._contextMenuGlobals.push(() => document.removeEventListener('keydown', onMenuKey));
7354
7370
  this._contextMenuGlobals.push(() => window.removeEventListener('resize', hideOnResize));
7355
7371
  }
7356
7372
 
@@ -7362,10 +7378,23 @@ export class DataTable {
7362
7378
  }
7363
7379
 
7364
7380
  _hideContextMenu() {
7365
- if (this._contextMenuEl) {
7366
- this._contextMenuEl.style.display = 'none';
7381
+ const menu = this._contextMenuEl;
7382
+ const returnTo = this._contextMenuReturnFocus;
7383
+ this._contextMenuReturnFocus = null;
7384
+ const held = !!menu && menu.style.display !== 'none' && menu.contains(document.activeElement);
7385
+ if (menu) {
7386
+ menu.style.display = 'none';
7367
7387
  }
7368
7388
  this._disarmContextMenuGlobals();
7389
+ // BUG-0040. Focus goes back where it was, but only if the menu still
7390
+ // had it: a click elsewhere has moved it deliberately.
7391
+ if (held && returnTo?.isConnected) returnTo.focus?.({ preventScroll: true });
7392
+ }
7393
+
7394
+ /** BUG-0040. The items the keyboard can land on. */
7395
+ _contextMenuEnabledItems() {
7396
+ const menu = this._contextMenuEl;
7397
+ return menu ? [...menu.querySelectorAll('.twm-context-menu-item:not(.disabled)')] : [];
7369
7398
  }
7370
7399
 
7371
7400
  _showContextMenu(clientX, clientY, custom = null) {
@@ -7381,20 +7410,17 @@ export class DataTable {
7381
7410
  item.style.pointerEvents = hasSelection ? '' : 'none';
7382
7411
  });
7383
7412
 
7384
- menu.style.display = 'block';
7385
- const rect = menu.getBoundingClientRect();
7386
- let left = clientX;
7387
- let top = clientY;
7388
-
7389
- if (left + rect.width > window.innerWidth) {
7390
- left = window.innerWidth - rect.width - 8;
7391
- }
7392
- if (top + rect.height > window.innerHeight) {
7393
- top = window.innerHeight - rect.height - 8;
7394
- }
7413
+ // BUG-0040. Every item is a menu item the keyboard can focus.
7414
+ menu.querySelectorAll('.twm-context-menu-item').forEach((item) => {
7415
+ item.tabIndex = -1;
7416
+ if (!item.hasAttribute('role')) item.setAttribute('role', 'menuitem');
7417
+ });
7418
+ if (!menu.contains(document.activeElement)) this._contextMenuReturnFocus = document.activeElement;
7395
7419
 
7396
- menu.style.left = `${left}px`;
7397
- menu.style.top = `${top}px`;
7420
+ // BUG-0040. Inside the window, at most its height, scrolling when longer.
7421
+ menu.style.display = 'block';
7422
+ placeMenuInViewport(menu, clientX, clientY);
7423
+ focusMenuItem(this._contextMenuEnabledItems()[0]);
7398
7424
  }
7399
7425
 
7400
7426
  /**
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes