display-ui
Auto-generated from exported functions tagged #api in the source. See
imports and re-exports for how
to import these from a plugin.
BottomRightCornerContext
The one box per display that owns the bottom-right corner.
It lives beside the overlay contract rather than with the chrome that mounts it,
because it is the half of that contract the types cannot carry:
DisplayChromeOverlays.BackgroundProgress is the one state told to render an
in-flow chip with no position and no corner offsets, and this is the box it is
laid out in. A host writing their own display over DisplayChromeBase needs to
be able to build that corner; while this sat one layer up in the LGV plugin, the
rule was a paragraph they could only obey by hand.
Two independent things want that corner and neither could see the other: the
display's own control row (BottomRightIndicators — track sizing, the isoform
notice, the solo chip) and the chrome's background-progress chip
(DisplayChromeOverlays.BackgroundProgress, the status channel for work with no
fetch behind it). Both portal into the same per-track overlay node, and both
used to claim bottom: 2; right: 2 there with their own position: absolute
box — so they simply drew on top of each other, the controls winning on z-index
and the status text disappearing under them.
It has never been reachable: the two displays that render the control row
(alignments, canvas) are not among the four that report a ready-phase status
(clustering, on multi-wiggle / multi-row features / maf / the multi-sample
variant pair). That is the reason to make it structural rather than to leave
it — nothing on either side is aware of the constraint, so the first display to
want both would find the bug, and it presents as a status message that silently
never appears.
BottomRightIndicators already described itself as "the single anchor point for
every bottom-right overlay ... so they lay out as one row instead of each
picking their own position and colliding". This makes that true of the chip too:
the chrome renders the anchor, puts its chip in it, and publishes the node here
so the display's row — rendered several components away, inside the chrome's
body — lands in the same flex box instead of over it.
null outside a chrome (a display an embedder mounts standalone, a unit test,
the SVG export), where BottomRightIndicators keeps its own anchored box. That
fallback is why this can be added without every consumer changing.
// type signature
Context<HTMLElement | null>
DisplayUIProvider
Both bring-your-own seams at once: what a display draws that is not data.
There are two contexts underneath because two different things render them — the
chrome around a display draws the status states, the display itself draws its
bottom-right controls — and that split is real at the implementation level,
since DisplayChromeBase takes its overlay set as a prop and never renders a
track control. It is not real for an embedder: nobody wants stock Material
loading scrims with plain corner controls, or the reverse. Every consumer in
this repo mounts the two together.
<DisplayUIProvider>{tracks}</DisplayUIProvider>
Both props default to the plain, toolkit-free sets, so the common case — "I do not want Material UI in my app" — needs no arguments and no second import.
overlays is a partial set, merged over the plain one, so replacing a
single state is one entry rather than five:
<DisplayUIProvider overlays={{ ErrorBar: MyErrorBar }}>
Partial rather than whole for two reasons. A host writing four of the five
states by hand only ever wanted one of them, and every example of this had
spread plainChromeOverlays in to say so. And a sixth state is a thing
JBrowse can add: with a whole set the host's object goes stale on upgrade — a
compile error if they typecheck, a missing component if they ship JS — while a
partial one keeps working and picks up the new plain default.
Declare that object at module scope if you can. The merge is stable per object
(resolveOverlays), so a constant costs one merge for the life of the app; a
literal written inline in JSX is a new object every render, and this value goes
into a context, so every display beneath re-renders with it.
The contexts themselves still default to undefined, and that stays true. A
display rendering outside any provider — a unit test, the SVG export,
breakpoint-split-view's overlayUtils — keeps JBrowse's own Material look,
because a plain ambient default would degrade those invisibly. Defaulting this
component's props is a different thing: mounting it is a deliberate act, and
the act means "not the Material default". Nothing gets a plain set without
someone having asked.
Colors are not a seam and are not here. A display reads usePalette() for its
own content colors, which is a palette of strings rather than a UI toolkit, so
it arrives through SessionPaletteProvider (@jbrowse/core/ui/PaletteContext)
whatever these are set to. A feature track needs it even with plain chrome.
This module reaches no @mui/* module, and muiFree.test.ts keeps it that way
— asking for less Material UI must not download more of it. What it cannot do is
unship the Material components a stock display imports: DisplayChrome and
TrackControl are in that display's chunk either way, and merely stop
rendering. Keeping them out of the graph means writing your own display
component over DisplayChromeBase, which takes overlays as a prop and imports
no toolkit. See
DISPLAYCHROME.md.
// type signature
({ overlays, trackControl, children, }: { overlays?: Partial<…> | undefined; trackControl?: TrackControlComponent | undefined; children: ReactNode; }) => Element
isLiveModel
Whether an overlay's model can still be called into.
The terminal states unmount the canvas, so a click landing after the track was
closed would otherwise run an action on a destroyed node — which is why both
overlay sets guard their one button with isAlive.
isAlive alone is wrong here, and it throws rather than answering. The four
model shapes in chromeOverlays.ts are structural on purpose ("A display
satisfies one by having the fields; no mixin has to be composed"), so a host
writing their own display over DisplayChromeBase may hand these components a
plain object. isAlive runs assertIsStateTreeNode and throws on one, inside
an event handler, where React logs it and moves on — leaving the Force load
button looking live and doing nothing, which is the exact state
DisplayChromeOverlays.TooLarge documents itself as existing to prevent.
A plain object is never destroyed, so it is always callable. The liveness question only exists for an MST node.
// type signature
(model: unknown) => boolean
plainChromeOverlays
The five displayPhase states drawn with no UI toolkit: no theme provider to
mount, no emotion in the host page, and nothing that reads as a stray Material
widget inside someone else's design system.
DisplayUIProvider installs this by default, so mount that rather than naming
this — reach for it directly only to wrap a state or to build a context value by
hand. Colours come from currentColor and the CSS system colours, so the host's
own cascade drives them in both light and dark.
The data-testid values it renders are a contract four of JBrowse's test
systems key on, so a replacement set that keeps them can be driven by those
suites too.
// type signature
DisplayChromeOverlays
PlainTrackControl
A display's ambient bottom-right control — track sizing, the isoform notice, the
show-only badge — drawn with no UI toolkit, the corner-control counterpart of
plainChromeOverlays.
DisplayUIProvider installs this by default. The behaviour is not in here:
dismissal, the keyboard, focus, the top layer and the anchoring are
useTrackControlMenu, so writing your own control means writing markup rather
than re-deriving why the menu opens upward.
The package exports it as plainTrackControl, lower-cased to match
plainChromeOverlays — the two are a pair, and both are things you hand to a
provider rather than render yourself.
// type signature
({ icon, tooltip, label, options, onClick, onMenuClose, onDelete, warning, }: TrackControlProps) => Element
tooLargeBannerText
Which axis tripped (empty when the display gates without a reason), then the way out.
zoomCanRelease decides whether "zoom in" is offered, and it has to be asked
because the advice is not always true. It was, once: the AUTO_FORCE_LOAD_BP
floor turned the byte gate off below 20kb, so zooming far enough always worked.
The byte gate no longer stops at any floor, and an index quotes whole blocks —
so for a file whose blocks are large the same bytes come down however far the
user goes, and telling them to keep zooming into a fetch whose cost cannot fall
is the one thing the banner must not do. zoomCanReleaseGate answers it from
two consecutive measurements rather than from a threshold; see
ByteEstimate.zoomIneffective.
// type signature
(regionTooLargeReason: string, { zoomCanRelease }?: { zoomCanRelease?: boolean | undefined; }) => string
Tooltip
A hover/focus label for a control, drawn rather than delegated to the browser's
title attribute — positioned so it clears the display's contain: strict box
and the window edge, dismissed by Escape, and drawn like every other JBrowse
tooltip instead of like whatever the host OS renders.
title is what this chrome used to use, and it was the wrong tool three ways:
it can be neither styled nor positioned, it waits about a second and then
disappears on a timer of its own, and on a control that already carries an
aria-label some screen readers announce both strings. This reaches no UI
toolkit, so the package's no-Material-UI guarantee holds.
Takes a single element child and clones it rather than wrapping it: the controls
that want a tooltip are absolutely positioned inside a legend or sit in a flex
row, where an extra <span> moves them. The child keeps its own handlers —
these compose on top of them. This is the same trigger pattern MUI's own
Tooltip uses, which is what the rest of JBrowse's chrome is built on; the
toolkit is what differs here, not the shape.
<Tooltip title="Hide legend">
<button type="button" aria-label="Hide legend" onClick={onDismiss}>
×
</button>
</Tooltip>
The child still needs its own accessible name, because this sets
aria-describedby and never aria-label — see useTooltip, which is this
without the cloning, for a host writing its own markup.
// type signature
({ title, placement, children, }: { title: ReactNode; placement?: TooltipPlacement | undefined; children: ReactElement<…>; }) => Element
TrackOverlayPortal
Lift floating track chrome out of the display's contain: strict sandbox and
into the host's overlay node, so the LGV's inter-region masks cannot bury it at
multi-region scale. TrackOverlaySlot is the other end.
The overlay node takes no pointer events, so anything of yours the user hovers
or clicks sets pointer-events: auto on its own positioned box.
With no slot above it this renders the children in place (fallbackInline, the
default), which preserves chrome that is still meaningful unescaped. Pass
fallbackInline={false} for chrome that only makes sense above the masks and
draws itself some other way elsewhere.
// type signature
({ children, fallbackInline, }: { children: ReactNode; fallbackInline?: boolean | undefined; }) => ReactNode
TrackOverlaySlot
The box a display is mounted in, plus the overlay node its floating chrome
escapes into. Pair to TrackOverlayPortal, which is the other end.
A display's React tree is sealed in a contain: strict sandbox — that is what
isolates its paint, and dropping it is measured and rejected (ADR-058). A
stacking context comes with the isolation, so floating chrome a display draws (a
colour key, hi-c's overlay panel, maf's row labels) cannot out-z-index anything
painted over the track stack from outside. The escape is a node mounted beside
the sandbox rather than inside it, published through TrackOverlayContext; this
component is that node, its context and the paint order between them, in the one
place they have to agree.
TrackContainer uses it, so JBrowse's own layout and an embedder's go through
the same code rather than two copies of one rule. An embedder mounting
RenderingComponent directly needs it too, and that is the case it was added
for: with no provider the context is null, the portal falls back to rendering
inline, and a host that paints region seams over its column buries the chrome
under them with nothing to say so.
<TrackOverlaySlot zIndex={3} style={{ height: display.height }}>
<div style={{ position: 'absolute', inset: 0, contain: 'strict' }}>
<RenderingComponent model={display} />
</div>
</TrackOverlaySlot>
zIndex is required, and deliberately has no default. It is the answer to
"above what?", and that is a fact about the caller's layout rather than about
this component: JBrowse's own track container passes 100, which is positioned
above PaddingBlocks and below TrackLabel at 200, and means nothing to a host
whose masks sit at 2. A default would be a number that is right in one layout
and silently wrong in every other, and the failure — chrome painted under a mask
— is invisible until someone looks at the right zoom.
The node takes no pointer events, so it does not eat the canvas's. Chrome that
wants them takes them back on its own positioned box; it also carries
data-gesture-owner, so anything that does is already exempt from the LGV's
click-drag pan.
// type signature
({ children, zIndex, style, overlayStyle, }: { children: ReactNode; zIndex: number; style?: CSSProperties | undefined; overlayStyle?: CSSProperties | undefined; }) => Element
useTooltip
A hover/focus label for one control, as props to spread — the headless half of
Tooltip, for a host writing its own chrome rather than restyling ours. Same
relationship useTrackControlMenu has to plainTrackControl.
const { triggerProps, tooltip } = useTooltip('Hide legend')
return (
<>
<button {...triggerProps} aria-label="Hide legend" onClick={onDismiss}>
×
</button>
{tooltip}
</>
)
triggerProps carries no onClick, so a control's own handler does not collide
with it. Any other handler on this list has to compose rather than replace —
spread first, then call triggerProps.onFocus from yours.
// type signature
(title: ReactNode, { placement }?: { placement?: TooltipPlacement | undefined; }) => TooltipTrigger
useTrackControlMenu
The behaviour behind a bottom-right track control's menu, as prop getters to
spread — dismissal (Escape, an outside press, an ancestor scrolling), the
keyboard (arrows, Home/End), focus, and the anchoring that clears both the
display's contain: strict box and the window edge.
For writing your own control rather than restyling plainTrackControl: each of
those rules is a bug when missed and none of them shows up in a screenshot.
Render menuProps only while open, and portal it to document.body —
createPortal is the caller's to aim, the maths is here. menuProps.style
carries position only.
// type signature
(onClose?: (() => void) | undefined) => TrackControlMenu
Related links
- Guide: Dependencies and re-exports