fix(street-level): viewer lifecycle, hover cost, keyboard focus, panel lift

From the IC8 review of #768 (P2s), each with a test that fails before:

- Switching the layer off and on during the first photo download no longer
  leaves the viewer stuck on "not mounted": an outdated mount only unmounts
  an adapter nobody else holds and subscribes poses only once current, and
  the MapillaryJS adapter never builds a viewer after it was unmounted.
- Hover picks at most every 120 ms (the CCTV convention), not while the
  camera moves or a button is held, and not after the layer went off.
- Esc shrinks the expanded viewer wherever focus is (document capture), so
  the map keeps its selection, and Tab from outside returns into it.
  Closing a photo moves focus to the panel instead of dropping it on body.
- Portable panels lift only after a mouse or pen moves past the drag
  threshold: a click on a docked panel's edge or a touch swipe on its
  header no longer turns it into a floating window, and pointercancel
  saves nothing and puts the panel back.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-05 15:55:18 +01:00
1 parent b1924ebb3e
commit 539d514adb
10 files changed
+793 -101

No files matched your search

@@ -14,6 +14,8 @@ export function createMapillaryViewer({ source, render } = {}) {
let prewarming = false;
/** In-flight viewer construction, so pre-warm and open never build two. */
let creating = null;
/** Bumped by `unmount`, so a construction it overtook builds nothing. */
let generation = 0;
let renderMode = 'letterbox';
/** Metadata of the image on screen; pov/position events reuse it. */
let current = null;
@@ -106,8 +108,12 @@ export function createMapillaryViewer({ source, render } = {}) {
}
async function createViewer(host) {
const built = generation;
destroyViewer();
const { Viewer } = await ensureLibrary();
// Unmounted (layer off) while the library loaded: a viewer built now
// would hold a WebGL context nobody releases.
if (built !== generation) throw new Error('Mapillary viewer was unmounted');
viewer = new Viewer({
accessToken: source.token,
container: host,
@@ -165,6 +171,9 @@ export function createMapillaryViewer({ source, render } = {}) {
unmount() {
pendingOpen = null;
// The next mount builds afresh rather than joining a stale build.
generation++;
creating = null;
destroyViewer();
},