@jbrowse/app-core 4.2.1 → 5.0.0-beta.1

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 (154) hide show
  1. package/esm/AppSession/AppRootModel.d.ts +30 -0
  2. package/esm/AppSession/AppRootModel.js +1 -0
  3. package/esm/AppSession/AppSessionMixin.d.ts +1598 -0
  4. package/esm/AppSession/AppSessionMixin.js +62 -0
  5. package/esm/AppSession/index.d.ts +2 -0
  6. package/esm/AppSession/index.js +1 -0
  7. package/esm/Assemblies/AssembliesMixin.d.ts +225 -0
  8. package/esm/Assemblies/AssembliesMixin.js +149 -0
  9. package/esm/Assemblies/index.d.ts +1 -2
  10. package/esm/Assemblies/index.js +1 -2
  11. package/esm/HistoryManagement/index.d.ts +19 -0
  12. package/esm/HistoryManagement/index.js +42 -5
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +167 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +85 -7
  15. package/esm/JBrowseConfig/index.d.ts +169 -84
  16. package/esm/JBrowseConfig/index.js +62 -2
  17. package/esm/JBrowseModel/index.d.ts +176 -95
  18. package/esm/JBrowseModel/index.js +122 -21
  19. package/esm/RootMenu/index.d.ts +77 -2
  20. package/esm/RootMenu/index.js +107 -61
  21. package/esm/SessionSpec/index.d.ts +6 -0
  22. package/esm/SessionSpec/index.js +3 -0
  23. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  24. package/esm/SessionSpec/lgvUrlInit.js +158 -0
  25. package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
  26. package/esm/SessionSpec/loadSessionSpec.js +311 -0
  27. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
  28. package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
  29. package/esm/SessionSpec/types.d.ts +33 -0
  30. package/esm/SessionSpec/types.js +1 -0
  31. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  32. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  33. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  34. package/esm/WorkspaceLayout/PanelView.js +99 -0
  35. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  36. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  37. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  38. package/esm/WorkspaceLayout/WorkspaceContainer.js +105 -0
  39. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  40. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  41. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  42. package/esm/WorkspaceLayout/WorkspaceTab.js +109 -0
  43. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  44. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  45. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  46. package/esm/WorkspaceLayout/dropZone.js +113 -0
  47. package/esm/WorkspaceLayout/index.d.ts +8 -0
  48. package/esm/WorkspaceLayout/index.js +4 -0
  49. package/esm/WorkspaceLayout/model.d.ts +240 -0
  50. package/esm/WorkspaceLayout/model.js +527 -0
  51. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  52. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  53. package/esm/WorkspaceLayout/spec.d.ts +58 -0
  54. package/esm/WorkspaceLayout/spec.js +153 -0
  55. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  56. package/esm/WorkspaceLayout/splitter.js +37 -0
  57. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  58. package/esm/WorkspaceLayout/tree.js +379 -0
  59. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  60. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  61. package/esm/index.d.ts +16 -9
  62. package/esm/index.js +13 -9
  63. package/esm/menus.d.ts +63 -62
  64. package/esm/menus.js +131 -84
  65. package/esm/queryParams.d.ts +9 -0
  66. package/esm/queryParams.js +92 -0
  67. package/esm/sessionModelFactory.d.ts +18 -0
  68. package/esm/sessionModelFactory.js +1 -0
  69. package/esm/ui/App/App.d.ts +3 -14
  70. package/esm/ui/App/App.js +30 -17
  71. package/esm/ui/App/AppFab.d.ts +12 -3
  72. package/esm/ui/App/AppFab.js +15 -8
  73. package/esm/ui/App/AppReadyMarker.d.ts +34 -0
  74. package/esm/ui/App/AppReadyMarker.js +56 -0
  75. package/esm/ui/App/AppToolbar.d.ts +2 -14
  76. package/esm/ui/App/AppToolbar.js +23 -24
  77. package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
  78. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  79. package/esm/ui/App/DialogQueue.d.ts +17 -1
  80. package/esm/ui/App/DialogQueue.js +30 -1
  81. package/esm/ui/App/Drawer.d.ts +1 -1
  82. package/esm/ui/App/Drawer.js +10 -39
  83. package/esm/ui/App/DrawerControls.d.ts +1 -5
  84. package/esm/ui/App/DrawerControls.js +1 -25
  85. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  86. package/esm/ui/App/DrawerHeader.js +24 -10
  87. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  88. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  89. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  90. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  91. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  92. package/esm/ui/App/DrawerWidget.js +9 -21
  93. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  94. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  95. package/esm/ui/App/ViewButtons.d.ts +2 -4
  96. package/esm/ui/App/ViewButtons.js +15 -3
  97. package/esm/ui/App/ViewContainer.d.ts +3 -2
  98. package/esm/ui/App/ViewContainer.js +144 -24
  99. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  100. package/esm/ui/App/ViewContainerTitle.js +9 -2
  101. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  102. package/esm/ui/App/ViewErrorFallback.js +42 -0
  103. package/esm/ui/App/ViewHeader.d.ts +3 -4
  104. package/esm/ui/App/ViewHeader.js +28 -17
  105. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  106. package/esm/ui/App/ViewLauncher.js +7 -5
  107. package/esm/ui/App/ViewMenu.d.ts +3 -5
  108. package/esm/ui/App/ViewMenu.js +87 -92
  109. package/esm/ui/App/ViewStack.d.ts +7 -0
  110. package/esm/ui/App/ViewStack.js +42 -0
  111. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  112. package/esm/ui/App/ViewWrapper.js +26 -10
  113. package/esm/ui/App/ViewsContainer.d.ts +3 -8
  114. package/esm/ui/App/ViewsContainer.js +9 -6
  115. package/esm/ui/App/copyView.js +1 -1
  116. package/esm/ui/App/index.d.ts +2 -1
  117. package/esm/ui/App/index.js +3 -2
  118. package/esm/ui/App/lazyChunk.d.ts +13 -0
  119. package/esm/ui/App/lazyChunk.js +91 -0
  120. package/esm/ui/App/types.d.ts +12 -3
  121. package/esm/ui/App/types.js +1 -1
  122. package/esm/ui/App/useViewVisibility.d.ts +34 -0
  123. package/esm/ui/App/useViewVisibility.js +87 -0
  124. package/esm/ui/App/viewTitle.d.ts +5 -0
  125. package/esm/ui/App/viewTitle.js +9 -0
  126. package/esm/ui/index.d.ts +1 -1
  127. package/esm/ui/index.js +1 -1
  128. package/package.json +12 -17
  129. package/esm/AppFocus/index.d.ts +0 -5
  130. package/esm/AppFocus/index.js +0 -12
  131. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  132. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  133. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  134. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  135. package/esm/DockviewLayout/index.d.ts +0 -35
  136. package/esm/DockviewLayout/index.js +0 -111
  137. package/esm/ui/App/DockviewContext.d.ts +0 -22
  138. package/esm/ui/App/DockviewContext.js +0 -29
  139. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  140. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  141. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  142. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  143. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  144. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  145. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  146. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  147. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  148. package/esm/ui/App/JBrowseViewTab.js +0 -97
  149. package/esm/ui/App/ModalWidget.d.ts +0 -6
  150. package/esm/ui/App/ModalWidget.js +0 -48
  151. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  152. package/esm/ui/App/TiledViewsContainer.js +0 -366
  153. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  154. package/esm/ui/App/dockviewUtils.js +0 -55
@@ -1,16 +1,24 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
2
3
  import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton';
3
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
5
  import CloseIcon from '@mui/icons-material/Close';
5
6
  import EditIcon from '@mui/icons-material/Edit';
6
7
  import MoreVertIcon from '@mui/icons-material/MoreVert';
7
8
  const useStyles = makeStyles()({
9
+ // hidden until the tab is hovered or focused, by a rule in `TabStrip` keyed
10
+ // on the `jbrowse-tab-menu` class — hence the plain class name beside the
11
+ // generated one. `visibility` rather than `display`, so the tab does not
12
+ // change width under the pointer as it arrives.
8
13
  tabIcons: {
9
14
  display: 'flex',
10
15
  alignItems: 'center',
11
16
  visibility: 'hidden',
12
17
  },
13
- tabIconsVisible: {
18
+ // the pointer is over the menu, not the tab, for as long as the menu is open,
19
+ // so the hover rule stops applying the moment it opens — leaving the popover
20
+ // anchored to an invisible button
21
+ open: {
14
22
  visibility: 'visible',
15
23
  },
16
24
  tabIcon: {
@@ -22,9 +30,20 @@ const useStyles = makeStyles()({
22
30
  fontSize: 14,
23
31
  },
24
32
  });
25
- export default function JBrowseTabMenu({ isHovered, onRename, onClose, }) {
26
- const { classes } = useStyles();
27
- return (_jsx("div", { className: `${classes.tabIcons} ${isHovered ? classes.tabIconsVisible : ''}`, children: _jsx(CascadingMenuButton, { menuItems: [
33
+ export default function JBrowseTabMenu({ onRename, onClose, }) {
34
+ const { classes, cx } = useStyles();
35
+ const [open, setOpen] = useState(false);
36
+ return (_jsx("div", { className: cx('jbrowse-tab-menu', classes.tabIcons, open && classes.open),
37
+ // The tab strip starts a tab drag on pointerdown and takes POINTER
38
+ // CAPTURE for it, and a captured pointer retargets the compatibility
39
+ // mouse events with it — so `click` is delivered to the tab rather than
40
+ // to this button, and the menu silently never opens. Stopping the
41
+ // pointerdown here means a press that starts on the menu is not a drag
42
+ // and never captures, which is the same reason the rename input in
43
+ // `WorkspaceTab` stops it.
44
+ onPointerDown: event => {
45
+ event.stopPropagation();
46
+ }, children: _jsx(CascadingMenuButton, { menuItems: [
28
47
  {
29
48
  label: 'Rename tab',
30
49
  icon: EditIcon,
@@ -35,5 +54,5 @@ export default function JBrowseTabMenu({ isHovered, onRename, onClose, }) {
35
54
  icon: CloseIcon,
36
55
  onClick: onClose,
37
56
  },
38
- ], size: "small", className: classes.tabIcon, stopPropagation: true, children: _jsx(MoreVertIcon, { className: classes.smallIcon }) }) }));
57
+ ], size: "small", className: classes.tabIcon, setOpen: setOpen, stopPropagation: true, children: _jsx(MoreVertIcon, { className: classes.smallIcon }) }) }));
39
58
  }
@@ -1,7 +1,5 @@
1
1
  import type { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
2
- declare const ViewButtons: ({ view, onClose, onMinimize, }: {
2
+ declare const ViewButtons: ({ view, }: {
3
3
  view: IBaseViewModel;
4
- onClose: () => void;
5
- onMinimize: () => void;
6
- }) => import("react/jsx-runtime").JSX.Element;
4
+ }) => import("react").JSX.Element;
7
5
  export default ViewButtons;
@@ -1,4 +1,5 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { getSession } from '@jbrowse/core/util';
2
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
3
4
  import AddIcon from '@mui/icons-material/Add';
4
5
  import CloseIcon from '@mui/icons-material/Close';
@@ -10,8 +11,19 @@ const useStyles = makeStyles()(theme => ({
10
11
  color: theme.palette.secondary.contrastText,
11
12
  },
12
13
  }));
13
- const ViewButtons = observer(function ViewButtons({ view, onClose, onMinimize, }) {
14
+ // Acts on the view directly rather than taking onClose/onMinimize callbacks.
15
+ // Those were written inline by ViewContainer, three components up, so they were
16
+ // fresh on every one of its renders and mobx-react's memo could never hold for
17
+ // this subtree. A resize of the view body re-rendered these MUI buttons along
18
+ // with the header and the menu. There was nothing for the caller to decide
19
+ // anyway: both handlers are the same two lines wherever they are written.
20
+ const ViewButtons = observer(function ViewButtons({ view, }) {
14
21
  const { classes } = useStyles();
15
- return (_jsxs(_Fragment, { children: [_jsx(IconButton, { "data-testid": "minimize_view", onClick: onMinimize, children: view.minimized ? (_jsx(AddIcon, { className: classes.icon, fontSize: "small" })) : (_jsx(MinimizeIcon, { className: classes.icon, fontSize: "small" })) }), _jsx(IconButton, { "data-testid": "close_view", onClick: onClose, children: _jsx(CloseIcon, { className: classes.icon, fontSize: "small" }) })] }));
22
+ const session = getSession(view);
23
+ return (_jsxs(_Fragment, { children: [_jsx(IconButton, { "data-testid": "minimize_view", onClick: () => {
24
+ view.setMinimized(!view.minimized);
25
+ }, children: view.minimized ? (_jsx(AddIcon, { className: classes.icon, fontSize: "small" })) : (_jsx(MinimizeIcon, { className: classes.icon, fontSize: "small" })) }), _jsx(IconButton, { "data-testid": "close_view", onClick: () => {
26
+ session.removeView(view);
27
+ }, children: _jsx(CloseIcon, { className: classes.icon, fontSize: "small" }) })] }));
16
28
  });
17
29
  export default ViewButtons;
@@ -1,6 +1,7 @@
1
1
  import type { AbstractViewModel, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
2
- declare const ViewContainer: ({ view, session, }: {
2
+ declare const ViewContainer: ({ view, session, scrollOnMount, }: {
3
3
  view: AbstractViewModel;
4
4
  session: SessionWithFocusedViewAndDrawerWidgets;
5
- }) => import("react/jsx-runtime").JSX.Element;
5
+ scrollOnMount?: boolean;
6
+ }) => import("react").JSX.Element;
6
7
  export default ViewContainer;
@@ -1,16 +1,94 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect } from 'react';
3
- import { useWidthSetter } from '@jbrowse/core/util';
3
+ import { useFocusOnInteraction, useWidthSetter } from '@jbrowse/core/util/hooks';
4
4
  import { cx, makeStyles } from '@jbrowse/core/util/tss-react';
5
- import { Paper, useTheme } from '@mui/material';
5
+ import { isAlive } from '@jbrowse/mobx-state-tree';
6
+ import { Paper } from '@mui/material';
6
7
  import { observer } from 'mobx-react';
7
- import ViewHeader from "./ViewHeader.js";
8
- import ViewWrapper from "./ViewWrapper.js";
8
+ import ViewHeader from './ViewHeader.js';
9
+ import ViewWrapper from './ViewWrapper.js';
10
+ import { useViewVisibility } from './useViewVisibility.js';
11
+ import { viewTitle } from './viewTitle.js';
12
+ // Intended to keep views mounted within ~1.5 viewport-heights of the visible
13
+ // band. **It does nothing, and making it work was measured as a regression.**
14
+ //
15
+ // An observer clips the target against every scrolling ancestor before applying
16
+ // the margin, which expands only the root's box — and both view containers are
17
+ // `overflow-y: auto`, so through one of them `150% 0px` qualifies exactly the
18
+ // views a `0px` margin would. Rooting the observer at the scroll port instead
19
+ // (`scrollPortOf`) does restore the band, and it came out a wash on scroll cost
20
+ // while holding 9-13 live GPU canvases instead of 6. The mount band trades
21
+ // pipeline rebuilds for live contexts, and with the ceiling at 16 live WebGL2
22
+ // contexts the contexts are the tighter constraint.
23
+ // See agent-docs/reference/GPU_CONTEXT_BUDGET.md.
24
+ //
25
+ // So don't tune this number expecting an effect, and don't make it live without
26
+ // first cutting contexts per display.
27
+ const VIEW_VISIBILITY_ROOT_MARGIN = '150% 0px';
28
+ // Scroll-space reserved for a view that has never been measured yet, so a fresh
29
+ // load spreads views down the page instead of stacking them all in the viewport
30
+ // (which would mark them all visible and defeat the lazy mount).
31
+ const ESTIMATED_VIEW_HEIGHT = 400;
32
+ // CALL THIS ONLY WHERE THE ESTIMATE IS ACTUALLY USED, i.e. under `reserveSpace`.
33
+ // A view's `height` is a computed over its track heights (LinearGenomeView's
34
+ // sums them), so reading it makes the caller an observer of every track resize
35
+ // and every display that grows with its data. Called unconditionally during
36
+ // render, that re-rendered the whole view chrome on each of those, to produce
37
+ // a placeholder height that only a hidden view ever shows. Measured: 2 chrome
38
+ // renders per height change became 0.
39
+ function viewHeight(view) {
40
+ return 'height' in view && typeof view.height === 'number'
41
+ ? view.height
42
+ : ESTIMATED_VIEW_HEIGHT;
43
+ }
44
+ // Every view that waits on something before it can mount content models that
45
+ // wait as `showLoading`. Views without the getter are mounted content the moment
46
+ // they render — which is the trap, because a view that waits and *doesn't*
47
+ // declare it is indistinguishable from one that never waits, and reports ready
48
+ // over a body that is plainly working. Spreadsheet and SV inspector were exactly
49
+ // that until they got the getter, and a spreadsheet mounts no displays, so no
50
+ // display-level wait covered for them either.
51
+ //
52
+ // LGV, dotplot, synteny, circular and breakpoint-split paint a spinner in place
53
+ // of their whole body meanwhile. Spreadsheet is the exception and keeps its
54
+ // import wizard mounted with a spinner above it, since the chosen file, type and
55
+ // assembly are worth more on screen than a bare loading screen — the phase is
56
+ // about the model, not about which component is mounted.
57
+ //
58
+ // Two values, not one per render branch: an import form is finished content, not
59
+ // a pending state, so it reports ready like anything else.
60
+ function viewPhase(view) {
61
+ return 'showLoading' in view && view.showLoading === true
62
+ ? 'loading'
63
+ : 'ready';
64
+ }
9
65
  const useStyles = makeStyles()(theme => ({
10
66
  viewContainer: {
11
67
  margin: theme.spacing(0.5),
12
68
  padding: `0 ${theme.spacing(1)} ${theme.spacing(1)}`,
13
69
  overflow: 'clip',
70
+ // xref https://stackoverflow.com/questions/43909940/why-does-overflowhidden-prevent-positionsticky-from-working
71
+ // note that contain:paint also seems to work
72
+ // `:focus-visible`, never `:focus`. The container carries tabIndex={0}, and
73
+ // a click on any non-focusable descendant of a tabbable node focuses that
74
+ // node — so on `:focus` this ring would appear around the whole view on
75
+ // essentially every click, which is a visible regression for every existing
76
+ // mouse user. `:focus-visible`'s heuristic excludes exactly that case.
77
+ //
78
+ // Inset (`outlineOffset: -2`) because `overflow: clip` above and the
79
+ // scroll port this sits in would clip a ring drawn outside the border box.
80
+ //
81
+ // `secondary.contrastText`, which is the same token the header's icons take,
82
+ // because inset is what decides the color: the ring lands entirely on this
83
+ // element's own secondary-colored band, so it has to contrast with THAT and
84
+ // not with the page. `primary.main` was the reflex and is midnight (#0D233F)
85
+ // against grape (#721E63) — drawn, measured on screen, and all but invisible.
86
+ // contrastText is the one token guaranteed legible on this background
87
+ // whatever an embedder's theme sets.
88
+ '&:focus-visible': {
89
+ outline: `2px solid ${theme.palette.secondary.contrastText}`,
90
+ outlineOffset: -2,
91
+ },
14
92
  },
15
93
  focusedView: {
16
94
  background: theme.palette.secondary.main,
@@ -19,31 +97,73 @@ const useStyles = makeStyles()(theme => ({
19
97
  background: theme.palette.secondary.dark,
20
98
  },
21
99
  }));
22
- const ViewContainer = observer(function ViewContainer({ view, session, }) {
23
- const theme = useTheme();
24
- const ref = useWidthSetter(view, theme.spacing(1));
100
+ const ViewContainer = observer(function ViewContainer({ view, session, scrollOnMount, }) {
101
+ const ref = useWidthSetter(view);
25
102
  const { classes } = useStyles();
103
+ const { ref: bodyRef, visible, measuredHeight, } = useViewVisibility(VIEW_VISIBILITY_ROOT_MARGIN);
104
+ useFocusOnInteraction(ref, () => {
105
+ session.setFocusedViewId(view.id);
106
+ });
107
+ // A minimized view renders no body, so it neither mounts the (GPU-heavy) view
108
+ // component nor reserves placeholder scroll-space; a spacer would otherwise
109
+ // re-expand the collapsed view to its pre-minimize height as soon as it
110
+ // scrolled out of the viewport.
111
+ const { minimized } = view;
112
+ const showBody = visible && !minimized;
113
+ const reserveSpace = !visible && !minimized;
114
+ // Tell the view whether its body is in the DOM. A display's phase counts its
115
+ // first paint as pending work, and a body that was never mounted has no
116
+ // canvas to paint — so without this an off-screen view holds the whole app's
117
+ // readiness marker at `loading` forever. See `BaseViewModel.bodyMounted`.
26
118
  useEffect(() => {
27
- function handleSelectView(e) {
28
- if (e.target instanceof Element && ref.current?.contains(e.target)) {
29
- session.setFocusedViewId(view.id);
30
- }
119
+ if (isAlive(view)) {
120
+ view.setBodyMounted(showBody);
31
121
  }
32
- document.addEventListener('mousedown', handleSelectView);
33
- document.addEventListener('keydown', handleSelectView);
34
- return () => {
35
- document.removeEventListener('mousedown', handleSelectView);
36
- document.removeEventListener('keydown', handleSelectView);
37
- };
38
- }, [ref, session, view]);
122
+ }, [view, showBody]);
123
+ // ...and tell it when this container goes away entirely, which is the case
124
+ // the effect above cannot see: React unmounts without running it again, so a
125
+ // view whose container left the DOM went on claiming a mounted body. The
126
+ // workspace does that on every tab switch — only the tab being shown is
127
+ // mounted (`WorkspaceLayout/CLAUDE.md`) — and the view it switched away from
128
+ // parked the app's readiness marker at `loading` for the rest of the session.
129
+ //
130
+ // Its own effect, depending on the view alone, so it fires on an actual
131
+ // unmount and not between the two writes of a `showBody` change: those writes
132
+ // are MST actions, so a false the container did not mean would reach every
133
+ // display's phase before the true landed.
134
+ useEffect(() => () => {
135
+ if (isAlive(view)) {
136
+ view.setBodyMounted(false);
137
+ }
138
+ }, [view]);
139
+ // Only this container tracks focus now, not its header: with the arrow icon
140
+ // in its own observer (ViewFocusIndicator) the header's props are unchanged
141
+ // for every view but the two the focus moved between, so mobx-react's memo
142
+ // holds and one click re-renders two headers rather than all of them.
39
143
  const backgroundColorClassName = session.focusedViewId === view.id
40
144
  ? classes.focusedView
41
145
  : classes.unfocusedView;
42
- const viewContainerClassName = cx(classes.viewContainer, backgroundColorClassName);
43
- return (_jsxs(Paper, { ref: ref, elevation: 12, className: viewContainerClassName, "data-testid": `view-container-${view.id}`, children: [_jsx(ViewHeader, { view: view, onClose: () => {
44
- session.removeView(view);
45
- }, onMinimize: () => {
46
- view.setMinimized(!view.minimized);
47
- }, className: backgroundColorClassName }), _jsx(Paper, { elevation: 0, children: _jsx(ViewWrapper, { view: view, session: session }) })] }));
146
+ // Same title `ViewContainerTitle` shows, recomputed rather than shared through
147
+ // a ref or aria-labelledby: the visible one lives inside an EditableTypography
148
+ // whose rendered node is an <input> while it is being edited, and a label
149
+ // pointing at an input reads back as the input, not as the name. Both of its
150
+ // inputs (`displayName`, `assemblyNames`) are cold — a rename and a change of
151
+ // displayed region — so this adds no per-frame read.
152
+ const { assemblyManager } = session;
153
+ return (_jsxs(Paper, { ref: ref, elevation: 12, className: cx(classes.viewContainer, backgroundColorClassName),
154
+ // The view's one tab stop, and the only thing that makes the ctrl/cmd +
155
+ // arrow shortcuts reachable without a mouse: they are gated on
156
+ // `session.focusedViewId`, which `useFocusOnInteraction` now assigns from
157
+ // `focusin`. It goes on the outermost node so there is exactly one stop
158
+ // per view ahead of the header's own buttons, and so every view type gets
159
+ // it rather than each one growing its own.
160
+ tabIndex: 0,
161
+ // A tab stop with no role and no name announces as nothing. `region` is
162
+ // the conservative reading — it is what the session's view stack is, a set
163
+ // of labelled panels — and a named region shows up in a screen reader's
164
+ // landmark list, so "jump to the second view" stops being a Tab count.
165
+ role: "region", "aria-label": viewTitle(view, r => assemblyManager.getDisplayName(r)), "data-testid": `view-container-${view.id}`, "data-view-phase": viewPhase(view), children: [_jsx(ViewHeader, { view: view, className: backgroundColorClassName, scrollOnMount: scrollOnMount }), _jsx(Paper, { elevation: 0, children: _jsx("div", { ref: bodyRef, style: reserveSpace
166
+ ? { height: measuredHeight ?? viewHeight(view) }
167
+ : undefined, children: showBody ? _jsx(ViewWrapper, { view: view, session: session }) : null }) })] }));
48
168
  });
49
169
  export default ViewContainer;
@@ -1,5 +1,5 @@
1
1
  import type { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes';
2
2
  declare const ViewContainerTitle: ({ view, }: {
3
3
  view: IBaseViewModel;
4
- }) => import("react/jsx-runtime").JSX.Element;
4
+ }) => import("react").JSX.Element;
5
5
  export default ViewContainerTitle;
@@ -4,10 +4,17 @@ import { getSession } from '@jbrowse/core/util';
4
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
5
  import { Tooltip } from '@mui/material';
6
6
  import { observer } from 'mobx-react';
7
+ import { viewTitle } from './viewTitle.js';
7
8
  const useStyles = makeStyles()(theme => ({
8
9
  input: {
9
10
  paddingBottom: 0,
10
11
  paddingTop: 2,
12
+ // long view titles (e.g. a read-vs-ref panel named after a long PacBio
13
+ // QNAME) truncate with an ellipsis instead of overflowing the header
14
+ maxWidth: 300,
15
+ textOverflow: 'ellipsis',
16
+ whiteSpace: 'nowrap',
17
+ overflow: 'hidden',
11
18
  },
12
19
  inputBase: {
13
20
  color: theme.palette.secondary.contrastText,
@@ -25,8 +32,8 @@ const useStyles = makeStyles()(theme => ({
25
32
  const ViewContainerTitle = observer(function ViewContainerTitle({ view, }) {
26
33
  const { classes } = useStyles();
27
34
  const { assemblyManager } = getSession(view);
28
- return (_jsx(Tooltip, { title: "Rename view", arrow: true, children: _jsx(EditableTypography, { value: view.displayName ||
29
- `${view.assemblyNames?.map(r => assemblyManager.get(r)?.displayName).join(',') || 'Untitled view'}${view.minimized ? ' (minimized)' : ''}`, setValue: val => {
35
+ const title = viewTitle(view, r => assemblyManager.getDisplayName(r));
36
+ return (_jsx(Tooltip, { title: `${title} (click to rename)`, arrow: true, children: _jsx(EditableTypography, { value: title, setValue: val => {
30
37
  view.setDisplayName(val);
31
38
  }, variant: "body2", classes: {
32
39
  input: classes.input,
@@ -0,0 +1,18 @@
1
+ import type { AbstractViewModel, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
2
+ /**
3
+ * What a view that threw during render is replaced by: which view died, the
4
+ * error itself, and the two ways out that are not reloading the application.
5
+ *
6
+ * Retry clears the boundary and remounts the view component — worth offering
7
+ * because a good share of these are transient (a lazy chunk that failed to
8
+ * fetch, a GPU context lost). Close goes through the session's `removeView`,
9
+ * which detaches before it destroys; nothing here may destroy the node itself
10
+ * (ADR-069).
11
+ */
12
+ declare const ViewErrorFallback: ({ view, session, error, onRetry, }: {
13
+ view: AbstractViewModel;
14
+ session: SessionWithFocusedViewAndDrawerWidgets;
15
+ error: unknown;
16
+ onRetry: () => void;
17
+ }) => import("react").JSX.Element;
18
+ export default ViewErrorFallback;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ErrorBanner } from '@jbrowse/core/ui';
3
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
4
+ import { Button } from '@mui/material';
5
+ import { observer } from 'mobx-react';
6
+ import { viewTitle } from './viewTitle.js';
7
+ const useStyles = makeStyles()(theme => ({
8
+ which: {
9
+ padding: `${theme.spacing(1)} ${theme.spacing(1)} 0`,
10
+ },
11
+ }));
12
+ // The fallback must not be able to throw. What crashes a view is often one of
13
+ // its own getters, and `viewTitle` reads `assemblyNames`, which is one — so
14
+ // naming the view is exactly where a fallback can rethrow, and a rethrow here
15
+ // goes to the app boundary and puts up FatalErrorDialog, i.e. the whole-page
16
+ // failure this boundary exists to avoid. `displayName` and `type` are plain
17
+ // properties and cannot.
18
+ function safeViewTitle(view, session) {
19
+ try {
20
+ return viewTitle(view, r => session.assemblyManager.getDisplayName(r));
21
+ }
22
+ catch {
23
+ return view.displayName ?? 'Untitled view';
24
+ }
25
+ }
26
+ /**
27
+ * What a view that threw during render is replaced by: which view died, the
28
+ * error itself, and the two ways out that are not reloading the application.
29
+ *
30
+ * Retry clears the boundary and remounts the view component — worth offering
31
+ * because a good share of these are transient (a lazy chunk that failed to
32
+ * fetch, a GPU context lost). Close goes through the session's `removeView`,
33
+ * which detaches before it destroys; nothing here may destroy the node itself
34
+ * (ADR-069).
35
+ */
36
+ const ViewErrorFallback = observer(function ViewErrorFallback({ view, session, error, onRetry, }) {
37
+ const { classes } = useStyles();
38
+ return (_jsxs("div", { children: [_jsx("div", { className: classes.which, children: `The ${view.type} "${safeViewTitle(view, session)}" could not be displayed` }), _jsx(ErrorBanner, { error: error, onReset: onRetry, extraAction: _jsx(Button, { "data-testid": "close_crashed_view", onClick: () => {
39
+ session.removeView(view);
40
+ }, children: "Close view" }) })] }));
41
+ });
42
+ export default ViewErrorFallback;
@@ -1,8 +1,7 @@
1
1
  import type { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
2
- declare const ViewHeader: ({ view, onClose, onMinimize, className, }: {
2
+ declare const ViewHeader: ({ view, className, scrollOnMount, }: {
3
3
  view: IBaseViewModel;
4
- onClose: () => void;
5
- onMinimize: () => void;
6
4
  className?: string;
7
- }) => import("react/jsx-runtime").JSX.Element;
5
+ scrollOnMount?: boolean;
6
+ }) => import("react").JSX.Element;
8
7
  export default ViewHeader;
@@ -2,13 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from 'react';
3
3
  import { VIEW_HEADER_HEIGHT } from '@jbrowse/core/ui';
4
4
  import { getSession } from '@jbrowse/core/util';
5
+ import { VIEW_HEADER_HEIGHT_VAR, useChromeHeightVar, } from '@jbrowse/core/util/hooks';
5
6
  import { cx, makeStyles } from '@jbrowse/core/util/tss-react';
6
- import { isSessionWithMultipleViews } from '@jbrowse/product-core';
7
7
  import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
8
8
  import { observer } from 'mobx-react';
9
- import ViewButtons from "./ViewButtons.js";
10
- import ViewContainerTitle from "./ViewContainerTitle.js";
11
- import ViewMenu from "./ViewMenu.js";
9
+ import ViewButtons from './ViewButtons.js';
10
+ import ViewContainerTitle from './ViewContainerTitle.js';
11
+ import ViewMenu from './ViewMenu.js';
12
12
  const useStyles = makeStyles()(theme => ({
13
13
  icon: {
14
14
  color: theme.palette.secondary.contrastText,
@@ -18,7 +18,11 @@ const useStyles = makeStyles()(theme => ({
18
18
  },
19
19
  viewHeader: {
20
20
  display: 'flex',
21
- height: VIEW_HEADER_HEIGHT,
21
+ // a minimum, not a height: the constant exists so the sticky boxes below
22
+ // can clear this one, and pinning the box to it clipped the title row at a
23
+ // larger root font size. `useChromeHeightVar` publishes what it actually
24
+ // measures, so growing here moves them rather than overlapping them
25
+ minHeight: VIEW_HEADER_HEIGHT,
22
26
  top: 0,
23
27
  zIndex: 900,
24
28
  },
@@ -27,22 +31,29 @@ const useStyles = makeStyles()(theme => ({
27
31
  alignItems: 'center',
28
32
  },
29
33
  }));
30
- const ViewHeader = observer(function ViewHeader({ view, onClose, onMinimize, className, }) {
34
+ // The one thing in the header that depends on which view has focus, kept in its
35
+ // own observer so the header does not. Read from ViewHeader itself,
36
+ // `focusedViewId` made a single click re-render every view's header and the MUI
37
+ // subtree under it (a Tooltip, three IconButtons and the menu), because an
38
+ // observer re-renders on its own observable change whatever its props say. Here
39
+ // a click re-renders N nodes that mostly render nothing.
40
+ const ViewFocusIndicator = observer(function ViewFocusIndicator({ view, className, }) {
41
+ return getSession(view).focusedViewId === view.id ? (_jsx(KeyboardArrowRightIcon, { className: className, fontSize: "small" })) : null;
42
+ });
43
+ const ViewHeader = observer(function ViewHeader({ view, className, scrollOnMount, }) {
31
44
  const { classes } = useStyles();
32
45
  const scrollRef = useRef(null);
33
- const session = getSession(view);
34
- let stickyViewHeaders = false;
35
- if (isSessionWithMultipleViews(session)) {
36
- ;
37
- ({ stickyViewHeaders } = session);
38
- }
46
+ useChromeHeightVar(scrollRef, VIEW_HEADER_HEIGHT_VAR);
47
+ const stickyViewHeaders = getSession(view).stickyViewHeaders === true;
48
+ // Scroll a newly-added view into view on mount. Gated on scrollOnMount so a
49
+ // cold load / session restore with several views doesn't have every header
50
+ // race to scrollIntoView (last one wins, landing on the bottom view);
51
+ // ViewStack only sets it for views added after the initial render.
39
52
  useEffect(() => {
40
- if (typeof jest === 'undefined' &&
41
- !navigator.webdriver &&
42
- window.self === window.top) {
53
+ if (scrollOnMount && !navigator.webdriver && window.self === window.top) {
43
54
  scrollRef.current?.scrollIntoView({ block: 'center' });
44
55
  }
45
- }, []);
46
- return (_jsxs("div", { ref: scrollRef, className: cx(classes.viewHeader, className), style: { position: stickyViewHeaders ? 'sticky' : undefined }, children: [_jsx(ViewMenu, { model: view, IconProps: { className: classes.icon } }), _jsx("div", { className: classes.grow }), _jsxs("div", { className: classes.viewTitle, children: [session.focusedViewId === view.id ? (_jsx(KeyboardArrowRightIcon, { className: classes.icon, fontSize: "small" })) : null, _jsx(ViewContainerTitle, { view: view })] }), _jsx("div", { className: classes.grow }), _jsx(ViewButtons, { onClose: onClose, onMinimize: onMinimize, view: view })] }));
56
+ }, [scrollOnMount]);
57
+ return (_jsxs("div", { ref: scrollRef, className: cx(classes.viewHeader, className), style: { position: stickyViewHeaders ? 'sticky' : undefined }, children: [_jsx(ViewMenu, { model: view, className: classes.icon }), _jsx("div", { className: classes.grow }), _jsxs("div", { className: classes.viewTitle, children: [_jsx(ViewFocusIndicator, { view: view, className: classes.icon }), _jsx(ViewContainerTitle, { view: view })] }), _jsx("div", { className: classes.grow }), _jsx(ViewButtons, { view: view })] }));
47
58
  });
48
59
  export default ViewHeader;
@@ -1,11 +1,5 @@
1
- import type { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
2
1
  import type { SessionWithDrawerWidgets } from '@jbrowse/core/util';
3
- type AppSession = SessionWithDrawerWidgets & {
4
- snackbarMessages: SnackbarMessage[];
5
- renameCurrentSession: (arg: string) => void;
6
- popSnackbarMessage: () => unknown;
7
- };
8
2
  declare const ViewLauncher: ({ session, }: {
9
- session: AppSession;
10
- }) => import("react/jsx-runtime").JSX.Element;
3
+ session: SessionWithDrawerWidgets;
4
+ }) => import("react").JSX.Element;
11
5
  export default ViewLauncher;
@@ -15,12 +15,14 @@ const useStyles = makeStyles()(theme => ({
15
15
  const ViewLauncher = observer(function ViewLauncher({ session, }) {
16
16
  const { classes } = useStyles();
17
17
  const { pluginManager } = getEnv(session);
18
- const viewTypes = pluginManager.getViewElements();
19
- const [value, setValue] = useState(viewTypes[0]?.name || '');
18
+ const viewTypes = pluginManager
19
+ .getViewElements()
20
+ .filter(({ viewMetadata }) => !viewMetadata.hiddenFromGUI);
21
+ const [value, setValue] = useState(viewTypes[0]?.name ?? '');
20
22
  return (_jsxs(Paper, { className: classes.selectPaper, children: [_jsx(Typography, { children: "Select a view to launch" }), _jsx(FormControl, { className: classes.m2, children: _jsx(Select, { value: value, onChange: event => {
21
23
  setValue(event.target.value);
22
- }, children: viewTypes
23
- .filter(({ viewMetadata }) => !viewMetadata.hiddenFromGUI)
24
- .map(({ displayName, name }) => (_jsx(MenuItem, { value: name, children: displayName }, name))) }) }), _jsx(FormControl, { className: classes.m2, children: _jsx(Button, { onClick: () => session.addView(value, {}), variant: "contained", color: "primary", children: "Launch view" }) })] }));
24
+ }, children: viewTypes.map(({ displayName, name }) => (_jsx(MenuItem, { value: name, children: displayName }, name))) }) }), _jsx(FormControl, { className: classes.m2, children: _jsx(Button, { onClick: () => {
25
+ session.addView(value, {});
26
+ }, variant: "contained", color: "primary", children: "Launch view" }) })] }));
25
27
  });
26
28
  export default ViewLauncher;
@@ -1,8 +1,6 @@
1
1
  import type { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
2
- import type { IconButtonProps as IconButtonPropsType, SvgIconProps } from '@mui/material';
3
- declare const ViewMenu: ({ model, IconProps, }: {
2
+ declare const ViewMenu: ({ model, className, }: {
4
3
  model: IBaseViewModel;
5
- IconButtonProps?: IconButtonPropsType;
6
- IconProps: SvgIconProps;
7
- }) => import("react/jsx-runtime").JSX.Element;
4
+ className?: string;
5
+ }) => import("react").JSX.Element;
8
6
  export default ViewMenu;