flexdesk 0.17.7 → 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 (284) hide show
  1. package/CHANGELOG.md +43 -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 +30 -3
  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-RMTRWWEO.js → chunk-2YTM3GJE.js} +67 -26
  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 +30 -3
  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 +95 -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/plot_grid.js +16 -4
  116. package/src/charts/plot_popout_window.js +2 -1
  117. package/src/charts/plotly_wrapper.js +0 -0
  118. package/src/charts/series_transforms.js +114 -20
  119. package/src/core/event_bus.js +0 -0
  120. package/src/core/logging.js +0 -0
  121. package/src/core/settings.js +0 -0
  122. package/src/core/state_guard.js +0 -0
  123. package/src/core/state_guard_config.js +0 -0
  124. package/src/core/state_machine.js +0 -0
  125. package/src/editor/editor_pane.js +0 -0
  126. package/src/editor/monaco_editor_factory.js +0 -0
  127. package/src/editor/monaco_loader.js +0 -0
  128. package/src/editor/notebook_search.js +0 -0
  129. package/src/editor/notebook_tab_bar.js +26 -7
  130. package/src/editor/notebook_undo_manager.js +0 -0
  131. package/src/editor/split_pane_container.js +0 -0
  132. package/src/flow/canvas_editor/connections.js +0 -0
  133. package/src/flow/canvas_editor/editor.js +0 -0
  134. package/src/flow/canvas_editor/index.js +0 -0
  135. package/src/flow/canvas_editor/layout.js +0 -0
  136. package/src/flow/canvas_editor/strings.js +0 -0
  137. package/src/flow/kit/always_before.js +0 -0
  138. package/src/flow/kit/catalogue.js +0 -0
  139. package/src/flow/kit/chip_input.js +0 -0
  140. package/src/flow/kit/dom.js +0 -0
  141. package/src/flow/kit/findings.js +0 -0
  142. package/src/flow/kit/history.js +0 -0
  143. package/src/flow/kit/index.js +0 -0
  144. package/src/flow/kit/keys.js +0 -0
  145. package/src/flow/kit/logic_graph.js +0 -0
  146. package/src/flow/kit/references.js +0 -0
  147. package/src/flow/kit/settings_panel.js +0 -0
  148. package/src/flow/kit/settings_schema.js +0 -0
  149. package/src/flow/kit/step_picker.js +0 -0
  150. package/src/flow/kit/strings.js +0 -0
  151. package/src/flow/kit/value_picker.js +0 -0
  152. package/src/flow/kit/widgets.js +0 -0
  153. package/src/flow/lanes/editor.js +0 -0
  154. package/src/flow/lanes/index.js +0 -0
  155. package/src/flow/lanes/layout.js +0 -0
  156. package/src/flow/lanes/pipeline.js +0 -0
  157. package/src/flow/lanes/preview.js +0 -0
  158. package/src/flow/lanes/strings.js +0 -0
  159. package/src/flow/outline/editor.js +0 -0
  160. package/src/flow/outline/index.js +0 -0
  161. package/src/flow/outline/mapping.js +0 -0
  162. package/src/flow/outline/menu.js +0 -0
  163. package/src/flow/outline/operations.js +0 -0
  164. package/src/flow/outline/recognise.js +0 -0
  165. package/src/flow/outline/strings.js +0 -0
  166. package/src/help/help_modal.js +0 -0
  167. package/src/help/help_registry.js +0 -0
  168. package/src/host/external_links.js +0 -0
  169. package/src/host/file_picker.js +0 -0
  170. package/src/host/host.js +0 -0
  171. package/src/host/photino_host.js +0 -0
  172. package/src/host/photino_host.md +0 -0
  173. package/src/host/pywebview_host.js +0 -0
  174. package/src/tiles/chart_tiles.js +0 -0
  175. package/src/tiles/config_schema.js +0 -0
  176. package/src/tiles/figure_tile.js +0 -0
  177. package/src/tiles/layout_persistence.js +0 -0
  178. package/src/tiles/list_tiles.js +0 -0
  179. package/src/tiles/scope_picker.js +0 -0
  180. package/src/tiles/tile_base.js +67 -16
  181. package/src/tiles/tile_config_form.js +0 -0
  182. package/src/tiles/tile_grid.js +0 -0
  183. package/src/tiles/tile_registry.js +0 -0
  184. package/src/tiling/bottom_panel_tabs.js +0 -0
  185. package/src/tiling/command_palette.js +0 -0
  186. package/src/tiling/content_registry.js +0 -0
  187. package/src/tiling/desktops.js +0 -0
  188. package/src/tiling/entity_sources.js +0 -0
  189. package/src/tiling/keymap.js +0 -0
  190. package/src/tiling/kind_taxonomy.js +0 -0
  191. package/src/tiling/landing_table.js +0 -0
  192. package/src/tiling/loading_overlay.js +0 -0
  193. package/src/tiling/nav_panel.js +0 -0
  194. package/src/tiling/page_factory.js +0 -0
  195. package/src/tiling/panel_availability.js +0 -0
  196. package/src/tiling/panel_keys.js +0 -0
  197. package/src/tiling/shell.js +0 -0
  198. package/src/tiling/tab_strip.js +0 -0
  199. package/src/tiling/tile_breadcrumb.js +0 -0
  200. package/src/tiling/tile_renderer.js +0 -0
  201. package/src/tiling/tile_tab_menu.js +0 -0
  202. package/src/tiling/tile_tree.js +0 -0
  203. package/src/tiling/wm.js +0 -0
  204. package/src/tiling/zoom.js +0 -0
  205. package/src/ui/base/component_base.js +0 -0
  206. package/src/ui/base/controller_base.js +0 -0
  207. package/src/ui/base/page_base.js +0 -0
  208. package/src/ui/components/about_dialog.js +0 -0
  209. package/src/ui/components/action_dropdown.js +19 -6
  210. package/src/ui/components/alert_log.js +0 -0
  211. package/src/ui/components/alerts_pane.js +0 -0
  212. package/src/ui/components/attribute_list_editor.js +0 -0
  213. package/src/ui/components/autocomplete_field.js +0 -0
  214. package/src/ui/components/cell_editor_host.js +0 -0
  215. package/src/ui/components/cell_editors.js +0 -0
  216. package/src/ui/components/cell_grid.js +0 -0
  217. package/src/ui/components/cell_paste.js +0 -0
  218. package/src/ui/components/cell_selection.js +0 -0
  219. package/src/ui/components/choice_controls.js +0 -0
  220. package/src/ui/components/color_field.js +0 -0
  221. package/src/ui/components/color_list_field.js +0 -0
  222. package/src/ui/components/column_fit.js +0 -0
  223. package/src/ui/components/column_groups.js +0 -0
  224. package/src/ui/components/column_order.js +0 -0
  225. package/src/ui/components/computing_status_window.js +0 -0
  226. package/src/ui/components/confirm_dialog.js +0 -0
  227. package/src/ui/components/context_menu.js +20 -20
  228. package/src/ui/components/conversation_view.js +0 -0
  229. package/src/ui/components/crosstab.js +0 -0
  230. package/src/ui/components/data_table.js +45 -19
  231. package/src/ui/components/detail_header.js +0 -0
  232. package/src/ui/components/document_outline.js +0 -0
  233. package/src/ui/components/drag_reorder.js +0 -0
  234. package/src/ui/components/fact_table.js +0 -0
  235. package/src/ui/components/fill_handle.js +0 -0
  236. package/src/ui/components/gallery_picker.js +0 -0
  237. package/src/ui/components/header_actions.js +0 -0
  238. package/src/ui/components/inline_renamer.js +0 -0
  239. package/src/ui/components/job_progress.js +0 -0
  240. package/src/ui/components/managed_window.js +0 -0
  241. package/src/ui/components/menu_bar.js +0 -0
  242. package/src/ui/components/modal.js +0 -0
  243. package/src/ui/components/notification_history.js +0 -0
  244. package/src/ui/components/progress.js +0 -0
  245. package/src/ui/components/record_list.js +0 -0
  246. package/src/ui/components/row_activation.js +0 -0
  247. package/src/ui/components/section_stack.js +0 -0
  248. package/src/ui/components/series.js +0 -0
  249. package/src/ui/components/settings_view.js +0 -0
  250. package/src/ui/components/slide_out_panel.js +0 -0
  251. package/src/ui/components/slider_field.js +0 -0
  252. package/src/ui/components/sortable_list.js +0 -0
  253. package/src/ui/components/staged_edits.js +0 -0
  254. package/src/ui/components/staged_history.js +0 -0
  255. package/src/ui/components/table_filter_chips.js +0 -0
  256. package/src/ui/components/table_state_store.js +0 -0
  257. package/src/ui/components/table_structure.js +0 -0
  258. package/src/ui/components/table_values.js +0 -0
  259. package/src/ui/components/tabs.js +0 -0
  260. package/src/ui/components/tag_input.js +0 -0
  261. package/src/ui/components/toast.js +0 -0
  262. package/src/ui/components/toggle_field.js +0 -0
  263. package/src/ui/components/tree_view.js +0 -0
  264. package/src/ui/components/tsv.js +0 -0
  265. package/src/ui/components/window_taskbar.js +0 -0
  266. package/src/ui/components/wizard.js +0 -0
  267. package/src/ui/controllers/window_chrome_controller.js +0 -0
  268. package/src/ui/notification_center.js +0 -0
  269. package/src/ui/utils/auto_scrollbars.js +0 -0
  270. package/src/ui/utils/markdown.js +0 -0
  271. package/src/ui/utils/menu_viewport.js +81 -0
  272. package/src/ui/utils/native_context_menu.js +0 -0
  273. package/src/ui/utils/overlay_scrollbar.js +0 -0
  274. package/src/ui/utils/raf_resize_observer.js +0 -0
  275. package/src/ui/utils/scroll_lock.js +0 -0
  276. package/src/ui/utils/strip_scroll.js +0 -0
  277. package/src/ui/utils/tooltip_service.js +0 -0
  278. package/tiles.js +0 -0
  279. package/widgets.js +0 -0
  280. package/wm.js +0 -0
  281. package/dist/chunk-LWV23YVE.js.map +0 -7
  282. package/dist/chunk-RMTRWWEO.js.map +0 -7
  283. package/dist/chunk-UURZ37SD.js.map +0 -7
  284. package/dist/chunk-UWO43BZU.js.map +0 -7
@@ -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
@@ -0,0 +1,81 @@
1
+ /**
2
+ * menu_viewport.js — keeping a context menu inside the window (BUG-0040).
3
+ *
4
+ * FlexDesk draws three context menus of its own: `showContextMenu`
5
+ * (../components/context_menu.js), the DataTable's right-click menu
6
+ * (../components/data_table.js) and `NotebookTabBar`'s tab menu
7
+ * (../../editor/notebook_tab_bar.js). Each placed itself with its own copy of
8
+ * `top = min(y, innerHeight - height - 8)`, and none had a max-height. A menu
9
+ * taller than the window then opened above its top edge (the DataTable's did
10
+ * not clamp at all: a 101-item row menu opened at y=-2153 in a 761 px window)
11
+ * or ran off its bottom, and nothing scrolled, so the items past the edge could
12
+ * not be reached by any means.
13
+ *
14
+ * One placement now, for all three:
15
+ * - the menu is at most the window's height less a margin above and below,
16
+ * and scrolls inside when it is longer (`overflow-y: auto` in flexdesk.css);
17
+ * - it opens at the pointer, moved left or up just enough to fit, and never
18
+ * above the window's top or left edge;
19
+ * - keyboard movement focuses an item and scrolls it into view within the
20
+ * menu, so every item can be reached with the arrows.
21
+ */
22
+
23
+ /** The gap kept between a menu and the window's edges, in px. */
24
+ export const MENU_VIEWPORT_MARGIN = 8;
25
+
26
+ /**
27
+ * Size and place an open (displayed) menu at `(x, y)` inside its window.
28
+ *
29
+ * @param {HTMLElement} menu the menu, already in the document and displayed
30
+ * @param {number} x client x of the pointer
31
+ * @param {number} y client y of the pointer
32
+ * @param {number} [margin] gap to the window's edges
33
+ */
34
+ export function placeMenuInViewport(menu, x, y, margin = MENU_VIEWPORT_MARGIN) {
35
+ const view = menu.ownerDocument?.defaultView || window;
36
+ const vw = view.innerWidth;
37
+ const vh = view.innerHeight;
38
+ const maxHeight = Math.max(0, vh - 2 * margin);
39
+ menu.style.maxHeight = `${maxHeight}px`;
40
+ menu.scrollTop = 0;
41
+ // flexdesk.css makes the menu `box-sizing: border-box`, so its measured
42
+ // height (padding and border included) is at most `maxHeight`.
43
+ const rect = menu.getBoundingClientRect();
44
+ const left = Math.min(x, vw - rect.width - margin);
45
+ const top = Math.min(y, vh - rect.height - margin);
46
+ menu.style.left = `${Math.max(0, left)}px`;
47
+ menu.style.top = `${Math.max(0, top)}px`;
48
+ }
49
+
50
+ /**
51
+ * Focus a menu item and bring it into view inside the (scrolling) menu.
52
+ * `preventScroll` keeps the page where it is; `block: 'nearest'` scrolls the
53
+ * menu only as far as the item needs.
54
+ */
55
+ export function focusMenuItem(item) {
56
+ if (!item) return;
57
+ item.focus({ preventScroll: true });
58
+ item.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
59
+ }
60
+
61
+ /**
62
+ * Up/Down/Home/End over `items` (the enabled ones), wrapping, from the one
63
+ * that has focus. Returns true when the key moved focus (the caller's event
64
+ * has then been `preventDefault`ed).
65
+ *
66
+ * @param {KeyboardEvent} e
67
+ * @param {HTMLElement[]} items
68
+ */
69
+ export function moveMenuFocus(e, items) {
70
+ if (!items.length) return false;
71
+ const at = items.indexOf(items[0].ownerDocument.activeElement);
72
+ let next = null;
73
+ if (e.key === 'ArrowDown') next = items[(at + 1 + items.length) % items.length];
74
+ else if (e.key === 'ArrowUp') next = items[(at - 1 + items.length) % items.length];
75
+ else if (e.key === 'Home') next = items[0];
76
+ else if (e.key === 'End') next = items[items.length - 1];
77
+ if (!next) return false;
78
+ e.preventDefault();
79
+ focusMenuItem(next);
80
+ return true;
81
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/tiles.js CHANGED
File without changes
package/widgets.js CHANGED
File without changes
package/wm.js CHANGED
File without changes