@jbrowse/display-ui 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +433 -0
- package/esm/DisplayUIProvider.d.ts +70 -0
- package/esm/DisplayUIProvider.js +96 -0
- package/esm/FloatingLegend.d.ts +16 -0
- package/esm/FloatingLegend.js +213 -0
- package/esm/PointerLayer.d.ts +26 -0
- package/esm/PointerLayer.js +27 -0
- package/esm/bottomRightCorner.d.ts +58 -0
- package/esm/bottomRightCorner.js +59 -0
- package/esm/chromeOverlayContext.d.ts +7 -0
- package/esm/chromeOverlayContext.js +44 -0
- package/esm/chromeOverlays.d.ts +88 -0
- package/esm/chromeOverlays.js +1 -0
- package/esm/index.d.ts +22 -0
- package/esm/index.js +51 -0
- package/esm/isLiveModel.d.ts +21 -0
- package/esm/isLiveModel.js +24 -0
- package/esm/plainChromeOverlays.d.ts +18 -0
- package/esm/plainChromeOverlays.js +189 -0
- package/esm/tooLargeBannerText.d.ts +18 -0
- package/esm/tooLargeBannerText.js +34 -0
- package/esm/tooltip/Tooltip.d.ts +52 -0
- package/esm/tooltip/Tooltip.js +67 -0
- package/esm/tooltip/useTooltip.d.ts +50 -0
- package/esm/tooltip/useTooltip.js +116 -0
- package/esm/trackControl/plainTrackControl.d.ts +17 -0
- package/esm/trackControl/plainTrackControl.js +133 -0
- package/esm/trackControl/trackControlContext.d.ts +7 -0
- package/esm/trackControl/trackControlContext.js +30 -0
- package/esm/trackControl/types.d.ts +58 -0
- package/esm/trackControl/types.js +1 -0
- package/esm/trackControl/useTrackControlMenu.d.ts +51 -0
- package/esm/trackControl/useTrackControlMenu.js +163 -0
- package/esm/trackOverlay/TrackOverlayContext.d.ts +1 -0
- package/esm/trackOverlay/TrackOverlayContext.js +18 -0
- package/esm/trackOverlay/TrackOverlayPortal.d.ts +18 -0
- package/esm/trackOverlay/TrackOverlayPortal.js +66 -0
- package/esm/trackOverlay/TrackOverlaySlot.d.ts +54 -0
- package/esm/trackOverlay/TrackOverlaySlot.js +60 -0
- package/package.json +48 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
/** What `ErrorBar` reads: a failed fetch, and the way to run it again. */
|
|
3
|
+
export interface DisplayErrorBarModel {
|
|
4
|
+
error: unknown;
|
|
5
|
+
reload: () => void;
|
|
6
|
+
}
|
|
7
|
+
/** What `Loading` reads. Everything is optional — a display that reports no
|
|
8
|
+
* progress and offers no cancel still gets a scrim. */
|
|
9
|
+
export interface DisplayLoadingOverlayModel {
|
|
10
|
+
statusMessage?: string;
|
|
11
|
+
statusProgress?: number;
|
|
12
|
+
fetchCanceled?: boolean;
|
|
13
|
+
cancelFetchByUser?: () => void;
|
|
14
|
+
reload?: () => void;
|
|
15
|
+
}
|
|
16
|
+
/** What `BackgroundProgress` reads: the status channel for work with no fetch
|
|
17
|
+
* behind it, while the phase is `ready`. */
|
|
18
|
+
export interface DisplayBackgroundProgressModel {
|
|
19
|
+
statusMessage?: string;
|
|
20
|
+
statusProgress?: number;
|
|
21
|
+
}
|
|
22
|
+
/** What `TooLarge` reads. `forceLoad` is the whole point of the state — see the
|
|
23
|
+
* entry below. */
|
|
24
|
+
export interface TooLargeMessageModel {
|
|
25
|
+
regionTooLargeReason: string;
|
|
26
|
+
zoomCanReleaseGate: boolean;
|
|
27
|
+
forceLoad: () => void;
|
|
28
|
+
}
|
|
29
|
+
export interface DisplayChromeOverlays {
|
|
30
|
+
/**
|
|
31
|
+
* GPU/render-backend failure. A subtree-replacing terminal state: the canvas
|
|
32
|
+
* unmounts and the backend disposes, so this owns the full display area.
|
|
33
|
+
*/
|
|
34
|
+
RenderError: ComponentType<{
|
|
35
|
+
error: unknown;
|
|
36
|
+
onRetry: () => void;
|
|
37
|
+
height: number;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* The byte gate tripped. Also subtree-replacing. Must offer `model.forceLoad`
|
|
41
|
+
* or the region becomes unreachable for the rest of the session.
|
|
42
|
+
*/
|
|
43
|
+
TooLarge: ComponentType<{
|
|
44
|
+
model: TooLargeMessageModel;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Fetch error, drawn *over* a live canvas rather than replacing it. Mounted
|
|
48
|
+
* unconditionally like the two below, so `visible` (`displayPhase ===
|
|
49
|
+
* 'error'`) is the gate -- don't re-derive it from `model.error`, which is
|
|
50
|
+
* the same subtraction `displayPhase` exists to retire.
|
|
51
|
+
*/
|
|
52
|
+
ErrorBar: ComponentType<{
|
|
53
|
+
model: DisplayErrorBarModel;
|
|
54
|
+
visible: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* The loading scrim. `visible` is `displayPhase === 'loading'`; `immediate`
|
|
58
|
+
* asks it to skip its anti-flash delay because nothing is painted yet.
|
|
59
|
+
*
|
|
60
|
+
* Mounted unconditionally, so it must handle `visible === false` itself — and
|
|
61
|
+
* that is load-bearing rather than a style choice: the anti-flash delay is
|
|
62
|
+
* component state, so a chrome that mounted this only while loading would
|
|
63
|
+
* restart the timer on every activation and never reach it. Keep any
|
|
64
|
+
* replacement mountable-while-hidden for the same reason.
|
|
65
|
+
*/
|
|
66
|
+
Loading: ComponentType<{
|
|
67
|
+
model: DisplayLoadingOverlayModel;
|
|
68
|
+
visible: boolean;
|
|
69
|
+
immediate?: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
/**
|
|
72
|
+
* Status for work with no fetch behind it, while the phase is `ready`.
|
|
73
|
+
* Mounted unconditionally; gates on `visible` itself.
|
|
74
|
+
*
|
|
75
|
+
* **The one state that does not own its own box.** The chrome anchors the
|
|
76
|
+
* bottom-right corner and lays this out there, because that corner is shared
|
|
77
|
+
* with the display's own control row (`BottomRightIndicators`) and two boxes
|
|
78
|
+
* claiming it independently is exactly what they used to do — each pinning
|
|
79
|
+
* itself to `bottom: 2; right: 2` of the same overlay layer, the controls
|
|
80
|
+
* winning on z-index and the status text vanishing underneath. So render an
|
|
81
|
+
* **in-flow** chip: no `position`, no `inset`, no corner offsets. Sizing and
|
|
82
|
+
* colours are yours; placement is not. See `bottomRightCorner.ts`.
|
|
83
|
+
*/
|
|
84
|
+
BackgroundProgress: ComponentType<{
|
|
85
|
+
model: DisplayBackgroundProgressModel;
|
|
86
|
+
visible: boolean;
|
|
87
|
+
}>;
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { default as DisplayUIProvider, resolveOverlays, } from './DisplayUIProvider.tsx';
|
|
2
|
+
export { DisplayChromeOverlayProvider, useChromeOverlayOverride, } from './chromeOverlayContext.ts';
|
|
3
|
+
export { default as plainChromeOverlays } from './plainChromeOverlays.tsx';
|
|
4
|
+
export type { DisplayBackgroundProgressModel, DisplayChromeOverlays, DisplayErrorBarModel, DisplayLoadingOverlayModel, TooLargeMessageModel, } from './chromeOverlays.ts';
|
|
5
|
+
export { TrackControlProvider, useTrackControlOverride, } from './trackControl/trackControlContext.ts';
|
|
6
|
+
export { default as plainTrackControl } from './trackControl/plainTrackControl.tsx';
|
|
7
|
+
export { useTrackControlMenu } from './trackControl/useTrackControlMenu.tsx';
|
|
8
|
+
export type { TrackControlMenu } from './trackControl/useTrackControlMenu.tsx';
|
|
9
|
+
export type { TrackControlComponent, TrackControlIcon, TrackControlOption, TrackControlProps, } from './trackControl/types.ts';
|
|
10
|
+
export { default as FloatingLegend } from './FloatingLegend.tsx';
|
|
11
|
+
export type { LegendItem, LegendSection } from './FloatingLegend.tsx';
|
|
12
|
+
export { default as Tooltip } from './tooltip/Tooltip.tsx';
|
|
13
|
+
export { useTooltip } from './tooltip/useTooltip.tsx';
|
|
14
|
+
export type { TooltipTrigger } from './tooltip/useTooltip.tsx';
|
|
15
|
+
export type { TooltipPlacement } from '@jbrowse/core/ui/BaseTooltip';
|
|
16
|
+
export { PointerLayer } from './PointerLayer.tsx';
|
|
17
|
+
export { TrackOverlayContext } from './trackOverlay/TrackOverlayContext.ts';
|
|
18
|
+
export { TrackOverlayPortal } from './trackOverlay/TrackOverlayPortal.tsx';
|
|
19
|
+
export { TrackOverlaySlot } from './trackOverlay/TrackOverlaySlot.tsx';
|
|
20
|
+
export { tooLargeBannerText } from './tooLargeBannerText.ts';
|
|
21
|
+
export { isLiveModel } from './isLiveModel.ts';
|
|
22
|
+
export { BOTTOM_RIGHT_CONTROLS_ORDER, BottomRightCornerContext, } from './bottomRightCorner.ts';
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The UI a display draws that is not data, and none of it reaches a UI toolkit.
|
|
2
|
+
//
|
|
3
|
+
// This package has no `@mui/*` dependency, which is the guarantee the whole
|
|
4
|
+
// bring-your-own story rests on and the one thing a rendered-element census
|
|
5
|
+
// cannot check. The Material implementations live in
|
|
6
|
+
// `@jbrowse/plugin-linear-genome-view`, which depends on this — never the
|
|
7
|
+
// reverse. See README.md.
|
|
8
|
+
// Both seams at once, defaulting to the plain sets: what an embedder mounts.
|
|
9
|
+
// `resolveOverlays` is the partial-over-plain merge on its own, for code
|
|
10
|
+
// building the context value by hand — it returns one stable object per input,
|
|
11
|
+
// which is what keeps a context value from re-rendering every display.
|
|
12
|
+
export { default as DisplayUIProvider, resolveOverlays, } from './DisplayUIProvider.js';
|
|
13
|
+
// The status states.
|
|
14
|
+
export { DisplayChromeOverlayProvider, useChromeOverlayOverride, } from './chromeOverlayContext.js';
|
|
15
|
+
export { default as plainChromeOverlays } from './plainChromeOverlays.js';
|
|
16
|
+
// The bottom-right controls.
|
|
17
|
+
export { TrackControlProvider, useTrackControlOverride, } from './trackControl/trackControlContext.js';
|
|
18
|
+
export { default as plainTrackControl } from './trackControl/plainTrackControl.js';
|
|
19
|
+
export { useTrackControlMenu } from './trackControl/useTrackControlMenu.js';
|
|
20
|
+
// The one floating legend every display that has colors to explain renders.
|
|
21
|
+
// Here rather than in the LGV plugin because six plugins render it, it reaches
|
|
22
|
+
// no UI toolkit, and it is chrome by the definition this package uses — a thing
|
|
23
|
+
// a display draws that is not its data. `@jbrowse/plugin-linear-genome-view`
|
|
24
|
+
// re-exports it, so its published name is unchanged.
|
|
25
|
+
export { default as FloatingLegend } from './FloatingLegend.js';
|
|
26
|
+
// The hover label every control in here used to delegate to the browser's
|
|
27
|
+
// `title` attribute. The box it draws is `@jbrowse/core`'s `BaseTooltip`, the
|
|
28
|
+
// same one the display tooltips draw, anchored to the control instead of to the
|
|
29
|
+
// cursor — what lives here is the hover, the focus and the dismissal. It stays
|
|
30
|
+
// behind a `lazy()` one module in, which `tooltip/eagerBoundary.test.ts` pins.
|
|
31
|
+
export { default as Tooltip } from './tooltip/Tooltip.js';
|
|
32
|
+
export { useTooltip } from './tooltip/useTooltip.js';
|
|
33
|
+
// What follows the pointer, in a leaf that re-renders alone.
|
|
34
|
+
export { PointerLayer } from './PointerLayer.js';
|
|
35
|
+
// The per-track overlay layer: the node floating chrome escapes into, and the
|
|
36
|
+
// host's half of that portal.
|
|
37
|
+
export { TrackOverlayContext } from './trackOverlay/TrackOverlayContext.js';
|
|
38
|
+
export { TrackOverlayPortal } from './trackOverlay/TrackOverlayPortal.js';
|
|
39
|
+
export { TrackOverlaySlot } from './trackOverlay/TrackOverlaySlot.js';
|
|
40
|
+
// What the byte gate says, so every set that renders the too-large state says
|
|
41
|
+
// the same thing. The measurement behind the gate stays with the mixin that
|
|
42
|
+
// makes it.
|
|
43
|
+
export { tooLargeBannerText } from './tooLargeBannerText.js';
|
|
44
|
+
// The liveness check an overlay's one button needs, for a model that may be an
|
|
45
|
+
// MST node or the plain object the contract invites. Exported because both
|
|
46
|
+
// shipped sets render that button and a bare `isAlive` is wrong in each.
|
|
47
|
+
export { isLiveModel } from './isLiveModel.js';
|
|
48
|
+
// The corner `BackgroundProgress` is laid out in — the half of that state's
|
|
49
|
+
// contract the prop types cannot carry, so it belongs with them rather than
|
|
50
|
+
// with the chrome that happens to mount it.
|
|
51
|
+
export { BOTTOM_RIGHT_CONTROLS_ORDER, BottomRightCornerContext, } from './bottomRightCorner.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #api
|
|
3
|
+
* Whether an overlay's model can still be called into.
|
|
4
|
+
*
|
|
5
|
+
* The terminal states unmount the canvas, so a click landing after the track was
|
|
6
|
+
* closed would otherwise run an action on a destroyed node — which is why both
|
|
7
|
+
* overlay sets guard their one button with `isAlive`.
|
|
8
|
+
*
|
|
9
|
+
* **`isAlive` alone is wrong here, and it throws rather than answering.** The
|
|
10
|
+
* four model shapes in `chromeOverlays.ts` are structural on purpose ("A display
|
|
11
|
+
* satisfies one by having the fields; no mixin has to be composed"), so a host
|
|
12
|
+
* writing their own display over `DisplayChromeBase` may hand these components a
|
|
13
|
+
* plain object. `isAlive` runs `assertIsStateTreeNode` and throws on one, inside
|
|
14
|
+
* an event handler, where React logs it and moves on — leaving the Force load
|
|
15
|
+
* button looking live and doing nothing, which is the exact state
|
|
16
|
+
* `DisplayChromeOverlays.TooLarge` documents itself as existing to prevent.
|
|
17
|
+
*
|
|
18
|
+
* A plain object is never destroyed, so it is always callable. The liveness
|
|
19
|
+
* question only exists for an MST node.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isLiveModel(model: unknown): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isAlive, isStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
2
|
+
/**
|
|
3
|
+
* #api
|
|
4
|
+
* Whether an overlay's model can still be called into.
|
|
5
|
+
*
|
|
6
|
+
* The terminal states unmount the canvas, so a click landing after the track was
|
|
7
|
+
* closed would otherwise run an action on a destroyed node — which is why both
|
|
8
|
+
* overlay sets guard their one button with `isAlive`.
|
|
9
|
+
*
|
|
10
|
+
* **`isAlive` alone is wrong here, and it throws rather than answering.** The
|
|
11
|
+
* four model shapes in `chromeOverlays.ts` are structural on purpose ("A display
|
|
12
|
+
* satisfies one by having the fields; no mixin has to be composed"), so a host
|
|
13
|
+
* writing their own display over `DisplayChromeBase` may hand these components a
|
|
14
|
+
* plain object. `isAlive` runs `assertIsStateTreeNode` and throws on one, inside
|
|
15
|
+
* an event handler, where React logs it and moves on — leaving the Force load
|
|
16
|
+
* button looking live and doing nothing, which is the exact state
|
|
17
|
+
* `DisplayChromeOverlays.TooLarge` documents itself as existing to prevent.
|
|
18
|
+
*
|
|
19
|
+
* A plain object is never destroyed, so it is always callable. The liveness
|
|
20
|
+
* question only exists for an MST node.
|
|
21
|
+
*/
|
|
22
|
+
export function isLiveModel(model) {
|
|
23
|
+
return !isStateTreeNode(model) || isAlive(model);
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DisplayChromeOverlays } from './chromeOverlays.ts';
|
|
2
|
+
/**
|
|
3
|
+
* #api
|
|
4
|
+
* The five `displayPhase` states drawn with no UI toolkit: no theme provider to
|
|
5
|
+
* mount, no emotion in the host page, and nothing that reads as a stray Material
|
|
6
|
+
* widget inside someone else's design system.
|
|
7
|
+
*
|
|
8
|
+
* `DisplayUIProvider` installs this by default, so mount that rather than
|
|
9
|
+
* naming this — reach for it directly only to wrap a state or to build a
|
|
10
|
+
* context value by hand. Colours come from `currentColor` and the CSS system
|
|
11
|
+
* colours, so the host's own cascade drives them in both light and dark.
|
|
12
|
+
*
|
|
13
|
+
* The `data-testid` values it renders are a contract four of JBrowse's test
|
|
14
|
+
* systems key on, so a replacement set that keeps them can be driven by those
|
|
15
|
+
* suites too.
|
|
16
|
+
*/
|
|
17
|
+
declare const plainChromeOverlays: DisplayChromeOverlays;
|
|
18
|
+
export default plainChromeOverlays;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// the subpath, not the `@jbrowse/core/ui` barrel: this file is the MUI-free
|
|
3
|
+
// chrome, and gpuFallback.ts is React-free for exactly that reason
|
|
4
|
+
import { GPU_FALLBACK_LABEL, GPU_FALLBACK_TOOLTIP, disableGpuRendering, shouldOfferGpuFallback, } from '@jbrowse/core/ui/gpuFallback';
|
|
5
|
+
import { progressLabel } from '@jbrowse/core/util/progress';
|
|
6
|
+
import { observer } from 'mobx-react';
|
|
7
|
+
import { isLiveModel } from './isLiveModel.js';
|
|
8
|
+
import { tooLargeBannerText } from './tooLargeBannerText.js';
|
|
9
|
+
import Tooltip from './tooltip/Tooltip.js';
|
|
10
|
+
// A dependency-free `DisplayChromeOverlays` for embedders who don't want MUI:
|
|
11
|
+
// no theme provider to mount, no emotion injected into the host page, and
|
|
12
|
+
// nothing that reads as a stray Material widget inside someone else's design
|
|
13
|
+
// system. Pair it with `DisplayChromeBase` (NOT `DisplayChrome`, which binds
|
|
14
|
+
// the MUI set and would defeat the point by keeping MUI in the module graph).
|
|
15
|
+
//
|
|
16
|
+
// Deliberately plain rather than pretty: every rule below is either layout that
|
|
17
|
+
// the chrome's contract requires, or a hairline so the state is legible against
|
|
18
|
+
// an unknown background. Colors come from `currentColor` and `color-mix` so the
|
|
19
|
+
// host's own cascade drives them. Restyle by wrapping these, or write your own
|
|
20
|
+
// set against the same interface.
|
|
21
|
+
//
|
|
22
|
+
// The testids are NOT cosmetic. `loading-overlay`, `loading-overlay-cancel`,
|
|
23
|
+
// `loading-overlay-retry`, `progress-chip`, `reload_button` and
|
|
24
|
+
// `use_canvas2d_button` are a contract across four test systems (jest, browser
|
|
25
|
+
// tests, screenshot harness, jbrowse-img), so a replacement set that wants to
|
|
26
|
+
// run the existing suites has to keep them.
|
|
27
|
+
// The scrim and the too-large/error banners all need to sit over the canvas
|
|
28
|
+
// without eating pointer events; only the inner content is interactive. Matches
|
|
29
|
+
// the MUI set's behavior, where leaving an interactive box mounted over every
|
|
30
|
+
// track silently swallowed clicks.
|
|
31
|
+
const overlayBox = {
|
|
32
|
+
position: 'absolute',
|
|
33
|
+
inset: 0,
|
|
34
|
+
display: 'flex',
|
|
35
|
+
justifyContent: 'center',
|
|
36
|
+
alignItems: 'flex-start',
|
|
37
|
+
pointerEvents: 'none',
|
|
38
|
+
zIndex: 1,
|
|
39
|
+
};
|
|
40
|
+
// Nearly opaque on purpose. These sit over a canvas whose colours are not known
|
|
41
|
+
// here, and a translucent chip over dense data was measurably hard to read.
|
|
42
|
+
// `Canvas`/`CanvasText` are the CSS system colours, so this tracks the host's
|
|
43
|
+
// light/dark mode without needing a theme object.
|
|
44
|
+
const chip = {
|
|
45
|
+
pointerEvents: 'auto',
|
|
46
|
+
margin: 4,
|
|
47
|
+
padding: '3px 8px',
|
|
48
|
+
fontSize: '0.8rem',
|
|
49
|
+
lineHeight: 1.4,
|
|
50
|
+
color: 'CanvasText',
|
|
51
|
+
border: '1px solid',
|
|
52
|
+
borderColor: 'color-mix(in srgb, CanvasText 35%, transparent)',
|
|
53
|
+
borderRadius: 4,
|
|
54
|
+
background: 'color-mix(in srgb, Canvas 94%, transparent)',
|
|
55
|
+
};
|
|
56
|
+
const button = {
|
|
57
|
+
font: 'inherit',
|
|
58
|
+
color: 'CanvasText',
|
|
59
|
+
marginLeft: 8,
|
|
60
|
+
padding: '0 6px',
|
|
61
|
+
cursor: 'pointer',
|
|
62
|
+
border: '1px solid',
|
|
63
|
+
borderColor: 'color-mix(in srgb, CanvasText 45%, transparent)',
|
|
64
|
+
borderRadius: 3,
|
|
65
|
+
background: 'transparent',
|
|
66
|
+
};
|
|
67
|
+
// Terminal states replace the whole subtree, so unlike the overlays above they
|
|
68
|
+
// own the display box and are the only thing in it.
|
|
69
|
+
function banner(height) {
|
|
70
|
+
return {
|
|
71
|
+
height,
|
|
72
|
+
boxSizing: 'border-box',
|
|
73
|
+
display: 'flex',
|
|
74
|
+
alignItems: 'center',
|
|
75
|
+
justifyContent: 'center',
|
|
76
|
+
gap: 8,
|
|
77
|
+
padding: 8,
|
|
78
|
+
fontSize: '0.8rem',
|
|
79
|
+
border: '1px solid',
|
|
80
|
+
borderColor: 'color-mix(in srgb, currentColor 25%, transparent)',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function errorText(error) {
|
|
84
|
+
return error instanceof Error ? error.message : String(error);
|
|
85
|
+
}
|
|
86
|
+
// Announcement, split across two elements on purpose.
|
|
87
|
+
//
|
|
88
|
+
// The chip carrying a status is a `role="status"` polite live region, so a
|
|
89
|
+
// screen reader reads the phase once when it changes ("Loading", "Clustering
|
|
90
|
+
// samples"). The percentage is deliberately NOT in that region: a determinate
|
|
91
|
+
// status ticks about ten times a second, and a live region carrying the number
|
|
92
|
+
// talks over everything else on the page until the fetch ends.
|
|
93
|
+
//
|
|
94
|
+
// The bar below is where the number goes. Screen readers do not announce
|
|
95
|
+
// `aria-valuenow` changes on a `progressbar`, so the value is reachable on
|
|
96
|
+
// demand and silent otherwise — which is the behaviour a progress indicator
|
|
97
|
+
// should have. `StatusProgressBar` in core does the same thing and is
|
|
98
|
+
// toolkit-free, but it paints `theme.palette.primary.main`; every rule in this
|
|
99
|
+
// file reads the host's own cascade instead, so the bar is drawn here rather
|
|
100
|
+
// than imported.
|
|
101
|
+
function ProgressBar({ fraction }) {
|
|
102
|
+
const filled = Math.min(1, Math.max(0, fraction));
|
|
103
|
+
return (_jsx("div", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(filled * 100), style: {
|
|
104
|
+
overflow: 'hidden',
|
|
105
|
+
width: 60,
|
|
106
|
+
height: 3,
|
|
107
|
+
marginLeft: 6,
|
|
108
|
+
borderRadius: 2,
|
|
109
|
+
background: 'color-mix(in srgb, currentColor 26%, transparent)',
|
|
110
|
+
}, children: _jsx("div", { style: {
|
|
111
|
+
width: '100%',
|
|
112
|
+
height: '100%',
|
|
113
|
+
background: 'currentColor',
|
|
114
|
+
// scaled rather than sized, so an update is a compositor transform
|
|
115
|
+
transformOrigin: 'left center',
|
|
116
|
+
transform: `scaleX(${filled})`,
|
|
117
|
+
transition: 'transform 200ms linear',
|
|
118
|
+
} }) }));
|
|
119
|
+
}
|
|
120
|
+
const PlainRenderError = observer(function PlainRenderError({ error, onRetry, height, }) {
|
|
121
|
+
return (_jsxs("div", { style: banner(height), role: "alert", children: [_jsx("span", { style: { wordBreak: 'break-word' }, children: errorText(error) }), _jsx("button", { type: "button", style: button, "data-testid": "reload_button", onClick: onRetry, children: "Retry" }), shouldOfferGpuFallback(error) ? (_jsx(Tooltip, { title: GPU_FALLBACK_TOOLTIP, children: _jsx("button", { type: "button", style: button, "data-testid": "use_canvas2d_button", onClick: () => {
|
|
122
|
+
disableGpuRendering();
|
|
123
|
+
onRetry();
|
|
124
|
+
}, children: GPU_FALLBACK_LABEL }) })) : null] }));
|
|
125
|
+
});
|
|
126
|
+
const PlainTooLarge = observer(function PlainTooLarge({ model, }) {
|
|
127
|
+
return (_jsxs("div", { style: banner(), role: "status", children: [_jsx("span", { style: { wordBreak: 'break-word' }, children: tooLargeBannerText(model.regionTooLargeReason, {
|
|
128
|
+
zoomCanRelease: model.zoomCanReleaseGate,
|
|
129
|
+
}) }), _jsx("button", { type: "button", style: button, onClick: () => {
|
|
130
|
+
// the banner unmounts the canvas, so a click landing after the track
|
|
131
|
+
// was closed would otherwise call an action on a dead node. Not bare
|
|
132
|
+
// `isAlive` — this model may be the plain object the contract invites,
|
|
133
|
+
// which that throws on. See isLiveModel.ts.
|
|
134
|
+
if (isLiveModel(model)) {
|
|
135
|
+
model.forceLoad();
|
|
136
|
+
}
|
|
137
|
+
}, children: "Force load" })] }));
|
|
138
|
+
});
|
|
139
|
+
const PlainErrorBar = observer(function PlainErrorBar({ model, visible, }) {
|
|
140
|
+
return visible && model.error ? (_jsx("div", { style: overlayBox, role: "alert", children: _jsxs("div", { style: chip, children: [_jsx("span", { style: { wordBreak: 'break-word' }, children: errorText(model.error) }), _jsx("button", { type: "button", style: button, "data-testid": "reload_button", onClick: () => {
|
|
141
|
+
model.reload();
|
|
142
|
+
}, children: "Retry" })] }) })) : null;
|
|
143
|
+
});
|
|
144
|
+
// No anti-flash delay here, unlike the MUI set: that timing is a product
|
|
145
|
+
// judgment JBrowse made for its own UI, and an embedder pacing their own
|
|
146
|
+
// loading affordances shouldn't inherit it invisibly. Wrap this if you want it.
|
|
147
|
+
const PlainLoading = observer(function PlainLoading({ model, visible, }) {
|
|
148
|
+
if (!visible) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const { statusMessage, statusProgress, fetchCanceled } = model;
|
|
152
|
+
return (_jsx("div", { style: overlayBox, "data-testid": "loading-overlay", children: _jsx("div", { style: { ...chip, display: 'flex', alignItems: 'center' }, role: "status", children: fetchCanceled ? (_jsxs(_Fragment, { children: [_jsx("span", { children: "Loading canceled" }), model.reload ? (_jsx("button", { type: "button", style: button, "data-testid": "loading-overlay-retry", onClick: () => {
|
|
153
|
+
model.reload?.();
|
|
154
|
+
}, children: "Retry" })) : null] })) : (_jsxs(_Fragment, { children: [_jsx("span", { children: statusProgress === undefined
|
|
155
|
+
? `${statusMessage || 'Loading'}...`
|
|
156
|
+
: progressLabel(statusMessage || 'Loading', statusProgress) }), statusProgress === undefined ? null : (_jsx(ProgressBar, { fraction: statusProgress })), model.cancelFetchByUser ? (_jsx("button", { type: "button", style: button, "data-testid": "loading-overlay-cancel", onClick: () => {
|
|
157
|
+
model.cancelFetchByUser?.();
|
|
158
|
+
}, children: "Cancel" })) : null] })) }) }));
|
|
159
|
+
});
|
|
160
|
+
const PlainBackgroundProgress = observer(function PlainBackgroundProgress({ model, visible, }) {
|
|
161
|
+
// The only state that renders no positioned box of its own: the chrome
|
|
162
|
+
// anchors the bottom-right corner and shares it with the display's control
|
|
163
|
+
// row, so this is laid out there. Claiming `inset: 0` here would collapse
|
|
164
|
+
// against that box, and pinning to the corner would sit under the row.
|
|
165
|
+
return visible && model.statusMessage ? (_jsxs("div", { style: { ...chip, display: 'flex', alignItems: 'center' }, role: "status", "data-testid": "progress-chip", children: [_jsx("span", { children: progressLabel(model.statusMessage, model.statusProgress) }), model.statusProgress === undefined ? null : (_jsx(ProgressBar, { fraction: model.statusProgress }))] })) : null;
|
|
166
|
+
});
|
|
167
|
+
/**
|
|
168
|
+
* #api
|
|
169
|
+
* The five `displayPhase` states drawn with no UI toolkit: no theme provider to
|
|
170
|
+
* mount, no emotion in the host page, and nothing that reads as a stray Material
|
|
171
|
+
* widget inside someone else's design system.
|
|
172
|
+
*
|
|
173
|
+
* `DisplayUIProvider` installs this by default, so mount that rather than
|
|
174
|
+
* naming this — reach for it directly only to wrap a state or to build a
|
|
175
|
+
* context value by hand. Colours come from `currentColor` and the CSS system
|
|
176
|
+
* colours, so the host's own cascade drives them in both light and dark.
|
|
177
|
+
*
|
|
178
|
+
* The `data-testid` values it renders are a contract four of JBrowse's test
|
|
179
|
+
* systems key on, so a replacement set that keeps them can be driven by those
|
|
180
|
+
* suites too.
|
|
181
|
+
*/
|
|
182
|
+
const plainChromeOverlays = {
|
|
183
|
+
RenderError: PlainRenderError,
|
|
184
|
+
TooLarge: PlainTooLarge,
|
|
185
|
+
ErrorBar: PlainErrorBar,
|
|
186
|
+
Loading: PlainLoading,
|
|
187
|
+
BackgroundProgress: PlainBackgroundProgress,
|
|
188
|
+
};
|
|
189
|
+
export default plainChromeOverlays;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #api
|
|
3
|
+
* Which axis tripped (empty when the display gates without a reason), then the
|
|
4
|
+
* way out.
|
|
5
|
+
*
|
|
6
|
+
* `zoomCanRelease` decides whether "zoom in" is offered, and it has to be asked
|
|
7
|
+
* because the advice is not always true. It was, once: the `AUTO_FORCE_LOAD_BP`
|
|
8
|
+
* floor turned the byte gate off below 20kb, so zooming far enough always
|
|
9
|
+
* worked. The byte gate no longer stops at any floor, and an index quotes whole
|
|
10
|
+
* blocks — so for a file whose blocks are large the same bytes come down however
|
|
11
|
+
* far the user goes, and telling them to keep zooming into a fetch whose cost
|
|
12
|
+
* cannot fall is the one thing the banner must not do. `zoomCanReleaseGate`
|
|
13
|
+
* answers it from two consecutive measurements rather than from a threshold; see
|
|
14
|
+
* `ByteEstimate.zoomIneffective`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function tooLargeBannerText(regionTooLargeReason: string, { zoomCanRelease }?: {
|
|
17
|
+
zoomCanRelease?: boolean;
|
|
18
|
+
}): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// What the byte gate says, and the only part of that gate that belongs to the
|
|
2
|
+
// UI. The measurement side — byte estimates, the sub-floor budget factor,
|
|
3
|
+
// `resolveByteLimit` — stays in `RegionTooLargeMixin`'s own module, which
|
|
4
|
+
// re-exports this so a display keeps one import.
|
|
5
|
+
//
|
|
6
|
+
// It is here because *both* overlay sets render it: JBrowse's Material
|
|
7
|
+
// `TooLargeMessage` and the toolkit-free `PlainTooLarge`, plus anyone writing a
|
|
8
|
+
// third. Wording that drifts between them is a difference nobody would see in
|
|
9
|
+
// review and the screenshot harness keys off the literal.
|
|
10
|
+
/**
|
|
11
|
+
* #api
|
|
12
|
+
* Which axis tripped (empty when the display gates without a reason), then the
|
|
13
|
+
* way out.
|
|
14
|
+
*
|
|
15
|
+
* `zoomCanRelease` decides whether "zoom in" is offered, and it has to be asked
|
|
16
|
+
* because the advice is not always true. It was, once: the `AUTO_FORCE_LOAD_BP`
|
|
17
|
+
* floor turned the byte gate off below 20kb, so zooming far enough always
|
|
18
|
+
* worked. The byte gate no longer stops at any floor, and an index quotes whole
|
|
19
|
+
* blocks — so for a file whose blocks are large the same bytes come down however
|
|
20
|
+
* far the user goes, and telling them to keep zooming into a fetch whose cost
|
|
21
|
+
* cannot fall is the one thing the banner must not do. `zoomCanReleaseGate`
|
|
22
|
+
* answers it from two consecutive measurements rather than from a threshold; see
|
|
23
|
+
* `ByteEstimate.zoomIneffective`.
|
|
24
|
+
*/
|
|
25
|
+
export function tooLargeBannerText(regionTooLargeReason, { zoomCanRelease = true } = {}) {
|
|
26
|
+
return [
|
|
27
|
+
regionTooLargeReason,
|
|
28
|
+
zoomCanRelease
|
|
29
|
+
? 'Zoom in to see features, or force load this track for the rest of the session (may be slow)'
|
|
30
|
+
: 'Force load this track for the rest of the session (may be slow)',
|
|
31
|
+
]
|
|
32
|
+
.filter(f => !!f)
|
|
33
|
+
.join('. ');
|
|
34
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TooltipPlacement } from '@jbrowse/core/ui/BaseTooltip';
|
|
2
|
+
import type { ReactElement, ReactNode, Ref } from 'react';
|
|
3
|
+
interface TriggerProps {
|
|
4
|
+
ref?: Ref<HTMLElement>;
|
|
5
|
+
onPointerEnter?: (event: React.PointerEvent<HTMLElement>) => void;
|
|
6
|
+
onPointerLeave?: (event: React.PointerEvent<HTMLElement>) => void;
|
|
7
|
+
onPointerDown?: (event: React.PointerEvent<HTMLElement>) => void;
|
|
8
|
+
onFocus?: (event: React.FocusEvent<HTMLElement>) => void;
|
|
9
|
+
onBlur?: (event: React.FocusEvent<HTMLElement>) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* #api
|
|
13
|
+
* A hover/focus label for a control, drawn rather than delegated to the
|
|
14
|
+
* browser's `title` attribute — positioned so it clears the display's
|
|
15
|
+
* `contain: strict` box and the window edge, dismissed by Escape, and drawn
|
|
16
|
+
* like every other JBrowse tooltip instead of like whatever the host OS
|
|
17
|
+
* renders.
|
|
18
|
+
*
|
|
19
|
+
* `title` is what this chrome used to use, and it was the wrong tool three
|
|
20
|
+
* ways: it can be neither styled nor positioned, it waits about a second and
|
|
21
|
+
* then disappears on a timer of its own, and on a control that already carries
|
|
22
|
+
* an `aria-label` some screen readers announce both strings. This reaches no UI
|
|
23
|
+
* toolkit, so the package's no-Material-UI guarantee holds.
|
|
24
|
+
*
|
|
25
|
+
* Takes a single element child and clones it rather than wrapping it: the
|
|
26
|
+
* controls that want a tooltip are absolutely positioned inside a legend or sit
|
|
27
|
+
* in a flex row, where an extra `<span>` moves them. The child keeps its own
|
|
28
|
+
* handlers — these compose on top of them. This is the same trigger pattern
|
|
29
|
+
* MUI's own `Tooltip` uses, which is what the rest of JBrowse's chrome is built
|
|
30
|
+
* on; the toolkit is what differs here, not the shape.
|
|
31
|
+
*
|
|
32
|
+
* ```tsx
|
|
33
|
+
* <Tooltip title="Hide legend">
|
|
34
|
+
* <button type="button" aria-label="Hide legend" onClick={onDismiss}>
|
|
35
|
+
* ×
|
|
36
|
+
* </button>
|
|
37
|
+
* </Tooltip>
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* **The child still needs its own accessible name**, because this sets
|
|
41
|
+
* `aria-describedby` and never `aria-label` — see {@link useTooltip}, which is
|
|
42
|
+
* this without the cloning, for a host writing its own markup.
|
|
43
|
+
*/
|
|
44
|
+
export default function Tooltip({ title, placement, children, }: {
|
|
45
|
+
/** What the bubble says. Nothing renders while this is empty. */
|
|
46
|
+
title: ReactNode;
|
|
47
|
+
/** Preferred side. `flip()` overrides it near a viewport edge. */
|
|
48
|
+
placement?: TooltipPlacement;
|
|
49
|
+
/** Exactly one element, cloned rather than wrapped. */
|
|
50
|
+
children: ReactElement<TriggerProps>;
|
|
51
|
+
}): import("react").JSX.Element;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cloneElement } from 'react';
|
|
3
|
+
import { useTooltip } from './useTooltip.js';
|
|
4
|
+
function compose(theirs, ours) {
|
|
5
|
+
return (event) => {
|
|
6
|
+
theirs?.(event);
|
|
7
|
+
ours(event);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* #api
|
|
12
|
+
* A hover/focus label for a control, drawn rather than delegated to the
|
|
13
|
+
* browser's `title` attribute — positioned so it clears the display's
|
|
14
|
+
* `contain: strict` box and the window edge, dismissed by Escape, and drawn
|
|
15
|
+
* like every other JBrowse tooltip instead of like whatever the host OS
|
|
16
|
+
* renders.
|
|
17
|
+
*
|
|
18
|
+
* `title` is what this chrome used to use, and it was the wrong tool three
|
|
19
|
+
* ways: it can be neither styled nor positioned, it waits about a second and
|
|
20
|
+
* then disappears on a timer of its own, and on a control that already carries
|
|
21
|
+
* an `aria-label` some screen readers announce both strings. This reaches no UI
|
|
22
|
+
* toolkit, so the package's no-Material-UI guarantee holds.
|
|
23
|
+
*
|
|
24
|
+
* Takes a single element child and clones it rather than wrapping it: the
|
|
25
|
+
* controls that want a tooltip are absolutely positioned inside a legend or sit
|
|
26
|
+
* in a flex row, where an extra `<span>` moves them. The child keeps its own
|
|
27
|
+
* handlers — these compose on top of them. This is the same trigger pattern
|
|
28
|
+
* MUI's own `Tooltip` uses, which is what the rest of JBrowse's chrome is built
|
|
29
|
+
* on; the toolkit is what differs here, not the shape.
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <Tooltip title="Hide legend">
|
|
33
|
+
* <button type="button" aria-label="Hide legend" onClick={onDismiss}>
|
|
34
|
+
* ×
|
|
35
|
+
* </button>
|
|
36
|
+
* </Tooltip>
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* **The child still needs its own accessible name**, because this sets
|
|
40
|
+
* `aria-describedby` and never `aria-label` — see {@link useTooltip}, which is
|
|
41
|
+
* this without the cloning, for a host writing its own markup.
|
|
42
|
+
*/
|
|
43
|
+
export default function Tooltip({ title, placement, children, }) {
|
|
44
|
+
const { triggerProps, tooltip } = useTooltip(title, { placement });
|
|
45
|
+
const props = children.props;
|
|
46
|
+
// eslint-disable-next-line @eslint-react/no-clone-element -- wrapping instead would move the absolutely-positioned and flex-row controls this exists for
|
|
47
|
+
const trigger = cloneElement(children, {
|
|
48
|
+
...triggerProps,
|
|
49
|
+
ref: (node) => {
|
|
50
|
+
;
|
|
51
|
+
triggerProps.ref(node);
|
|
52
|
+
const theirs = props.ref;
|
|
53
|
+
if (typeof theirs === 'function') {
|
|
54
|
+
theirs(node);
|
|
55
|
+
}
|
|
56
|
+
else if (theirs) {
|
|
57
|
+
theirs.current = node;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
onPointerEnter: compose(props.onPointerEnter, triggerProps.onPointerEnter),
|
|
61
|
+
onPointerLeave: compose(props.onPointerLeave, triggerProps.onPointerLeave),
|
|
62
|
+
onPointerDown: compose(props.onPointerDown, triggerProps.onPointerDown),
|
|
63
|
+
onFocus: compose(props.onFocus, triggerProps.onFocus),
|
|
64
|
+
onBlur: compose(props.onBlur, triggerProps.onBlur),
|
|
65
|
+
});
|
|
66
|
+
return (_jsxs(_Fragment, { children: [trigger, tooltip] }));
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { TooltipPlacement } from '@jbrowse/core/ui/BaseTooltip';
|
|
2
|
+
import type { ReactNode, Ref } from 'react';
|
|
3
|
+
export interface TooltipTrigger {
|
|
4
|
+
/** Whether the bubble is up, for a caller that wants to style the trigger. */
|
|
5
|
+
open: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Spread onto the control the tooltip describes. `aria-describedby`, not
|
|
8
|
+
* `aria-label`: the tooltip is a visual affordance, and a name pointing at a
|
|
9
|
+
* node that exists only during a hover leaves the control unnamed the rest of
|
|
10
|
+
* the time. **The control still needs its own accessible name.**
|
|
11
|
+
*/
|
|
12
|
+
triggerProps: {
|
|
13
|
+
ref: Ref<HTMLElement>;
|
|
14
|
+
'aria-describedby': string | undefined;
|
|
15
|
+
onPointerEnter: (event: React.PointerEvent<HTMLElement>) => void;
|
|
16
|
+
onPointerLeave: () => void;
|
|
17
|
+
onPointerDown: () => void;
|
|
18
|
+
onFocus: () => void;
|
|
19
|
+
onBlur: () => void;
|
|
20
|
+
};
|
|
21
|
+
/** Render this. It is `null` until the bubble is up, and portals itself. */
|
|
22
|
+
tooltip: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* #api
|
|
26
|
+
* A hover/focus label for one control, as props to spread — the headless half
|
|
27
|
+
* of {@link Tooltip}, for a host writing its own chrome rather than restyling
|
|
28
|
+
* ours. Same relationship `useTrackControlMenu` has to `plainTrackControl`.
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const { triggerProps, tooltip } = useTooltip('Hide legend')
|
|
32
|
+
* return (
|
|
33
|
+
* <>
|
|
34
|
+
* <button {...triggerProps} aria-label="Hide legend" onClick={onDismiss}>
|
|
35
|
+
* ×
|
|
36
|
+
* </button>
|
|
37
|
+
* {tooltip}
|
|
38
|
+
* </>
|
|
39
|
+
* )
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* `triggerProps` carries no `onClick`, so a control's own handler does not
|
|
43
|
+
* collide with it. Any other handler on this list has to compose rather than
|
|
44
|
+
* replace — spread first, then call `triggerProps.onFocus` from yours.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useTooltip(
|
|
47
|
+
/** What the bubble says. It stays down while this is empty. */
|
|
48
|
+
title: ReactNode, { placement }?: {
|
|
49
|
+
placement?: TooltipPlacement;
|
|
50
|
+
}): TooltipTrigger;
|