mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(street-level): modal viewer, pinned redirects, real-route CI checks
From the IC8 review of #768, each with tests that fail before: - P2: the expanded viewer is a real modal: while it is on screen every other <body> child is inert (only the ones it set are restored, on shrink, close and destroy, and when Clean View/recording/cockpit hide it). The voice pill and toasts are not clickable while it is expanded. - P2: the tile proxy follows redirects by hand, at most 3 hops, each one HTTPS on exactly https://tiles.mapillary.com without credentials; anything else is a 502 and the token never leaves that origin. - P2: the real dev and preview server smoke tests request /api/mapillary/status and a tile, and the browser gate first checks the server's real status route before any interception, so CI fails if the Mapillary plugin is not registered. - P2: both gates press panel headers through one shared helper (scripts/qa-panelDrag.mjs): re-resolve the header until it is under the pointer, record what the press hit, wait on the real floating/docked state, and retry only a press that provably missed the header. Verified: unit 5910 pass; fixture gate 17/17; panel gate 5/5; live gate with a real Mapillary token 22/22. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bd97d38a99
commit
038d8facc2
12 files changed
+881
-97
No files matched your search
@@ -57,6 +57,13 @@ export class StreetLevelControls {
|
||||
this._resizeQueued = false;
|
||||
this._wrapHome = null;
|
||||
this._expandReturnFocus = null;
|
||||
/**
|
||||
* The <body> children this class made inert while the expanded viewer is
|
||||
* a modal on screen; only these are released again.
|
||||
* @type {Set<Element>}
|
||||
*/
|
||||
this._inerted = new Set();
|
||||
this._modalObserver = null;
|
||||
this._elements = this._collect();
|
||||
this._bind();
|
||||
}
|
||||
@@ -332,7 +339,10 @@ export class StreetLevelControls {
|
||||
wrap.setAttribute('aria-label', 'Street-level image');
|
||||
wrap.tabIndex = -1;
|
||||
wrap.focus({ preventScroll: true });
|
||||
this._watchModal(true);
|
||||
} else {
|
||||
// Release the application first: the focus returned below may be in it.
|
||||
this._watchModal(false);
|
||||
wrap.classList.remove('sl-viewer-wrap-expanded');
|
||||
wrap.removeAttribute('role');
|
||||
wrap.removeAttribute('aria-modal');
|
||||
@@ -391,9 +401,56 @@ export class StreetLevelControls {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the application modal-inert while the expanded viewer is on screen:
|
||||
* every other <body> child is `inert` (one already inert is left alone), so
|
||||
* no control around the viewer takes a click or focus. A viewer that CSS
|
||||
* hides (Clean View, recording, cockpit) releases them, since a hidden
|
||||
* modal must not leave the app dead. Returns whether the dialog is shown.
|
||||
*/
|
||||
_syncModal() {
|
||||
if (this.destroyed || !this._isDialogShown()) {
|
||||
this._releaseModal();
|
||||
return false;
|
||||
}
|
||||
const wrap = this._elements.viewerWrap;
|
||||
for (const node of [...(document.body?.children || [])]) {
|
||||
if (node === wrap || node.inert) continue;
|
||||
node.inert = true;
|
||||
this._inerted.add(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Give the application back every node `_syncModal` made inert. */
|
||||
_releaseModal() {
|
||||
for (const node of this._inerted) node.inert = false;
|
||||
this._inerted.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* While expanded, re-check the modal whenever <body> changes class (the
|
||||
* modes that hide the viewer) or gains a child (which must be inert too).
|
||||
* Shrinking stops watching and releases the application.
|
||||
*/
|
||||
_watchModal(on) {
|
||||
this._modalObserver?.disconnect();
|
||||
this._modalObserver = null;
|
||||
if (!on) return this._releaseModal();
|
||||
if (typeof MutationObserver === 'function' && document.body) {
|
||||
this._modalObserver = new MutationObserver(() => this._syncModal());
|
||||
this._modalObserver.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
childList: true,
|
||||
});
|
||||
}
|
||||
this._syncModal();
|
||||
}
|
||||
|
||||
_onDialogKey(event) {
|
||||
if (event.key !== 'Escape' && event.key !== 'Tab') return;
|
||||
if (!this._isDialogShown()) return;
|
||||
if (!this._syncModal()) return;
|
||||
// Only keys meant for the viewer: from inside it, from nowhere (<body>)
|
||||
// or from the globe. A field elsewhere (search) keeps its Esc and Tab.
|
||||
const target = event.target;
|
||||
@@ -607,6 +664,7 @@ export class StreetLevelControls {
|
||||
if (this.destroyed) return;
|
||||
this.destroyed = true;
|
||||
this.setViewerExpanded(false);
|
||||
this._watchModal(false);
|
||||
this.listeners.abort();
|
||||
this._resizeObserver?.disconnect();
|
||||
this._resizeObserver = null;
|
||||
|
||||
Reference in new issue
Block a user