@jbrowse/app-core 4.3.0 → 5.0.0-beta.10

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 (165) 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 +1324 -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 +294 -0
  8. package/esm/Assemblies/AssembliesMixin.js +154 -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 +11 -0
  12. package/esm/HistoryManagement/index.js +43 -10
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +123 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +79 -7
  15. package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
  16. package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
  17. package/esm/JBrowseConfig/index.d.ts +175 -84
  18. package/esm/JBrowseConfig/index.js +64 -2
  19. package/esm/JBrowseModel/index.d.ts +175 -97
  20. package/esm/JBrowseModel/index.js +132 -31
  21. package/esm/JbApi/index.d.ts +2 -0
  22. package/esm/JbApi/index.js +1 -0
  23. package/esm/JbApi/jbApi.d.ts +2767 -0
  24. package/esm/JbApi/jbApi.js +1383 -0
  25. package/esm/RootMenu/index.d.ts +69 -3
  26. package/esm/RootMenu/index.js +96 -61
  27. package/esm/SessionSpec/index.d.ts +6 -0
  28. package/esm/SessionSpec/index.js +3 -0
  29. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  30. package/esm/SessionSpec/lgvUrlInit.js +161 -0
  31. package/esm/SessionSpec/loadSessionSpec.d.ts +75 -0
  32. package/esm/SessionSpec/loadSessionSpec.js +473 -0
  33. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +7 -0
  34. package/esm/SessionSpec/parseSessionSpecUrl.js +181 -0
  35. package/esm/SessionSpec/types.d.ts +37 -0
  36. package/esm/SessionSpec/types.js +1 -0
  37. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  38. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  39. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  40. package/esm/WorkspaceLayout/PanelView.js +99 -0
  41. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  42. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  43. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  44. package/esm/WorkspaceLayout/WorkspaceContainer.js +107 -0
  45. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  46. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  47. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  48. package/esm/WorkspaceLayout/WorkspaceTab.js +105 -0
  49. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  50. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  51. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  52. package/esm/WorkspaceLayout/dropZone.js +113 -0
  53. package/esm/WorkspaceLayout/index.d.ts +8 -0
  54. package/esm/WorkspaceLayout/index.js +4 -0
  55. package/esm/WorkspaceLayout/model.d.ts +260 -0
  56. package/esm/WorkspaceLayout/model.js +586 -0
  57. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  58. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  59. package/esm/WorkspaceLayout/spec.d.ts +88 -0
  60. package/esm/WorkspaceLayout/spec.js +226 -0
  61. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  62. package/esm/WorkspaceLayout/splitter.js +37 -0
  63. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  64. package/esm/WorkspaceLayout/tree.js +379 -0
  65. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  66. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  67. package/esm/index.d.ts +18 -9
  68. package/esm/index.js +16 -9
  69. package/esm/menus.d.ts +55 -68
  70. package/esm/menus.js +117 -86
  71. package/esm/queryParams.d.ts +9 -0
  72. package/esm/queryParams.js +93 -0
  73. package/esm/sessionModelFactory.d.ts +18 -0
  74. package/esm/sessionModelFactory.js +1 -0
  75. package/esm/ui/App/App.d.ts +3 -14
  76. package/esm/ui/App/App.js +39 -19
  77. package/esm/ui/App/AppFab.d.ts +12 -3
  78. package/esm/ui/App/AppFab.js +15 -8
  79. package/esm/ui/App/AppToolbar.d.ts +2 -14
  80. package/esm/ui/App/AppToolbar.js +45 -31
  81. package/esm/ui/App/ClassicViewsContainer.d.ts +4 -10
  82. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  83. package/esm/ui/App/DialogQueue.d.ts +17 -1
  84. package/esm/ui/App/DialogQueue.js +30 -1
  85. package/esm/ui/App/Drawer.d.ts +1 -1
  86. package/esm/ui/App/Drawer.js +10 -39
  87. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  88. package/esm/ui/App/DrawerHeader.js +24 -11
  89. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  90. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  91. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  92. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  93. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  94. package/esm/ui/App/DrawerWidget.js +9 -21
  95. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  96. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  97. package/esm/ui/App/ThemeModeButton.d.ts +12 -0
  98. package/esm/ui/App/ThemeModeButton.js +34 -0
  99. package/esm/ui/App/ViewButtons.d.ts +2 -4
  100. package/esm/ui/App/ViewButtons.js +15 -3
  101. package/esm/ui/App/ViewContainer.d.ts +3 -2
  102. package/esm/ui/App/ViewContainer.js +128 -24
  103. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  104. package/esm/ui/App/ViewContainerTitle.js +21 -11
  105. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  106. package/esm/ui/App/ViewErrorFallback.js +42 -0
  107. package/esm/ui/App/ViewHeader.d.ts +3 -4
  108. package/esm/ui/App/ViewHeader.js +28 -17
  109. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  110. package/esm/ui/App/ViewLauncher.js +7 -5
  111. package/esm/ui/App/ViewMenu.d.ts +3 -5
  112. package/esm/ui/App/ViewMenu.js +87 -92
  113. package/esm/ui/App/ViewStack.d.ts +7 -0
  114. package/esm/ui/App/ViewStack.js +48 -0
  115. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  116. package/esm/ui/App/ViewWrapper.js +26 -10
  117. package/esm/ui/App/ViewsContainer.d.ts +4 -11
  118. package/esm/ui/App/ViewsContainer.js +6 -9
  119. package/esm/ui/App/index.d.ts +2 -1
  120. package/esm/ui/App/index.js +3 -2
  121. package/esm/ui/App/lazyChunk.d.ts +13 -0
  122. package/esm/ui/App/lazyChunk.js +91 -0
  123. package/esm/ui/App/lazyParts.d.ts +32 -0
  124. package/esm/ui/App/lazyParts.js +8 -0
  125. package/esm/ui/App/preloadAppShell.d.ts +14 -0
  126. package/esm/ui/App/preloadAppShell.js +41 -0
  127. package/esm/ui/App/types.d.ts +17 -3
  128. package/esm/ui/App/types.js +1 -1
  129. package/esm/ui/App/useViewVisibility.d.ts +35 -0
  130. package/esm/ui/App/useViewVisibility.js +88 -0
  131. package/esm/ui/App/viewTitle.d.ts +12 -0
  132. package/esm/ui/App/viewTitle.js +17 -0
  133. package/esm/ui/index.d.ts +1 -1
  134. package/esm/ui/index.js +1 -1
  135. package/package.json +12 -17
  136. package/esm/AppFocus/index.d.ts +0 -5
  137. package/esm/AppFocus/index.js +0 -12
  138. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  139. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  140. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  141. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  142. package/esm/DockviewLayout/index.d.ts +0 -35
  143. package/esm/DockviewLayout/index.js +0 -111
  144. package/esm/ui/App/DockviewContext.d.ts +0 -22
  145. package/esm/ui/App/DockviewContext.js +0 -29
  146. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  147. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  148. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  149. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  150. package/esm/ui/App/DrawerControls.d.ts +0 -5
  151. package/esm/ui/App/DrawerControls.js +0 -25
  152. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  153. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  154. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  155. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  156. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  157. package/esm/ui/App/JBrowseViewTab.js +0 -97
  158. package/esm/ui/App/ModalWidget.d.ts +0 -6
  159. package/esm/ui/App/ModalWidget.js +0 -48
  160. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  161. package/esm/ui/App/TiledViewsContainer.js +0 -366
  162. package/esm/ui/App/copyView.d.ts +0 -1
  163. package/esm/ui/App/copyView.js +0 -29
  164. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  165. package/esm/ui/App/dockviewUtils.js +0 -55
@@ -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, { "aria-label": view.minimized ? 'Restore view' : 'Minimize view', "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, { "aria-label": "Close view", "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,78 @@
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
+ // Scroll-space reserved for a view that has never been measured yet, so a fresh
13
+ // load spreads views down the page instead of stacking them all in the viewport
14
+ // (which would mark them all visible and defeat the lazy mount).
15
+ const ESTIMATED_VIEW_HEIGHT = 400;
16
+ // CALL THIS ONLY WHERE THE ESTIMATE IS ACTUALLY USED, i.e. under `reserveSpace`.
17
+ // A view's `height` is a computed over its track heights (LinearGenomeView's
18
+ // sums them), so reading it makes the caller an observer of every track resize
19
+ // and every display that grows with its data. Called unconditionally during
20
+ // render, that re-rendered the whole view chrome on each of those, to produce
21
+ // a placeholder height that only a hidden view ever shows. Measured: 2 chrome
22
+ // renders per height change became 0.
23
+ function viewHeight(view) {
24
+ return 'height' in view && typeof view.height === 'number'
25
+ ? view.height
26
+ : ESTIMATED_VIEW_HEIGHT;
27
+ }
28
+ // Every view that waits on something before it can mount content models that
29
+ // wait as `showLoading`. Views without the getter are mounted content the moment
30
+ // they render — which is the trap, because a view that waits and *doesn't*
31
+ // declare it is indistinguishable from one that never waits, and reports ready
32
+ // over a body that is plainly working. Spreadsheet and SV inspector were exactly
33
+ // that until they got the getter, and a spreadsheet mounts no displays, so no
34
+ // display-level wait covered for them either.
35
+ //
36
+ // LGV, dotplot, synteny, circular and breakpoint-split paint a spinner in place
37
+ // of their whole body meanwhile. Spreadsheet is the exception and keeps its
38
+ // import wizard mounted with a spinner above it, since the chosen file, type and
39
+ // assembly are worth more on screen than a bare loading screen — the phase is
40
+ // about the model, not about which component is mounted.
41
+ //
42
+ // Two values, not one per render branch: an import form is finished content, not
43
+ // a pending state, so it reports ready like anything else.
44
+ function viewPhase(view) {
45
+ return 'showLoading' in view && view.showLoading === true
46
+ ? 'loading'
47
+ : 'ready';
48
+ }
9
49
  const useStyles = makeStyles()(theme => ({
10
50
  viewContainer: {
11
51
  margin: theme.spacing(0.5),
12
52
  padding: `0 ${theme.spacing(1)} ${theme.spacing(1)}`,
13
53
  overflow: 'clip',
54
+ // xref https://stackoverflow.com/questions/43909940/why-does-overflowhidden-prevent-positionsticky-from-working
55
+ // note that contain:paint also seems to work
56
+ // `:focus-visible`, never `:focus`. The container carries tabIndex={0}, and
57
+ // a click on any non-focusable descendant of a tabbable node focuses that
58
+ // node — so on `:focus` this ring would appear around the whole view on
59
+ // essentially every click, which is a visible regression for every existing
60
+ // mouse user. `:focus-visible`'s heuristic excludes exactly that case.
61
+ //
62
+ // Inset (`outlineOffset: -2`) because `overflow: clip` above and the
63
+ // scroll port this sits in would clip a ring drawn outside the border box.
64
+ //
65
+ // `secondary.contrastText`, which is the same token the header's icons take,
66
+ // because inset is what decides the color: the ring lands entirely on this
67
+ // element's own secondary-colored band, so it has to contrast with THAT and
68
+ // not with the page. `primary.main` was the reflex and is midnight (#0D233F)
69
+ // against grape (#721E63) — drawn, measured on screen, and all but invisible.
70
+ // contrastText is the one token guaranteed legible on this background
71
+ // whatever an embedder's theme sets.
72
+ '&:focus-visible': {
73
+ outline: `2px solid ${theme.palette.secondary.contrastText}`,
74
+ outlineOffset: -2,
75
+ },
14
76
  },
15
77
  focusedView: {
16
78
  background: theme.palette.secondary.main,
@@ -19,31 +81,73 @@ const useStyles = makeStyles()(theme => ({
19
81
  background: theme.palette.secondary.dark,
20
82
  },
21
83
  }));
22
- const ViewContainer = observer(function ViewContainer({ view, session, }) {
23
- const theme = useTheme();
24
- const ref = useWidthSetter(view, theme.spacing(1));
84
+ const ViewContainer = observer(function ViewContainer({ view, session, scrollOnMount, }) {
85
+ const ref = useWidthSetter(view);
25
86
  const { classes } = useStyles();
87
+ const { ref: bodyRef, visible, measuredHeight } = useViewVisibility();
88
+ useFocusOnInteraction(ref, () => {
89
+ session.setFocusedViewId(view.id);
90
+ });
91
+ // A minimized view renders no body, so it neither mounts the (GPU-heavy) view
92
+ // component nor reserves placeholder scroll-space; a spacer would otherwise
93
+ // re-expand the collapsed view to its pre-minimize height as soon as it
94
+ // scrolled out of the viewport.
95
+ const { minimized } = view;
96
+ const showBody = visible && !minimized;
97
+ const reserveSpace = !visible && !minimized;
98
+ // Tell the view whether its body is in the DOM. A display's phase counts its
99
+ // first paint as pending work, and a body that was never mounted has no
100
+ // canvas to paint — so without this an off-screen view holds the whole app's
101
+ // readiness marker at `loading` forever. See `BaseViewModel.bodyMounted`.
26
102
  useEffect(() => {
27
- function handleSelectView(e) {
28
- if (e.target instanceof Element && ref.current?.contains(e.target)) {
29
- session.setFocusedViewId(view.id);
30
- }
103
+ if (isAlive(view)) {
104
+ view.setBodyMounted(showBody);
31
105
  }
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]);
106
+ }, [view, showBody]);
107
+ // ...and tell it when this container goes away entirely, which is the case
108
+ // the effect above cannot see: React unmounts without running it again, so a
109
+ // view whose container left the DOM went on claiming a mounted body. The
110
+ // workspace does that on every tab switch — only the tab being shown is
111
+ // mounted (`WorkspaceLayout/CLAUDE.md`) — and the view it switched away from
112
+ // parked the app's readiness marker at `loading` for the rest of the session.
113
+ //
114
+ // Its own effect, depending on the view alone, so it fires on an actual
115
+ // unmount and not between the two writes of a `showBody` change: those writes
116
+ // are MST actions, so a false the container did not mean would reach every
117
+ // display's phase before the true landed.
118
+ useEffect(() => () => {
119
+ if (isAlive(view)) {
120
+ view.setBodyMounted(false);
121
+ }
122
+ }, [view]);
123
+ // Only this container tracks focus now, not its header: with the arrow icon
124
+ // in its own observer (ViewFocusIndicator) the header's props are unchanged
125
+ // for every view but the two the focus moved between, so mobx-react's memo
126
+ // holds and one click re-renders two headers rather than all of them.
39
127
  const backgroundColorClassName = session.focusedViewId === view.id
40
128
  ? classes.focusedView
41
129
  : 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 }) })] }));
130
+ // Same title `ViewContainerTitle` shows, recomputed rather than shared through
131
+ // a ref or aria-labelledby: the visible one lives inside an EditableTypography
132
+ // whose rendered node is an <input> while it is being edited, and a label
133
+ // pointing at an input reads back as the input, not as the name. Both of its
134
+ // inputs (`displayName`, `assemblyNames`) are cold — a rename and a change of
135
+ // displayed region — so this adds no per-frame read.
136
+ const { assemblyManager } = session;
137
+ return (_jsxs(Paper, { ref: ref, elevation: 12, className: cx(classes.viewContainer, backgroundColorClassName),
138
+ // The view's one tab stop, and the only thing that makes the ctrl/cmd +
139
+ // arrow shortcuts reachable without a mouse: they are gated on
140
+ // `session.focusedViewId`, which `useFocusOnInteraction` now assigns from
141
+ // `focusin`. It goes on the outermost node so there is exactly one stop
142
+ // per view ahead of the header's own buttons, and so every view type gets
143
+ // it rather than each one growing its own.
144
+ tabIndex: 0,
145
+ // A tab stop with no role and no name announces as nothing. `region` is
146
+ // the conservative reading — it is what the session's view stack is, a set
147
+ // of labelled panels — and a named region shows up in a screen reader's
148
+ // landmark list, so "jump to the second view" stops being a Tab count.
149
+ 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
150
+ ? { height: measuredHeight ?? viewHeight(view) }
151
+ : undefined, children: showBody ? _jsx(ViewWrapper, { view: view, session: session }) : null }) })] }));
48
152
  });
49
153
  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;
@@ -1,13 +1,20 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import EditableTypography from '@jbrowse/core/ui/EditableTypography';
3
3
  import { getSession } from '@jbrowse/core/util';
4
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
- import { Tooltip } from '@mui/material';
5
+ import { Tooltip, Typography } from '@mui/material';
6
6
  import { observer } from 'mobx-react';
7
+ import { viewName, 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,
@@ -21,18 +28,21 @@ const useStyles = makeStyles()(theme => ({
21
28
  borderColor: theme.palette.primary.main,
22
29
  backgroundColor: theme.palette.secondary.light,
23
30
  },
31
+ minimized: {
32
+ color: theme.palette.secondary.contrastText,
33
+ },
24
34
  }));
25
35
  const ViewContainerTitle = observer(function ViewContainerTitle({ view, }) {
26
36
  const { classes } = useStyles();
27
37
  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 => {
30
- view.setDisplayName(val);
31
- }, variant: "body2", classes: {
32
- input: classes.input,
33
- inputBase: classes.inputBase,
34
- inputRoot: classes.inputRoot,
35
- inputFocused: classes.inputFocused,
36
- } }) }));
38
+ const getDisplayName = (r) => assemblyManager.getDisplayName(r);
39
+ return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { title: `${viewTitle(view, getDisplayName)} (click to rename)`, arrow: true, children: _jsx(EditableTypography, { value: viewName(view, getDisplayName), setValue: val => {
40
+ view.setDisplayName(val);
41
+ }, variant: "body2", classes: {
42
+ input: classes.input,
43
+ inputBase: classes.inputBase,
44
+ inputRoot: classes.inputRoot,
45
+ inputFocused: classes.inputFocused,
46
+ } }) }), view.minimized ? (_jsx(Typography, { variant: "body2", className: classes.minimized, children: "(minimized)" })) : null] }));
37
47
  });
38
48
  export default ViewContainerTitle;
@@ -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
+ void session.launchView(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;