flexdesk 0.17.17 → 0.18.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.
- package/CHANGELOG.md +70 -0
- package/LICENSE +0 -0
- package/README.md +1 -0
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +0 -0
- package/charts.js +0 -0
- package/core.js +0 -0
- package/css/flexdesk.css +7499 -5851
- package/css/reset.css +1 -1
- package/css/tokens.css +23 -1
- package/dist/canvas.js +0 -0
- package/dist/canvas.js.map +0 -0
- package/dist/charts.js +10 -6
- package/dist/charts.js.map +1 -1
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-5W4EEKSK.js +778 -0
- package/dist/chunk-5W4EEKSK.js.map +7 -0
- package/dist/chunk-DNJ3N4T4.js +82 -0
- package/dist/chunk-DNJ3N4T4.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/chunk-GOQLR5WR.js +0 -0
- package/dist/chunk-GOQLR5WR.js.map +0 -0
- package/dist/{chunk-JKZLNKG6.js → chunk-HKPB6OFA.js} +90 -25
- package/dist/{chunk-JKZLNKG6.js.map → chunk-HKPB6OFA.js.map} +2 -2
- package/dist/chunk-HNU6MM2B.js +0 -0
- package/dist/chunk-HNU6MM2B.js.map +0 -0
- package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
- package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
- package/dist/chunk-N7HC3ADV.js +0 -0
- package/dist/chunk-N7HC3ADV.js.map +0 -0
- package/dist/{chunk-7OL6F2AM.js → chunk-P74F6I6P.js} +6 -4
- package/dist/{chunk-7OL6F2AM.js.map → chunk-P74F6I6P.js.map} +1 -1
- package/dist/chunk-PAI2ZUWV.js +260 -0
- package/dist/chunk-PAI2ZUWV.js.map +7 -0
- package/dist/chunk-Q7BALEPX.js +527 -0
- package/dist/chunk-Q7BALEPX.js.map +7 -0
- package/dist/chunk-QXAOTGCC.js +40 -0
- package/dist/chunk-QXAOTGCC.js.map +7 -0
- package/dist/{chunk-XJNADRGR.js → chunk-RKVYIXDC.js} +664 -91
- package/dist/chunk-RKVYIXDC.js.map +7 -0
- package/dist/{chunk-2Z2ENHR3.js → chunk-U5GKJUOC.js} +3 -3
- package/dist/{chunk-2Z2ENHR3.js.map → chunk-U5GKJUOC.js.map} +1 -1
- package/dist/chunk-UKP22RUG.js +219 -0
- package/dist/chunk-UKP22RUG.js.map +7 -0
- package/dist/chunk-VLOAI3HS.js +43 -0
- package/dist/chunk-VLOAI3HS.js.map +7 -0
- package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
- package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
- package/dist/{chunk-PGZ4EHSE.js → chunk-ZL3VKINP.js} +41 -390
- package/dist/chunk-ZL3VKINP.js.map +7 -0
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/flexdesk.css +7499 -5851
- package/dist/flow.js +29 -97
- package/dist/flow.js.map +3 -3
- package/dist/git.js +1356 -0
- package/dist/git.js.map +7 -0
- package/dist/host.js +0 -0
- package/dist/host.js.map +0 -0
- package/dist/reset.css +1 -1
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +20 -15
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +23 -1
- package/dist/widgets.js +3969 -1189
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +121 -33
- package/dist/wm.js.map +2 -2
- package/docs/README.md +9 -4
- package/docs/best-practices.md +2 -2
- package/docs/components/chat-shell.md +198 -0
- package/docs/components/command-palette.md +0 -0
- package/docs/components/conversation.md +10 -0
- package/docs/components/data-table-grid.md +0 -0
- package/docs/components/data-table.md +0 -0
- package/docs/components/editor.md +0 -0
- package/docs/components/filter-builder.md +246 -0
- package/docs/components/flow-canvas-editor.md +0 -0
- package/docs/components/flow-editors.md +0 -0
- package/docs/components/flow-lane-editor.md +0 -0
- package/docs/components/flow-outline-editor.md +0 -0
- package/docs/components/forms-and-dialogs.md +22 -0
- package/docs/components/git.md +355 -0
- package/docs/components/input-fields.md +167 -2
- package/docs/components/keymap.md +8 -0
- package/docs/components/markdown-and-outline.md +0 -0
- package/docs/components/menus.md +120 -3
- package/docs/components/panels-and-desktops.md +16 -5
- package/docs/components/record-list-and-fact-table.md +0 -0
- package/docs/components/settings-view.md +0 -0
- package/docs/components/shell.md +104 -1
- package/docs/components/wizard.md +0 -0
- package/docs/components/workspace-picker.md +149 -0
- package/docs/concepts/building-and-packaging.md +2 -2
- package/docs/concepts/domain-neutrality.md +0 -0
- package/docs/concepts/host-port.md +0 -0
- package/docs/concepts/packages-and-entry-points.md +14 -13
- package/docs/concepts/peer-dependencies.md +0 -0
- package/docs/concepts/state-guard.md +3 -3
- package/docs/quickstart.md +1 -1
- package/editor.js +0 -0
- package/flow.js +0 -0
- package/git.js +13 -0
- package/host.js +0 -0
- package/package.json +6 -1
- package/src/canvas/canvas_adapter.js +0 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +0 -0
- package/src/canvas/layered_layout.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +0 -0
- package/src/charts/plot_config.js +0 -0
- package/src/charts/plotly_wrapper.js +0 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +0 -0
- package/src/core/settings.js +0 -0
- package/src/core/state_guard.js +0 -0
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +0 -0
- package/src/editor/monaco_editor_factory.js +0 -0
- package/src/editor/monaco_loader.js +0 -0
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +0 -0
- package/src/flow/canvas_editor/connections.js +0 -0
- package/src/flow/canvas_editor/editor.js +0 -0
- package/src/flow/canvas_editor/index.js +0 -0
- package/src/flow/canvas_editor/layout.js +0 -0
- package/src/flow/canvas_editor/strings.js +0 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +0 -0
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +28 -119
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +0 -0
- package/src/flow/kit/keys.js +0 -0
- package/src/flow/kit/logic_graph.js +0 -0
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +0 -0
- package/src/flow/kit/step_picker.js +1 -1
- package/src/flow/kit/strings.js +0 -0
- package/src/flow/kit/value_picker.js +1 -1
- package/src/flow/kit/widgets.js +0 -0
- package/src/flow/lanes/editor.js +0 -0
- package/src/flow/lanes/index.js +0 -0
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +0 -0
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +0 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +1 -1
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/git/diff_viewer.js +212 -0
- package/src/git/diff_window.js +51 -0
- package/src/git/git_graph.js +401 -0
- package/src/git/git_ops.js +61 -0
- package/src/git/git_panel.js +755 -0
- package/src/git/index.js +9 -0
- package/src/git/line_diff.js +202 -0
- package/src/help/help_modal.js +0 -0
- package/src/help/help_registry.js +0 -0
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +0 -0
- package/src/host/host.js +0 -0
- package/src/host/photino_host.js +0 -0
- package/src/host/photino_host.md +0 -0
- package/src/host/pywebview_host.js +0 -0
- package/src/tiles/chart_tiles.js +0 -0
- package/src/tiles/config_schema.js +0 -0
- package/src/tiles/figure_tile.js +0 -0
- package/src/tiles/layout_persistence.js +0 -0
- package/src/tiles/list_tiles.js +0 -0
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +5 -4
- package/src/tiles/tile_config_form.js +0 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +0 -0
- package/src/tiling/command_palette.js +0 -0
- package/src/tiling/content_registry.js +0 -0
- package/src/tiling/desktops.js +10 -8
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +6 -5
- package/src/tiling/kind_taxonomy.js +15 -2
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +0 -0
- package/src/tiling/page_factory.js +0 -0
- package/src/tiling/panel_availability.js +0 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +124 -17
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_renderer.js +2 -3
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +4 -78
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/anchored_layer.js +255 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +0 -0
- package/src/ui/components/cell_editors.js +0 -0
- package/src/ui/components/cell_grid.js +0 -0
- package/src/ui/components/cell_paste.js +0 -0
- package/src/ui/components/cell_selection.js +0 -0
- package/src/ui/components/chat_shell.js +920 -0
- package/src/ui/components/chat_state_machine.js +223 -0
- package/src/ui/components/choice_controls.js +45 -2
- package/src/ui/components/color_field.js +3 -2
- package/src/ui/components/color_list_field.js +3 -2
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +0 -0
- package/src/ui/components/column_order.js +0 -0
- package/src/ui/components/combobox.js +422 -0
- package/src/ui/components/conversation_view.js +17 -2
- package/src/ui/components/crosstab.js +0 -0
- package/src/ui/components/data_table.js +2 -2
- package/src/ui/components/detail_header.js +0 -0
- package/src/ui/components/document_outline.js +0 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +0 -0
- package/src/ui/components/filter_builder.js +766 -0
- package/src/ui/components/filter_model.js +614 -0
- package/src/ui/components/gallery_picker.js +0 -0
- package/src/ui/components/header_actions.js +0 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +0 -0
- package/src/ui/components/managed_window.js +24 -47
- package/src/ui/components/menu_bar.js +101 -24
- package/src/ui/components/modal.js +79 -109
- package/src/ui/components/option_list.js +160 -0
- package/src/ui/components/popover.js +94 -0
- package/src/ui/components/progress.js +0 -0
- package/src/ui/components/recent_items.js +169 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +0 -0
- package/src/ui/components/select.js +322 -0
- package/src/ui/components/series.js +0 -0
- package/src/ui/components/settings_view.js +0 -0
- package/src/ui/components/slide_out_panel.js +0 -0
- package/src/ui/components/slider_field.js +0 -0
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +0 -0
- package/src/ui/components/staged_history.js +0 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +0 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +0 -0
- package/src/ui/components/tag_input.js +77 -8
- package/src/ui/components/toast.js +0 -0
- package/src/ui/components/toggle_field.js +0 -0
- package/src/ui/components/tsv.js +0 -0
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/workspace_picker.js +1155 -0
- package/src/ui/controllers/window_chrome_controller.js +4 -1
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/fire_event.js +17 -0
- package/src/ui/utils/focusable.js +25 -0
- package/src/ui/utils/markdown.js +0 -0
- package/src/ui/utils/modal_host.js +27 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +0 -0
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +0 -0
- package/src/ui/utils/tooltip_service.js +0 -0
- package/tiles.js +0 -0
- package/widgets.js +12 -2
- package/wm.js +0 -0
- package/css/base.css +0 -10527
- package/css/overrides.css +0 -2596
- package/dist/chunk-7JI7HHSY.js +0 -107
- package/dist/chunk-7JI7HHSY.js.map +0 -7
- package/dist/chunk-MIW7D6WC.js +0 -526
- package/dist/chunk-MIW7D6WC.js.map +0 -7
- package/dist/chunk-PGZ4EHSE.js.map +0 -7
- package/dist/chunk-XJNADRGR.js.map +0 -7
- /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { getSetting } from '../../core/settings.js';
|
|
21
|
+
import { isInAnchoredLayer } from './anchored_layer.js';
|
|
22
|
+
import { collectFocusable } from '../utils/focusable.js';
|
|
21
23
|
|
|
22
24
|
/** Where window geometry lives when the embedder names no key. An embedder
|
|
23
25
|
* that saved geometry under another key keeps it by passing that key to
|
|
@@ -70,29 +72,6 @@ let _activeWindows = new Map(); // id -> ManagedWindow instance
|
|
|
70
72
|
* the same event reaches it — one Esc must close one window. */
|
|
71
73
|
const _escAnswered = new WeakSet();
|
|
72
74
|
|
|
73
|
-
/** Collect the focusable elements inside `root`, in DOM order. Used
|
|
74
|
-
* by the modal focus trap so Tab cycles within the modal frame and
|
|
75
|
-
* can't escape into the underlying tiles. */
|
|
76
|
-
function _collectFocusable(root) {
|
|
77
|
-
if (!root) return [];
|
|
78
|
-
const sel = [
|
|
79
|
-
'a[href]',
|
|
80
|
-
'button:not([disabled])',
|
|
81
|
-
'input:not([disabled]):not([type="hidden"])',
|
|
82
|
-
'select:not([disabled])',
|
|
83
|
-
'textarea:not([disabled])',
|
|
84
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
85
|
-
].join(',');
|
|
86
|
-
return Array.from(root.querySelectorAll(sel)).filter((el) => {
|
|
87
|
-
// Skip hidden / zero-size elements — Tab visits only what the
|
|
88
|
-
// user can actually see.
|
|
89
|
-
if (el.hidden) return false;
|
|
90
|
-
if (el.closest('[hidden]')) return false;
|
|
91
|
-
const r = el.getBoundingClientRect();
|
|
92
|
-
return r.width > 0 || r.height > 0;
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
75
|
// Global state cache
|
|
97
76
|
let _stateCache = null;
|
|
98
77
|
|
|
@@ -548,12 +527,17 @@ export class ManagedWindow {
|
|
|
548
527
|
// taskbar can filter on receipt: an in-panel taskbar shows only the
|
|
549
528
|
// windows mounted in its own panel, and the viewport taskbar shows only
|
|
550
529
|
// the ones with no container.
|
|
551
|
-
|
|
552
|
-
detail: { id: this.id, container: this.container }
|
|
553
|
-
}));
|
|
530
|
+
this._announce('managed-window-closed', { id: this.id, container: this.container });
|
|
554
531
|
this._release();
|
|
555
532
|
}
|
|
556
533
|
|
|
534
|
+
/** Every window event goes out on the page's window, and a window takes only
|
|
535
|
+
* an event built by its own `CustomEvent`: Node's global one is refused
|
|
536
|
+
* under jsdom, in the middle of the close that fired it. */
|
|
537
|
+
_announce(name, detail) {
|
|
538
|
+
window.dispatchEvent(new window.CustomEvent(name, { detail }));
|
|
539
|
+
}
|
|
540
|
+
|
|
557
541
|
/** BUG-0053. A closed window leaves the registry and lets go of what it
|
|
558
542
|
* held: its content, its DOM and every consumer callback (an `onClose`
|
|
559
543
|
* typically captures its owner). Until now the registry kept all of it
|
|
@@ -607,12 +591,10 @@ export class ManagedWindow {
|
|
|
607
591
|
}
|
|
608
592
|
|
|
609
593
|
// Dispatch event first so the taskbar button is created synchronously
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
}));
|
|
594
|
+
this._announce('managed-window-minimized', {
|
|
595
|
+
id: this.id, title: this.title, icon: this.icon,
|
|
596
|
+
container: this.container,
|
|
597
|
+
});
|
|
616
598
|
|
|
617
599
|
if (!this.element) return;
|
|
618
600
|
|
|
@@ -744,9 +726,7 @@ export class ManagedWindow {
|
|
|
744
726
|
this.bringToFront();
|
|
745
727
|
|
|
746
728
|
// Notify taskbar (removes the taskbar button)
|
|
747
|
-
|
|
748
|
-
detail: { id: this.id, container: this.container }
|
|
749
|
-
}));
|
|
729
|
+
this._announce('managed-window-restored', { id: this.id, container: this.container });
|
|
750
730
|
}
|
|
751
731
|
|
|
752
732
|
/**
|
|
@@ -825,7 +805,7 @@ export class ManagedWindow {
|
|
|
825
805
|
|
|
826
806
|
// A CLASS THE STYLESHEET HAS ALWAYS KNOWN AND NOTHING EVER SET.
|
|
827
807
|
// `.twm-managed-window--maximized .twm-managed-window__resize
|
|
828
|
-
// { display: none }` is in `css/
|
|
808
|
+
// { display: none }` is in `css/flexdesk.css` — "a maximised window is not
|
|
829
809
|
// resized by dragging its edges" — and it has never matched anything,
|
|
830
810
|
// because no code path adds the modifier. So a maximised window keeps
|
|
831
811
|
// eight live resize handles hanging 3px outside its edges. Exactly the
|
|
@@ -841,9 +821,7 @@ export class ManagedWindow {
|
|
|
841
821
|
// tiles — had no edge to hang the rule on and would have had to poll.
|
|
842
822
|
// Same detail shape as the other three window events, `container`
|
|
843
823
|
// included, so a per-region consumer can filter on receipt.
|
|
844
|
-
|
|
845
|
-
detail: { id: this.id, maximized: this.isMaximized, container: this.container },
|
|
846
|
-
}));
|
|
824
|
+
this._announce('managed-window-maximized', { id: this.id, maximized: this.isMaximized, container: this.container });
|
|
847
825
|
}
|
|
848
826
|
|
|
849
827
|
/**
|
|
@@ -965,7 +943,7 @@ export class ManagedWindow {
|
|
|
965
943
|
}
|
|
966
944
|
this.element.setAttribute('data-window-id', this.id);
|
|
967
945
|
// BUG-0053. Focusable by script and by a press, never by Tab (the
|
|
968
|
-
// modal trap's `
|
|
946
|
+
// modal trap's `collectFocusable` skips tabindex -1): a press on the
|
|
969
947
|
// title bar or on empty content keeps focus in the window, where a
|
|
970
948
|
// non-modal window's Esc looks for it.
|
|
971
949
|
this.element.tabIndex = -1;
|
|
@@ -1274,9 +1252,7 @@ export class ManagedWindow {
|
|
|
1274
1252
|
this.width = bounds.width; this.height = bounds.height;
|
|
1275
1253
|
}
|
|
1276
1254
|
this._applyPosition();
|
|
1277
|
-
|
|
1278
|
-
detail: { id: this.id, container },
|
|
1279
|
-
}));
|
|
1255
|
+
this._announce('managed-window-moved', { id: this.id, container });
|
|
1280
1256
|
return true;
|
|
1281
1257
|
}
|
|
1282
1258
|
|
|
@@ -1736,9 +1712,7 @@ export class ManagedWindow {
|
|
|
1736
1712
|
this.height = rect.height;
|
|
1737
1713
|
this._applyPosition();
|
|
1738
1714
|
this.element?.classList.add('twm-managed-window--snapped');
|
|
1739
|
-
|
|
1740
|
-
detail: { id: this.id, zone, container: this.container },
|
|
1741
|
-
}));
|
|
1715
|
+
this._announce('managed-window-snapped', { id: this.id, zone, container: this.container });
|
|
1742
1716
|
}
|
|
1743
1717
|
|
|
1744
1718
|
/** Restore the geometry a snap replaced. Called when a snapped window is
|
|
@@ -1850,7 +1824,10 @@ export class ManagedWindow {
|
|
|
1850
1824
|
// active element into the background — bad UX, and a
|
|
1851
1825
|
// genuine WCAG focus-trap violation for accessibility tools.
|
|
1852
1826
|
if (e.key === 'Tab' && this.modal && this.element) {
|
|
1853
|
-
|
|
1827
|
+
// A popover opened from this modal is over it but outside its frame,
|
|
1828
|
+
// and sees to its own Tab.
|
|
1829
|
+
if (isInAnchoredLayer(document.activeElement)) return;
|
|
1830
|
+
const focusables = collectFocusable(this.element);
|
|
1854
1831
|
if (focusables.length === 0) {
|
|
1855
1832
|
e.preventDefault();
|
|
1856
1833
|
return;
|
|
@@ -62,6 +62,15 @@
|
|
|
62
62
|
* shortcut, context, action }`), so the palette and the bar list identical
|
|
63
63
|
* verbs.
|
|
64
64
|
*
|
|
65
|
+
* ── Two styles: a bar, or one hamburger button (STORY-0133) ───────────
|
|
66
|
+
* `style: 'bar'` (the default) draws a title per menu: File Edit Help.
|
|
67
|
+
* `style: 'hamburger'` draws ONE icon button (`icon`, 'menu' by default,
|
|
68
|
+
* named by `label`) whose menu lists the menus, each flying out its items:
|
|
69
|
+
* ☰ › File › Open…. It is the same model, so `commands()`, `setEnabled`,
|
|
70
|
+
* `setChecked` and `onAction` do not change; `open(menuId)` opens the
|
|
71
|
+
* hamburger with that menu flown out. `setStyle(style)` switches at run
|
|
72
|
+
* time (a setting, a narrow window), closing whatever is open.
|
|
73
|
+
*
|
|
65
74
|
* ── Roles ─────────────────────────────────────────────────────────────
|
|
66
75
|
* The WAI-ARIA menubar pattern: `role="menubar"` on the element you pass,
|
|
67
76
|
* a `role="menuitem"` title per menu inside a `role="none"` wrapper, each
|
|
@@ -77,6 +86,9 @@
|
|
|
77
86
|
const SEL_TITLE = '.twm-menubar__title';
|
|
78
87
|
const SEL_ITEM = '.twm-menubar__item';
|
|
79
88
|
const SEL_MENU = '.twm-menubar__menu';
|
|
89
|
+
const STYLES = ['bar', 'hamburger'];
|
|
90
|
+
/** The hamburger's own model ids. A menu's row in it is `${HAMBURGER}:${menu.id}`. */
|
|
91
|
+
const HAMBURGER = 'twm-menubar:hamburger';
|
|
80
92
|
|
|
81
93
|
/** @returns {MenuBar} */
|
|
82
94
|
export function createMenuBar(el, opts) {
|
|
@@ -91,16 +103,22 @@ export class MenuBar {
|
|
|
91
103
|
* @param {Function} [opts.onAction] `(id, { item, checked }) => void`
|
|
92
104
|
* @param {string} [opts.label] the bar's accessible name ('Menu')
|
|
93
105
|
* @param {string} [opts.emptyLabel] the disabled row an empty submenu shows ('Empty')
|
|
106
|
+
* @param {string} [opts.style] 'bar' (a title per menu, the default) or 'hamburger' (one button)
|
|
107
|
+
* @param {string} [opts.icon] the hamburger's Material Symbols icon ('menu')
|
|
94
108
|
*/
|
|
95
|
-
constructor(el, { menus, onAction = null, label = 'Menu', emptyLabel = 'Empty' } = {}) {
|
|
109
|
+
constructor(el, { menus, onAction = null, label = 'Menu', emptyLabel = 'Empty', style = 'bar', icon = 'menu' } = {}) {
|
|
96
110
|
if (!el || typeof el.appendChild !== 'function') {
|
|
97
111
|
throw new TypeError('createMenuBar: `el` must be an element');
|
|
98
112
|
}
|
|
99
113
|
_validateMenus(menus);
|
|
114
|
+
_validateStyle(style);
|
|
100
115
|
this.el = el;
|
|
101
116
|
this.menus = menus;
|
|
102
117
|
this._onAction = typeof onAction === 'function' ? onAction : null;
|
|
103
118
|
this._emptyLabel = emptyLabel;
|
|
119
|
+
this._label = label;
|
|
120
|
+
this._icon = icon;
|
|
121
|
+
this._style = style;
|
|
104
122
|
this._doc = el.ownerDocument;
|
|
105
123
|
this._disabled = new Map(); // id -> boolean (override)
|
|
106
124
|
this._checked = new Map(); // id -> boolean (override, check items)
|
|
@@ -112,26 +130,10 @@ export class MenuBar {
|
|
|
112
130
|
this._returnTo = null; // what had focus before the bar did
|
|
113
131
|
this._altArmed = false;
|
|
114
132
|
|
|
115
|
-
el.innerHTML = '';
|
|
116
133
|
el.classList.add('twm-menubar');
|
|
117
134
|
el.setAttribute('role', 'menubar');
|
|
118
135
|
el.setAttribute('aria-label', label);
|
|
119
|
-
this.
|
|
120
|
-
const entry = this._doc.createElement('div');
|
|
121
|
-
entry.className = 'twm-menubar__entry';
|
|
122
|
-
entry.setAttribute('role', 'none');
|
|
123
|
-
const btn = this._doc.createElement('button');
|
|
124
|
-
btn.type = 'button';
|
|
125
|
-
btn.className = 'twm-menubar__title';
|
|
126
|
-
btn.setAttribute('role', 'menuitem');
|
|
127
|
-
btn.setAttribute('aria-haspopup', 'menu');
|
|
128
|
-
btn.setAttribute('aria-expanded', 'false');
|
|
129
|
-
btn.tabIndex = i === 0 ? 0 : -1;
|
|
130
|
-
btn.textContent = m.label;
|
|
131
|
-
entry.appendChild(btn);
|
|
132
|
-
el.appendChild(entry);
|
|
133
|
-
return btn;
|
|
134
|
-
});
|
|
136
|
+
this._renderTitles();
|
|
135
137
|
|
|
136
138
|
this._h = {
|
|
137
139
|
mousedown: (e) => this._onMouseDown(e),
|
|
@@ -163,12 +165,35 @@ export class MenuBar {
|
|
|
163
165
|
/** Whether a menu is open. */
|
|
164
166
|
isOpen() { return this._openIdx >= 0; }
|
|
165
167
|
|
|
166
|
-
/** Open the menu with this id (focus on its title, or on its first item).
|
|
168
|
+
/** Open the menu with this id (focus on its title, or on its first item).
|
|
169
|
+
* A hamburger opens with that menu flown out (focus on its row, or on
|
|
170
|
+
* the menu's first item). */
|
|
167
171
|
open(menuId, { focusItem = false } = {}) {
|
|
168
172
|
const i = this.menus.findIndex((m) => m.id === menuId);
|
|
169
173
|
if (i < 0) return;
|
|
170
174
|
this._enter();
|
|
171
|
-
this._open(i, focusItem ? 'first' : null);
|
|
175
|
+
if (this._style !== 'hamburger') { this._open(i, focusItem ? 'first' : null); return; }
|
|
176
|
+
this._open(0, null);
|
|
177
|
+
const row = this._itemsIn(this._stack[0].menu)
|
|
178
|
+
.find((b) => this._models.get(b)?.id === `${HAMBURGER}:${menuId}`);
|
|
179
|
+
if (!row) return;
|
|
180
|
+
row.focus({ preventScroll: true });
|
|
181
|
+
this._openSub(row, focusItem ? 'first' : null);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 'bar' or 'hamburger'. */
|
|
185
|
+
get style() { return this._style; }
|
|
186
|
+
|
|
187
|
+
/** Draw the menus as a bar or as one hamburger button. Whatever is open
|
|
188
|
+
* closes; the model and every setEnabled / setChecked stay. */
|
|
189
|
+
setStyle(style) {
|
|
190
|
+
_validateStyle(style);
|
|
191
|
+
if (style === this._style) return;
|
|
192
|
+
const hadFocus = this.el.contains(this._doc.activeElement);
|
|
193
|
+
this._deactivate();
|
|
194
|
+
this._style = style;
|
|
195
|
+
this._renderTitles();
|
|
196
|
+
if (hadFocus) this._focusTitle(0);
|
|
172
197
|
}
|
|
173
198
|
|
|
174
199
|
/** Close every open menu. Focus stays in the bar if it was there. */
|
|
@@ -239,7 +264,7 @@ export class MenuBar {
|
|
|
239
264
|
this._doc.removeEventListener('mousedown', this._h.docMousedown, true);
|
|
240
265
|
this._doc.defaultView?.removeEventListener('blur', this._h.winBlur);
|
|
241
266
|
this.el.innerHTML = '';
|
|
242
|
-
this.el.classList.remove('twm-menubar');
|
|
267
|
+
this.el.classList.remove('twm-menubar', 'twm-menubar--hamburger');
|
|
243
268
|
this.el.removeAttribute('role');
|
|
244
269
|
this.el.removeAttribute('aria-label');
|
|
245
270
|
}
|
|
@@ -317,6 +342,50 @@ export class MenuBar {
|
|
|
317
342
|
}
|
|
318
343
|
}
|
|
319
344
|
|
|
345
|
+
// ── titles ────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
/** The titles for the current style. `_top` is what they open: the
|
|
348
|
+
* menus themselves, or the hamburger's one menu that lists them. */
|
|
349
|
+
_renderTitles() {
|
|
350
|
+
const d = this._doc;
|
|
351
|
+
const burger = this._style === 'hamburger';
|
|
352
|
+
this.el.innerHTML = '';
|
|
353
|
+
this.el.classList.toggle('twm-menubar--hamburger', burger);
|
|
354
|
+
this._top = burger
|
|
355
|
+
? [{ id: HAMBURGER, label: this._label, items: this.menus.map((m) => ({
|
|
356
|
+
id: `${HAMBURGER}:${m.id}`, label: m.label, submenu: m.items,
|
|
357
|
+
})) }]
|
|
358
|
+
: this.menus;
|
|
359
|
+
this._titles = this._top.map((m, i) => {
|
|
360
|
+
const entry = d.createElement('div');
|
|
361
|
+
entry.className = 'twm-menubar__entry';
|
|
362
|
+
entry.setAttribute('role', 'none');
|
|
363
|
+
const btn = d.createElement('button');
|
|
364
|
+
btn.type = 'button';
|
|
365
|
+
btn.className = 'twm-menubar__title';
|
|
366
|
+
btn.setAttribute('role', 'menuitem');
|
|
367
|
+
btn.setAttribute('aria-haspopup', 'menu');
|
|
368
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
369
|
+
btn.tabIndex = i === 0 ? 0 : -1;
|
|
370
|
+
if (burger) {
|
|
371
|
+
btn.classList.add('twm-menubar__title--icon', 'twm-has-tooltip');
|
|
372
|
+
btn.setAttribute('aria-label', m.label);
|
|
373
|
+
btn.dataset.tooltip = m.label;
|
|
374
|
+
btn.dataset.tooltipPlacement = 'bottom';
|
|
375
|
+
const icon = d.createElement('span');
|
|
376
|
+
icon.className = 'material-symbols-outlined';
|
|
377
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
378
|
+
icon.textContent = this._icon;
|
|
379
|
+
btn.appendChild(icon);
|
|
380
|
+
} else {
|
|
381
|
+
btn.textContent = m.label;
|
|
382
|
+
}
|
|
383
|
+
entry.appendChild(btn);
|
|
384
|
+
this.el.appendChild(entry);
|
|
385
|
+
return btn;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
320
389
|
// ── open / close ──────────────────────────────────────────────────
|
|
321
390
|
|
|
322
391
|
_setRoving(i) {
|
|
@@ -359,7 +428,7 @@ export class MenuBar {
|
|
|
359
428
|
// being removed never holds focus.
|
|
360
429
|
this._focusTitle(i);
|
|
361
430
|
this._closeAll();
|
|
362
|
-
const m = this.
|
|
431
|
+
const m = this._top[i];
|
|
363
432
|
const title = this._titles[i];
|
|
364
433
|
const menu = this._buildMenu(m.items, m.label);
|
|
365
434
|
title.parentElement.appendChild(menu);
|
|
@@ -596,6 +665,8 @@ export class MenuBar {
|
|
|
596
665
|
_titleKey(key, i) {
|
|
597
666
|
const n = this._titles.length;
|
|
598
667
|
const open = this._openIdx >= 0;
|
|
668
|
+
// One title (a hamburger): there is no next or previous menu.
|
|
669
|
+
if (n === 1 && (key === 'ArrowRight' || key === 'ArrowLeft' || key === 'Home' || key === 'End')) return true;
|
|
599
670
|
switch (key) {
|
|
600
671
|
case 'ArrowRight': open ? this._open(i + 1, null) : this._focusTitle((i + 1) % n); return true;
|
|
601
672
|
case 'ArrowLeft': open ? this._open(i - 1, null) : this._focusTitle((i - 1 + n) % n); return true;
|
|
@@ -642,11 +713,11 @@ export class MenuBar {
|
|
|
642
713
|
case 'End': this._focusIn(menu, 'last'); return true;
|
|
643
714
|
case 'ArrowRight':
|
|
644
715
|
if (item?.submenu != null && !this._isDisabled(item)) this._openSub(btn, 'first');
|
|
645
|
-
else this._open(this._openIdx + 1, 'first');
|
|
716
|
+
else if (this._titles.length > 1) this._open(this._openIdx + 1, 'first');
|
|
646
717
|
return true;
|
|
647
718
|
case 'ArrowLeft':
|
|
648
719
|
if (level > 0) this._closeFrom(level);
|
|
649
|
-
else this._open(this._openIdx - 1, 'first');
|
|
720
|
+
else if (this._titles.length > 1) this._open(this._openIdx - 1, 'first');
|
|
650
721
|
return true;
|
|
651
722
|
case 'Enter': case ' ':
|
|
652
723
|
this._activate(btn);
|
|
@@ -706,6 +777,12 @@ function _kind(item) {
|
|
|
706
777
|
return 'menuitem';
|
|
707
778
|
}
|
|
708
779
|
|
|
780
|
+
function _validateStyle(style) {
|
|
781
|
+
if (!STYLES.includes(style)) {
|
|
782
|
+
throw new TypeError(`createMenuBar: \`style\` must be ${STYLES.map((s) => `'${s}'`).join(' or ')}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
709
786
|
/** A bad model is a boot error, not a menu that silently lacks a row. */
|
|
710
787
|
function _validateMenus(menus) {
|
|
711
788
|
if (!Array.isArray(menus) || !menus.length) {
|