@jbrowse/app-core 5.0.0-beta.11 → 5.0.0-beta.14

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 (78) hide show
  1. package/esm/AppSession/AppSessionMixin.d.ts +110 -92
  2. package/esm/Assemblies/AssembliesMixin.d.ts +30 -0
  3. package/esm/HistoryManagement/index.d.ts +2 -0
  4. package/esm/HistoryManagement/index.js +2 -0
  5. package/esm/JBrowseConfig/RootConfiguration.d.ts +4 -10
  6. package/esm/JBrowseConfig/RootConfiguration.js +5 -8
  7. package/esm/JBrowseConfig/index.d.ts +3 -11
  8. package/esm/JBrowseConfig/index.js +2 -3
  9. package/esm/JBrowseModel/index.d.ts +10 -9
  10. package/esm/JBrowseModel/index.js +8 -7
  11. package/esm/JbApi/jbApi.d.ts +2 -2
  12. package/esm/JbApi/jbApi.js +16 -11
  13. package/esm/RootMenu/index.d.ts +2 -0
  14. package/esm/RootMenu/index.js +2 -0
  15. package/esm/SessionSpec/index.d.ts +2 -2
  16. package/esm/SessionSpec/index.js +1 -1
  17. package/esm/SessionSpec/loadSessionSpec.js +2 -12
  18. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +19 -0
  19. package/esm/SessionSpec/parseSessionSpecUrl.js +29 -0
  20. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +0 -10
  21. package/esm/WorkspaceLayout/LayoutRenderer.js +34 -101
  22. package/esm/WorkspaceLayout/PanelView.js +19 -45
  23. package/esm/WorkspaceLayout/TabStrip.d.ts +2 -2
  24. package/esm/WorkspaceLayout/TabStrip.js +36 -104
  25. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +0 -9
  26. package/esm/WorkspaceLayout/WorkspaceContainer.js +9 -42
  27. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +1 -16
  28. package/esm/WorkspaceLayout/WorkspacePanelActions.js +46 -51
  29. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +3 -19
  30. package/esm/WorkspaceLayout/WorkspaceTab.js +12 -48
  31. package/esm/WorkspaceLayout/dropZone.d.ts +7 -37
  32. package/esm/WorkspaceLayout/dropZone.js +7 -44
  33. package/esm/WorkspaceLayout/model.d.ts +45 -120
  34. package/esm/WorkspaceLayout/model.js +145 -303
  35. package/esm/WorkspaceLayout/panelChrome.d.ts +4 -7
  36. package/esm/WorkspaceLayout/panelChrome.js +0 -2
  37. package/esm/WorkspaceLayout/spec.d.ts +10 -51
  38. package/esm/WorkspaceLayout/spec.js +12 -80
  39. package/esm/WorkspaceLayout/splitter.d.ts +3 -19
  40. package/esm/WorkspaceLayout/splitter.js +5 -21
  41. package/esm/WorkspaceLayout/tabName.d.ts +8 -0
  42. package/esm/WorkspaceLayout/tabName.js +14 -0
  43. package/esm/WorkspaceLayout/tree.d.ts +19 -79
  44. package/esm/WorkspaceLayout/tree.js +34 -131
  45. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +4 -22
  46. package/esm/WorkspaceLayout/useLayoutDrag.js +55 -177
  47. package/esm/WorkspaceLayout/usePointerGesture.d.ts +24 -0
  48. package/esm/WorkspaceLayout/usePointerGesture.js +83 -0
  49. package/esm/WorkspaceLayout/workspaceTheme.d.ts +27 -0
  50. package/esm/WorkspaceLayout/workspaceTheme.js +37 -0
  51. package/esm/index.d.ts +5 -3
  52. package/esm/index.js +3 -2
  53. package/esm/queryParams.d.ts +2 -0
  54. package/esm/queryParams.js +16 -5
  55. package/esm/sessionSummary.d.ts +16 -0
  56. package/esm/sessionSummary.js +57 -0
  57. package/esm/ui/App/App.js +3 -0
  58. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +3 -2
  59. package/esm/ui/App/DrawerHeaderHelpDialog.js +4 -2
  60. package/esm/ui/App/JBrowseTabMenu.d.ts +3 -2
  61. package/esm/ui/App/JBrowseTabMenu.js +5 -3
  62. package/esm/ui/App/ThemeModeButton.d.ts +4 -5
  63. package/esm/ui/App/ThemeModeButton.js +22 -14
  64. package/esm/ui/App/ViewContainer.js +7 -1
  65. package/esm/ui/App/ViewMenu.js +69 -42
  66. package/esm/ui/App/ViewWrapper.js +2 -2
  67. package/esm/ui/App/ViewsContainer.js +3 -4
  68. package/esm/ui/App/lazyParts.d.ts +0 -5
  69. package/esm/ui/App/lazyParts.js +0 -1
  70. package/esm/ui/App/preloadAppShell.js +3 -6
  71. package/esm/ui/App/types.d.ts +3 -2
  72. package/esm/ui/App/viewTitle.d.ts +6 -1
  73. package/esm/ui/App/viewTitle.js +7 -2
  74. package/package.json +7 -7
  75. package/esm/WorkspaceLayout/dockviewTheme.d.ts +0 -69
  76. package/esm/WorkspaceLayout/dockviewTheme.js +0 -82
  77. package/esm/ui/App/ClassicViewsContainer.d.ts +0 -6
  78. package/esm/ui/App/ClassicViewsContainer.js +0 -22
@@ -1,15 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, useCallback, useRef } from 'react';
2
+ import { Fragment } from 'react';
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
4
  import { observer } from 'mobx-react';
5
5
  import { PanelView } from './PanelView.js';
6
- import { dv } from './dockviewTheme.js';
7
6
  import { pairSpan, withBoundaryAt } from './splitter.js';
8
7
  import { isBranch } from './tree.js';
8
+ import { usePointerGesture } from './usePointerGesture.js';
9
+ import { workspaceTheme } from './workspaceTheme.js';
9
10
  /**
10
- * A node's share of its parent's space — the whole grid engine. The `min*: 0`
11
- * are what stop a flex item refusing to shrink below its content; without them
12
- * a wide view pushes its own cell past its share.
11
+ * A node's share of its parent's space, as `flex-grow`, so window resizes need
12
+ * no code. `min*: 0` stops a wide view pushing its cell past its share.
13
13
  */
14
14
  function paneStyle(size) {
15
15
  return {
@@ -27,12 +27,7 @@ export const LayoutRenderer = observer(function LayoutRenderer(props) {
27
27
  }
28
28
  return (_jsx("div", { style: { ...paneStyle(node.size), flexDirection: node.direction }, children: node.children.map((child, i) => (_jsxs(Fragment, { children: [i > 0 && _jsx(Splitter, { branch: node, index: i, layout: layout }), _jsx(LayoutRenderer, { ...props, node: child })] }, child.id))) }));
29
29
  });
30
- /**
31
- * The pixels the two panes either side of a handle occupy. Read from the DOM
32
- * because sizes are shares of a container whose width only the browser knows.
33
- * The panes are the handle's siblings minus the other handles, which is what
34
- * `data-splitter` marks.
35
- */
30
+ /** The pixels the two panes either side of a handle occupy. */
36
31
  function measurePairPx(handle, index, horizontal) {
37
32
  const panes = [...(handle.parentElement?.children ?? [])].filter(el => !Object.hasOwn(el.dataset, 'splitter'));
38
33
  const before = panes[index - 1]?.getBoundingClientRect();
@@ -42,25 +37,21 @@ function measurePairPx(handle, index, horizontal) {
42
37
  }
43
38
  return horizontal ? before.width + after.width : before.height + after.height;
44
39
  }
45
- // dockview's sash is a transparent grab strip with a 1px separator line drawn
46
- // down the middle of it — the line is what you see, the 4px is what you can
47
- // hit. Its dark theme deliberately gives the sash no hover colour at all.
40
+ // a transparent grab strip with a 1px line drawn down its middle
48
41
  const useSplitterStyles = makeStyles()({
49
42
  splitter: {
50
- flex: `0 0 ${dv.sashSize}px`,
43
+ flex: `0 0 ${workspaceTheme.splitterSize}px`,
51
44
  position: 'relative',
52
45
  background: 'transparent',
53
46
  touchAction: 'none',
54
- // it is focusable, so it has to show focus — dockview's sash deliberately
55
- // has no HOVER colour, which is a different thing and still holds
56
47
  '&:focus-visible': {
57
- outline: `2px solid ${dv.edgeDockIndicatorColor}`,
48
+ outline: `2px solid ${workspaceTheme.accent}`,
58
49
  outlineOffset: -1,
59
50
  },
60
51
  '&::before': {
61
52
  content: '""',
62
53
  position: 'absolute',
63
- background: dv.separatorBorder,
54
+ background: workspaceTheme.splitterLine,
64
55
  },
65
56
  },
66
57
  horizontal: {
@@ -71,86 +62,35 @@ const useSplitterStyles = makeStyles()({
71
62
  },
72
63
  });
73
64
  /**
74
- * Drags the boundary between children `index - 1` and `index`.
75
- *
76
- * The DOM half only: it measures, and hands position and pair span to
77
- * `splitter.ts`, which decides where the boundary is allowed to land. Same
78
- * split as the drag — geometry pure, wiring dumb.
65
+ * Drags the boundary between children `index - 1` and `index`; `splitter.ts`
66
+ * decides where it may land.
79
67
  */
80
68
  const Splitter = observer(function Splitter({ branch, index, layout, }) {
81
69
  const { classes, cx } = useSplitterStyles();
82
- const dragRef = useRef(undefined);
83
70
  const horizontal = branch.direction === 'row';
84
- /**
85
- * The same three rules the tab drag owns (see `useLayoutDrag`), on the handle
86
- * that was written before they were written down.
87
- *
88
- * A resize starts on the PRIMARY button of the primary pointer. A right-press
89
- * armed the drag and then lost its `pointerup` to the native context menu,
90
- * and capture routes every later move back here — so a button-less pointer
91
- * went on resizing from the start position the right-press recorded.
92
- */
93
- const onPointerDown = useCallback((event) => {
94
- if (event.button !== 0 || !event.isPrimary) {
95
- return;
96
- }
97
- const pairPx = measurePairPx(event.currentTarget, index, horizontal);
98
- if (pairPx <= 0) {
99
- return;
100
- }
101
- dragRef.current = {
102
- pointerId: event.pointerId,
103
- axis: horizontal ? 'clientX' : 'clientY',
104
- start: horizontal ? event.clientX : event.clientY,
105
- pairPx,
106
- startSizes: branch.children.map(c => c.size),
107
- };
108
- event.currentTarget.setPointerCapture(event.pointerId);
109
- }, [branch, index, horizontal]);
110
- // One `pointerId` per gesture: a second finger landing on the handle steered
111
- // the first one's resize from the first one's start position, and its release
112
- // ended the gesture the first one was still holding.
113
- const onPointerMove = useCallback((event) => {
114
- const drag = dragRef.current;
115
- if (drag?.pointerId !== event.pointerId) {
116
- return;
117
- }
118
- // the pointer's travel as a fraction of the pair's pixels, applied to the
119
- // pair's share — so the handle tracks the pointer whatever the units are
120
- const delta = (event[drag.axis] - drag.start) / drag.pairPx;
121
- const sizes = drag.startSizes;
122
- layout.setSizes(branch.id, withBoundaryAt(sizes, index, sizes[index - 1] + delta * pairSpan(sizes, index), drag.pairPx));
123
- }, [branch.id, index, layout]);
124
- const endDrag = useCallback(() => {
125
- dragRef.current = undefined;
126
- }, []);
127
- const onPointerUp = useCallback((event) => {
128
- if (dragRef.current && dragRef.current.pointerId !== event.pointerId) {
129
- return;
130
- }
131
- endDrag();
132
- event.currentTarget.releasePointerCapture(event.pointerId);
133
- }, [endDrag]);
134
- // `pointercancel` ends the gesture — a touch long-press opens the platform's
135
- // context menu and cancels the pointer with no `pointerup` behind it. A
136
- // browser also fires `lostpointercapture` for that, so this is the rule
137
- // stated where it belongs rather than inferred from a second event.
138
- const onPointerCancel = useCallback((event) => {
139
- if (dragRef.current?.pointerId === event.pointerId) {
140
- endDrag();
141
- }
142
- }, [endDrag]);
143
- // A `separator` that can be moved is focusable and takes the arrow keys; one
144
- // that cannot is just decoration and should not claim the role. This one
145
- // claimed it while being neither focusable nor operable, which announces an
146
- // affordance to a screen reader that its user then cannot find.
147
- const onKeyDown = useCallback((event) => {
71
+ const gesture = usePointerGesture({
72
+ start(event) {
73
+ const pairPx = measurePairPx(event.currentTarget, index, horizontal);
74
+ return pairPx > 0
75
+ ? {
76
+ axis: horizontal ? 'clientX' : 'clientY',
77
+ start: horizontal ? event.clientX : event.clientY,
78
+ pairPx,
79
+ startSizes: branch.children.map(c => c.size),
80
+ }
81
+ : undefined;
82
+ },
83
+ move(drag, event) {
84
+ const delta = (event[drag.axis] - drag.start) / drag.pairPx;
85
+ const sizes = drag.startSizes;
86
+ layout.setSizes(branch.id, withBoundaryAt(sizes, index, sizes[index - 1] + delta * pairSpan(sizes, index), drag.pairPx));
87
+ },
88
+ });
89
+ const onKeyDown = (event) => {
148
90
  const sizes = branch.children.map(c => c.size);
149
91
  const pair = pairSpan(sizes, index);
150
92
  const decrease = horizontal ? 'ArrowLeft' : 'ArrowUp';
151
93
  const increase = horizontal ? 'ArrowRight' : 'ArrowDown';
152
- // 2% of the PAIR per press, matching the drag: the boundary moves within
153
- // the two panes either side of it and every other pane holds still
154
94
  const step = pair * 0.02;
155
95
  let moved;
156
96
  if (event.key === decrease) {
@@ -169,17 +109,10 @@ const Splitter = observer(function Splitter({ branch, index, layout, }) {
169
109
  return;
170
110
  }
171
111
  event.preventDefault();
172
- // Home/End mean "as far as this goes", which is the minimum rather than
173
- // zero — the same stop the drag hits
174
112
  layout.setSizes(branch.id, withBoundaryAt(sizes, index, moved, measurePairPx(event.currentTarget, index, horizontal)));
175
- }, [branch, index, horizontal, layout]);
176
- // the pane BEFORE the handle, as a percentage of the pair it divides — which
177
- // is what the handle actually moves
113
+ };
114
+ // the pane before the handle, as a percentage of the pair it divides
178
115
  const sizes = branch.children.map(c => c.size);
179
116
  const valueNow = Math.round((sizes[index - 1] / pairSpan(sizes, index)) * 100);
180
- return (_jsx("div", { "data-splitter": true, role: "separator", tabIndex: 0, "aria-orientation": horizontal ? 'vertical' : 'horizontal', "aria-label": horizontal ? 'Resize panels' : 'Resize rows', "aria-valuenow": valueNow, "aria-valuemin": 0, "aria-valuemax": 100, onKeyDown: onKeyDown, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel,
181
- // capture can end without a pointerup — the browser drops it if the
182
- // element is removed, and then the next pointer that merely passes over
183
- // the handle would go on resizing from the stale start sizes
184
- onLostPointerCapture: endDrag, className: cx(classes.splitter, horizontal ? classes.horizontal : classes.vertical), style: { cursor: horizontal ? 'ew-resize' : 'ns-resize' } }));
117
+ return (_jsx("div", { "data-splitter": true, role: "separator", tabIndex: 0, "aria-orientation": horizontal ? 'vertical' : 'horizontal', "aria-label": horizontal ? 'Resize panels' : 'Resize rows', "aria-valuenow": valueNow, "aria-valuemin": 0, "aria-valuemax": 100, onKeyDown: onKeyDown, onPointerDown: gesture.onPointerDown, onPointerMove: gesture.onPointerMove, onPointerUp: gesture.onPointerUp, onPointerCancel: gesture.onPointerCancel, onLostPointerCapture: gesture.onLostPointerCapture, className: cx(classes.splitter, horizontal ? classes.horizontal : classes.vertical), style: { cursor: horizontal ? 'ew-resize' : 'ns-resize' } }));
185
118
  });
@@ -4,28 +4,13 @@ import { useScrollPortHeightVar } from '@jbrowse/core/util/hooks';
4
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
5
  import { observer } from 'mobx-react';
6
6
  import { TabStrip } from './TabStrip.js';
7
- import { dv } from './dockviewTheme.js';
8
7
  import { indicatorRect } from './dropZone.js';
9
8
  import { tabDomId, tabPanelDomId } from './panelChrome.js';
10
9
  import { activeTabIn } from './tree.js';
11
- /**
12
- * One cell of the grid: a tab strip, and the content of whichever tab is
13
- * showing.
14
- *
15
- * The strip and its state are `TabStrip`; this is the frame around them, and
16
- * has no state of its own — it is a function of its node, which is the whole
17
- * point of the layout being one MST tree.
18
- *
19
- * The line between chrome and content is the strip's lower edge: everything
20
- * above it is dockview's dark theme in either JBrowse theme (see
21
- * `dockviewTheme.ts`), everything below it follows the app's.
22
- */
10
+ import { workspaceTheme } from './workspaceTheme.js';
23
11
  const useStyles = makeStyles()(theme => ({
24
- // `flex: 1` and `minWidth: 0` are load-bearing, not tidiness. This is a child
25
- // of a `display: flex` row, so without them its width is its CONTENT's width
26
- // — and a view measures its container to decide how wide to draw
27
- // (useWidthSetter), so the two settle at the view's intrinsic width and the
28
- // panel renders at half the window with dead space beside it.
12
+ // without `flex: 1` and `minWidth: 0` the cell takes its content's width, and
13
+ // a view sizes itself to its container, so both settle too narrow
29
14
  panel: {
30
15
  position: 'relative',
31
16
  display: 'flex',
@@ -33,9 +18,7 @@ const useStyles = makeStyles()(theme => ({
33
18
  flex: 1,
34
19
  minWidth: 0,
35
20
  minHeight: 0,
36
- // the surface the views sit on, so it is the app's background and not
37
- // dockview's — see `dockviewTheme.ts`. A cell is taller than its views
38
- // whenever they don't fill it, so this is most of what a panel shows.
21
+ // below the strip the cell follows the app theme, not `workspaceTheme`
39
22
  background: theme.palette.background.default,
40
23
  },
41
24
  content: {
@@ -48,51 +31,42 @@ const useStyles = makeStyles()(theme => ({
48
31
  indicator: {
49
32
  position: 'absolute',
50
33
  pointerEvents: 'none',
51
- // above this panel's own content and nothing else — the panel is
52
- // position:relative, so an app-wide z-index would let it cover menus
34
+ // local to the position:relative panel, so it cannot cover menus
53
35
  zIndex: 1,
54
- background: dv.dragOverBackground,
55
- outline: `1px solid ${dv.edgeDockIndicatorColor}`,
36
+ background: workspaceTheme.dropWash,
37
+ outline: `1px solid ${workspaceTheme.accent}`,
56
38
  },
57
- // where a tab dragged onto the strip would land
58
39
  caret: {
59
40
  position: 'absolute',
60
41
  pointerEvents: 'none',
61
42
  zIndex: 1,
62
43
  top: 0,
63
44
  width: 2,
64
- height: dv.tabsHeight,
65
- background: dv.edgeDockIndicatorColor,
45
+ height: workspaceTheme.stripHeight,
46
+ background: workspaceTheme.accent,
66
47
  },
67
48
  }));
68
49
  export const PanelView = observer(function PanelView({ panel, layout, chrome, drop, }) {
69
50
  const { classes } = useStyles();
70
51
  const active = activeTabIn(panel);
71
- const contentRef = useScrollPortHeightVar();
72
52
  return (_jsxs("div", { "data-panel-id": panel.id, className: classes.panel, onPointerDownCapture: () => {
73
- // clicking anywhere in a cell makes it the one a new view lands in.
74
- // Capture, so it still registers when the click is consumed by a
75
- // control inside the view.
53
+ // capture phase, so a control inside the view that consumes the click
54
+ // still activates the cell
76
55
  if (layout.activePanelId !== panel.id) {
77
56
  layout.setActivePanelId(panel.id);
78
57
  }
79
- }, children: [_jsx(TabStrip, { panel: panel, layout: layout, chrome: chrome, active: active, groupActive: layout.activePanelId === panel.id }), _jsx("div", { role: "tabpanel", id: tabPanelDomId(panel.id), "aria-labelledby": active ? tabDomId(active.id) : undefined, className: classes.content, ref: contentRef, children: active ? chrome.renderTabContent(active) : null }), _jsx(DropIndicator, { drop: drop, classes: classes })] }));
58
+ }, children: [layout.panels.length > 1 || panel.tabs.length > 1 ? (_jsx(TabStrip, { panel: panel, layout: layout, chrome: chrome, active: active, cellActive: layout.activePanelId === panel.id })) : null, active ? (_jsx(TabPanel, { panelId: panel.id, tab: active, chrome: chrome, className: classes.content }, active.id)) : (_jsx("div", { role: "tabpanel", className: classes.content })), _jsx(DropIndicator, { drop: drop, classes: classes })] }));
80
59
  });
81
- /**
82
- * Where a drop would land, drawn over the cell.
83
- *
84
- * A strip drop gets a caret at the gap rather than a wash over half the cell:
85
- * it is a position in the tab order, and shading half the panel would say
86
- * "split this cell", which is the wrong thing.
87
- */
60
+ // keyed by tab, so a switch gets a fresh ViewStack and scroll port
61
+ const TabPanel = observer(function TabPanel({ panelId, tab, chrome, className, }) {
62
+ const ref = useScrollPortHeightVar();
63
+ return (_jsx("div", { role: "tabpanel", id: tabPanelDomId(panelId), "aria-labelledby": tabDomId(tab.id), className: className, ref: ref, children: chrome.renderTabContent(tab) }));
64
+ });
65
+ /** Where a drop would land: a caret at a strip gap, or a wash over a zone. */
88
66
  const DropIndicator = observer(function DropIndicator({ drop, classes, }) {
89
67
  if (drop?.strip) {
90
68
  return (_jsx("div", { "data-drop-caret": drop.strip.index, className: classes.caret,
91
- // A scrolled strip's left-most gap has a negative panel-relative x, and
92
- // the panel has no `overflow: hidden` — the caret drew into the
93
- // neighbour. Clamped here because `stripDropAt` is deliberately
94
- // ignorant of which coordinate space it was handed, so it cannot know
95
- // where zero is.
69
+ // a scrolled strip's first gap is left of the panel, which does not clip
96
70
  style: { left: Math.max(0, drop.strip.left) } }));
97
71
  }
98
72
  return drop ? (_jsx("div", { "data-drop-indicator": drop.zone, className: classes.indicator, style: indicatorRect(drop.zone) })) : null;
@@ -8,7 +8,7 @@ interface TabStripProps {
8
8
  /** the tab this panel is showing — the strip does not decide it */
9
9
  active: TabNode | undefined;
10
10
  /** whether this is the cell a newly launched view lands in */
11
- groupActive: boolean;
11
+ cellActive: boolean;
12
12
  }
13
- export declare const TabStrip: ({ panel, layout, chrome, active, groupActive, }: TabStripProps) => import("react").JSX.Element;
13
+ export declare const TabStrip: ({ panel, layout, chrome, active, cellActive, }: TabStripProps) => import("react").JSX.Element;
14
14
  export {};
@@ -2,37 +2,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
4
  import { observer } from 'mobx-react';
5
- import { dv, tabColors } from './dockviewTheme.js';
6
5
  import { tabDomId, tabPanelDomId } from './panelChrome.js';
7
- /**
8
- * A panel's tab strip: the tabs, and the panel's own buttons beside them.
9
- *
10
- * Everything with state in a cell lives here — the roving tabindex, the wheel
11
- * translation, the scroll-into-view — which leaves `PanelView` a pure function
12
- * of its node. The strip is dockview's dark theme, transcribed in
13
- * `dockviewTheme.ts`; see there for why it is fixed rather than derived from
14
- * the MUI theme.
15
- */
6
+ import { tabColors, workspaceTheme } from './workspaceTheme.js';
16
7
  const useStyles = makeStyles()({
17
8
  strip: {
18
9
  display: 'flex',
19
10
  flexShrink: 0,
20
11
  boxSizing: 'border-box',
21
- height: dv.tabsHeight,
22
- fontSize: dv.tabsFontSize,
23
- background: dv.tabsBackground,
12
+ height: workspaceTheme.stripHeight,
13
+ fontSize: workspaceTheme.stripFontSize,
14
+ background: workspaceTheme.stripBackground,
24
15
  },
25
16
  tabs: {
26
17
  display: 'flex',
27
18
  overflowX: 'auto',
28
- // shrink but never GROW: the panel actions are the next child of the strip,
29
- // so a tab list that takes the leftover space pushes the `+` to the far
30
- // right edge, arbitrarily far from the tab it acts on. `0 1 auto` sizes the
31
- // list to its tabs and leaves the `+` beside the last one, and the shrink
32
- // half is what makes it scroll here rather than push the `+` off the strip.
19
+ // never grow, so the `+` after the list sits beside the last tab
33
20
  flex: '0 1 auto',
34
21
  minWidth: 0,
35
- // the strip is chrome, so a scrollbar across it would be noise
36
22
  scrollbarWidth: 'none',
37
23
  '&::-webkit-scrollbar': { display: 'none' },
38
24
  },
@@ -46,31 +32,16 @@ const useStyles = makeStyles()({
46
32
  cursor: 'pointer',
47
33
  userSelect: 'none',
48
34
  touchAction: 'none',
49
- borderRight: `1px solid ${dv.tabDividerColor}`,
35
+ borderRight: `1px solid ${workspaceTheme.tabDivider}`,
50
36
  '&:hover .jbrowse-tab-menu': { visibility: 'visible' },
51
- // a keyboard user needs the ⋮ too, and hover is not a thing they can do
52
37
  '&:focus-within .jbrowse-tab-menu': { visibility: 'visible' },
53
- // the strip is dark in either theme, so the focus ring is the drop
54
- // indicator's blue rather than the UA default black-on-dark
55
38
  '&:focus-visible': {
56
- outline: `2px solid ${dv.edgeDockIndicatorColor}`,
39
+ outline: `2px solid ${workspaceTheme.accent}`,
57
40
  outlineOffset: -2,
58
41
  },
59
42
  },
60
43
  });
61
- /**
62
- * A wheel event's delta in PIXELS.
63
- *
64
- * `deltaY` is only pixels when `deltaMode` says so. Firefox reports whole lines
65
- * for a mouse wheel — `deltaMode: 1`, `deltaY: ±3` — where Chrome reports
66
- * `deltaMode: 0`, `deltaY: ±100`. Using the raw number moves the strip three
67
- * pixels per notch there: scrolling, technically, and unusable in practice.
68
- *
69
- * The line height is nominal rather than measured. `getComputedStyle` per wheel
70
- * event to resolve a `line-height: normal` that is itself font-dependent buys
71
- * accuracy nobody can perceive in a scroll gesture, and the strip is a fixed
72
- * 35px of chrome in one size.
73
- */
44
+ // Firefox reports a mouse wheel in lines (`deltaMode: 1`, `deltaY: ±3`)
74
45
  const WHEEL_LINE_PX = 16;
75
46
  function wheelDeltaPixels(event, pageSize) {
76
47
  switch (event.deltaMode) {
@@ -82,40 +53,23 @@ function wheelDeltaPixels(event, pageSize) {
82
53
  return event.deltaY;
83
54
  }
84
55
  }
85
- export const TabStrip = observer(function TabStrip({ panel, layout, chrome, active, groupActive, }) {
56
+ export const TabStrip = observer(function TabStrip({ panel, layout, chrome, active, cellActive, }) {
86
57
  const { classes } = useStyles();
87
58
  const { renderPanelActions } = chrome;
88
- // Roving tabindex: the strip is ONE tab stop, and the arrow keys move within
89
- // it. Focus is tracked separately from selection because activation here is
90
- // manual (see `onKeyDown`), so the two genuinely differ while arrowing.
59
+ // roving tabindex; focus differs from selection because activation is manual
91
60
  const stripRef = useRef(null);
92
61
  const [focusedTabId, setFocusedTabId] = useState(undefined);
93
62
  const roving = panel.tabs.find(t => t.id === focusedTabId)?.id ?? active?.id;
94
- // by dataset rather than an interpolated attribute selector: a tab id is
95
- // nanoid output and would need CSS.escape, which jsdom does not have
63
+ // not an attribute selector: a nanoid needs CSS.escape, which jsdom lacks
96
64
  function tabElement(tabId) {
97
65
  return [...(stripRef.current?.children ?? [])].find(child => child.dataset.tabId === tabId);
98
66
  }
99
67
  function focusTab(tabId) {
100
68
  setFocusedTabId(tabId);
101
- // focusing scrolls it into view on its own, which is the whole reason the
102
- // keyboard could always reach an overflowing strip when the mouse could not
103
69
  tabElement(tabId)?.focus();
104
70
  }
105
- /**
106
- * Keep the shown tab in view.
107
- *
108
- * The strip scrolls, so a tab can become current while sitting outside it —
109
- * `+` on a full strip appends a tab, makes it active, and leaves the user
110
- * looking at the tabs it scrolled past. Clicking never needs this (you
111
- * clicked something visible) and arrowing gets it from `focus()`, so this is
112
- * for the tab that becomes current without being touched.
113
- *
114
- * `block: 'nearest'` so it cannot scroll an ancestor vertically. jsdom has no
115
- * scrollIntoView at all, but `config/jest/scrollIntoView.js` already no-ops it
116
- * for the whole suite — don't add an optional call for that, it reads as a
117
- * browser that might not have the method.
118
- */
71
+ // for a tab made current without being touched, e.g. by `+` on a full strip;
72
+ // `block: 'nearest'` keeps it from scrolling an ancestor vertically
119
73
  useEffect(() => {
120
74
  tabElement(active?.id)?.scrollIntoView({
121
75
  block: 'nearest',
@@ -123,14 +77,9 @@ export const TabStrip = observer(function TabStrip({ panel, layout, chrome, acti
123
77
  });
124
78
  }, [active?.id]);
125
79
  /**
126
- * MANUAL activation: arrows move focus, Enter/Space selects.
127
- *
128
- * The automatic form (arrowing selects as it goes) is the more common reading
129
- * of the tabs pattern, and is wrong here for the reason WAI-ARIA names as its
130
- * exception — showing a tab is expensive. Only the selected tab's views are
131
- * mounted, and a JBrowse view costs a WebGL2 context per display against a
132
- * ceiling of 16, so arrowing across five tabs would build and tear down five
133
- * sets of them to pass through.
80
+ * Manual activation (arrows focus, Enter/Space selects), the WAI-ARIA
81
+ * exception for expensive tabs: showing one mounts views that each cost
82
+ * WebGL2 contexts.
134
83
  */
135
84
  function onKeyDown(event, tabId) {
136
85
  const ids = panel.tabs.map(t => t.id);
@@ -157,70 +106,53 @@ export const TabStrip = observer(function TabStrip({ panel, layout, chrome, acti
157
106
  }
158
107
  }
159
108
  return (_jsxs("div", { "data-tab-strip": true, className: classes.strip,
160
- // Double-clicking the strip's empty space maximizes the cell and
161
- // restores it — the IDE convention, and free here because the strip's
162
- // only other `onDoubleClick` is the one on a tab's own label, for rename.
163
- //
164
- // `target === currentTarget` is what keeps those two apart, and it is the
165
- // test rather than a `stopPropagation` in `WorkspaceTab` because it holds
166
- // for anything ever put on the strip: a rename double-click bubbles out
167
- // of the label to here, and so would a double-click on the `+`.
109
+ // empty strip space only, so a tab's rename double-click does not bubble
110
+ // into a maximize
168
111
  onDoubleClick: event => {
169
112
  if (event.target === event.currentTarget) {
170
113
  layout.toggleMaximizedPanel(panel.id);
171
114
  }
172
- }, children: [_jsx("div", { role: "tablist", ref: stripRef, className: classes.tabs,
173
- // A mouse wheel only has a vertical axis, and this scrolls
174
- // horizontally — so without translating it, a strip with more tabs
175
- // than fit is reachable by trackpad swipe and by keyboard and NOT AT
176
- // ALL by mouse. The scrollbar is hidden on purpose (the strip is
177
- // chrome) which removes the other way of noticing there is more.
178
- //
179
- // `deltaX` is a trackpad's own horizontal axis and the browser has
180
- // already applied it; taking the larger axis leaves that gesture alone
181
- // rather than doubling it.
115
+ }, children: [_jsx("div", { role: "tablist", ref: stripRef, className: classes.tabs, onBlur: event => {
116
+ if (!event.currentTarget.contains(event.relatedTarget)) {
117
+ setFocusedTabId(undefined);
118
+ }
119
+ },
120
+ // translate a vertical wheel to horizontal scroll; a trackpad's larger
121
+ // `deltaX` is already applied by the browser
182
122
  onWheel: event => {
183
123
  const el = stripRef.current;
184
124
  if (!el || Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
185
125
  return;
186
126
  }
187
127
  el.scrollLeft += wheelDeltaPixels(event, el.clientWidth);
188
- }, children: panel.tabs.map(tab => (_jsx(Tab, { tab: tab, panel: panel, layout: layout, chrome: chrome, className: classes.tab, selected: tab.id === active?.id, groupActive: groupActive, tabIndex: tab.id === roving ? 0 : -1, onKeyDown: onKeyDown, onFocus: setFocusedTabId }, tab.id))) }), renderPanelActions?.(panel)] }));
128
+ }, children: panel.tabs.map(tab => (_jsx(Tab, { tab: tab, panel: panel, layout: layout, chrome: chrome, className: classes.tab, selected: tab.id === active?.id, cellActive: cellActive, tabIndex: tab.id === roving ? 0 : -1, onKeyDown: onKeyDown, onFocus: setFocusedTabId }, tab.id))) }), renderPanelActions?.(panel)] }));
189
129
  });
190
- const Tab = observer(function Tab({ tab, panel, layout, chrome, className, selected, groupActive, tabIndex, onKeyDown, onFocus, }) {
130
+ const Tab = observer(function Tab({ tab, panel, layout, chrome, className, selected, cellActive, tabIndex, onKeyDown, onFocus, }) {
191
131
  const { renderTabLabel, dragHandlers, onTabClose } = chrome;
192
132
  return (_jsx("div", { role: "tab", id: tabDomId(tab.id), "data-tab-id": tab.id, "aria-selected": selected, "aria-controls": selected ? tabPanelDomId(panel.id) : undefined, tabIndex: tabIndex, onKeyDown: event => {
193
- onKeyDown(event, tab.id);
133
+ // keys from a control inside the tab belong to that control
134
+ if (event.target === event.currentTarget) {
135
+ onKeyDown(event, tab.id);
136
+ }
194
137
  }, onFocus: () => {
195
138
  onFocus(tab.id);
196
139
  }, onPointerDown: event => {
197
- // The middle button closes rather than drags, and its default action is
198
- // the browser's autoscroll — which would otherwise start the moment a
199
- // tab is middle-pressed.
200
- //
201
- // Cancelling pointerdown suppresses the compatibility mouse events
202
- // (which is what stops the autoscroll) and NOT `click` / `auxclick`:
203
- // Pointer Events L3 dispatches those two independently of that mapping.
204
- // So the close below still fires, which is the thing this looks like it
205
- // would break.
140
+ // Stops middle-button autoscroll; Pointer Events L3 still dispatches
141
+ // the `auxclick` that closes the tab
206
142
  if (event.button === 1) {
207
143
  event.preventDefault();
208
144
  return;
209
145
  }
210
- // Showing a tab is expensive — it mounts a stack of views, each display
211
- // costing a WebGL2 context — so it is the LEFT button that asks for it,
212
- // the same gate dockview puts on `_activateOnPointerDown`. A right-press
213
- // is on its way to a context menu and is not a request to look at
214
- // anything.
146
+ // only the left button activates: showing a tab mounts views
215
147
  if (event.button !== 0) {
216
148
  return;
217
149
  }
218
150
  layout.setActiveTab(panel.id, tab.id);
219
- dragHandlers.onTabPointerDown(tab.id, event);
151
+ dragHandlers.onPointerDown(event);
220
152
  }, onAuxClick: event => {
221
153
  if (event.button === 1) {
222
154
  event.preventDefault();
223
155
  onTabClose?.(tab.id);
224
156
  }
225
- }, onPointerMove: dragHandlers.onTabPointerMove, onPointerUp: dragHandlers.onTabPointerUp, onPointerCancel: dragHandlers.onTabPointerCancel, className: className, style: tabColors(groupActive, selected), children: renderTabLabel(tab) }));
157
+ }, onPointerMove: dragHandlers.onPointerMove, onPointerUp: dragHandlers.onPointerUp, onPointerCancel: dragHandlers.onPointerCancel, onLostPointerCapture: dragHandlers.onLostPointerCapture, className: className, style: tabColors(cellActive, selected), children: renderTabLabel(tab) }));
226
158
  });
@@ -1,15 +1,6 @@
1
1
  import type { WorkspaceSessionType } from '../ui/App/types.ts';
2
2
  import type { WorkspaceLayout } from './model.ts';
3
3
  type WorkspaceSession = WorkspaceSessionType & WorkspaceLayout;
4
- /**
5
- * The workspace. Compare `TiledViewsContainer` + `useDockviewController`, which
6
- * this replaces: there is no api to hold, no `onReady`, no event to subscribe
7
- * to, and no reconciliation — the layout is session state and this renders it.
8
- *
9
- * The one reaction left is homing: `session.views` is owned by the session, so
10
- * a view launched from a menu arrives belonging to no tab and has to land
11
- * somewhere. That is one-directional and idempotent, and nothing reads back.
12
- */
13
4
  export declare const WorkspaceContainer: ({ session, }: {
14
5
  session: WorkspaceSession;
15
6
  }) => import("react").JSX.Element;