From 209dda55070deddcde795d7a655f72a0c37c66db Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 8 Oct 2026 15:01:42 +0400 Subject: [PATCH] refactor(street-level): open the panel from the switch-on request's origin only Co-Authored-By: Claude Opus 5.5 --- src/ui/streetLevelControls.js | 38 +++++----------- src/ui/streetLevelControls.test.mjs | 69 +++++++++-------------------- 2 files changed, 31 insertions(+), 76 deletions(-) diff --git a/src/ui/streetLevelControls.js b/src/ui/streetLevelControls.js index c8f0c18e2..80411f6a5 100644 --- a/src/ui/streetLevelControls.js +++ b/src/ui/streetLevelControls.js @@ -38,12 +38,8 @@ export class StreetLevelControls { this._state = null; this._wasEnabled = null; this._wasOpen = false; - // Until the user first touches the app, an off → on is the saved layer - // state being restored, not a user switching the layer on. - this._restoreWindow = true; - // Whether the last switch-on was explicit (user, voice, tool) rather than - // a restore; null when the shell does not report request origins. - this._explicitEnable = null; + // Whether the last switch-on was explicit (user, voice, tool), not a restore. + this._explicitEnable = false; this._unsubscribeEnableRequests = null; this._resizeQueued = false; this._elements = this._collect(); @@ -147,11 +143,6 @@ export class StreetLevelControls { } catch { /* storage unavailable */ } - const endRestoreWindow = () => { - this._restoreWindow = false; - }; - for (const type of ['pointerdown', 'keydown']) - this.listen(document, type, endRestoreWindow, { capture: true }); // MapillaryJS only tracks window resizes; the panel resizes on its own. if (typeof ResizeObserver === 'function' && el.viewer) { this._resizeObserver = new ResizeObserver(() => this._requestResize()); @@ -212,9 +203,7 @@ export class StreetLevelControls { /** Through the data manager, so saved state and share links record it. */ _setParams(params) { - if (this.actions.setParams) - this.actions.setParams(params, { origin: 'user' }); - else this.layer.setParams?.(params); + this.actions.setParams(params, { origin: 'user' }); } /** Whether EXPAND has the viewer full screen (or, on iPhone, open over the page). */ @@ -308,10 +297,11 @@ export class StreetLevelControls { this._unsubscribe = null; if (this.destroyed || !this.root) return; this._unsubscribeEnableRequests?.(); - this._unsubscribeEnableRequests = - this.actions.subscribeEnableRequests?.((origin) => { + this._unsubscribeEnableRequests = this.actions.subscribeEnableRequests( + (origin) => { this._explicitEnable = isExplicitLayerStateOrigin(origin); - }) || null; + }, + ); this._unsubscribe = this.layer.subscribe?.((state) => this.render(state)); if (this.layer.getUIState) this.render(this.layer.getUIState()); } @@ -441,17 +431,9 @@ export class StreetLevelControls { _reactToTransitions(view) { const enabled = view.enabled; if (enabled && this._wasEnabled === false) { - // A restore must not reopen a panel the user or a share link kept - // collapsed. Without request origins, the restore window decides. - const preference = this.root.dataset?.collapsedPreference; - const restoring = - this._explicitEnable === null - ? this._restoreWindow && - (preference === 'stored' || preference === 'share') - : !this._explicitEnable; - if (!restoring) this.setCollapsed(false, { persist: false }); - this._restoreWindow = false; - this._explicitEnable = this._explicitEnable === null ? null : false; + // A restore must not reopen a panel the user or a share link kept collapsed. + if (this._explicitEnable) this.setCollapsed(false, { persist: false }); + this._explicitEnable = false; } this._wasEnabled = enabled; // Other size changes reach the viewer through the ResizeObserver; opening diff --git a/src/ui/streetLevelControls.test.mjs b/src/ui/streetLevelControls.test.mjs index 1cf0b5f7c..ee6804013 100644 --- a/src/ui/streetLevelControls.test.mjs +++ b/src/ui/streetLevelControls.test.mjs @@ -293,6 +293,7 @@ async function productionPanel(dom) { manager.setLayerParams('street-level', params, options), setPanelCollapsed() {}, showToast() {}, + subscribeEnableRequests: () => () => {}, }, }); controls.connect(); @@ -402,6 +403,8 @@ function stubPanel(dom, state, extraActions = {}) { const layer = stubLayer(); const calls = { setParams: [], setEnabled: [], collapsed: [] }; let enabled = state.enabled; + /** Reports a switch-on request's origin, as the data manager does. */ + let announce = null; layer.publish(state); const controls = new StreetLevelControls({ root: dom.root, @@ -416,11 +419,23 @@ function stubPanel(dom, state, extraActions = {}) { setPanelCollapsed: (collapsed, options) => calls.collapsed.push([collapsed, options]), showToast() {}, + subscribeEnableRequests: (listener) => { + announce = listener; + return () => { + announce = null; + }; + }, ...extraActions, }, }); controls.connect(); - return { layer, calls, controls }; + return { + layer, + calls, + controls, + announce: (origin) => announce?.(origin), + subscribed: () => announce !== null, + }; } test('the header pill switches the layer, bringing back Mapillary switched off elsewhere', () => @@ -634,56 +649,12 @@ for (const mode of ['ui-clean-view', 'recording-mode', 'cockpit-mode']) /* ── Restores and keyboard rules ───────────────────────────────────────── */ -test('a restored layer does not reopen a panel the user collapsed, and automatic opens are never stored (P2-1)', () => +test('only an explicit switch-on opens the panel, unstored; a restore never does (P2-1)', () => withDom(async (dom) => { - // The panel chrome restored the user's stored "collapsed" before connect. - dom.root.dataset.collapsedPreference = 'stored'; - const { layer, calls, controls } = stubPanel( + const { layer, calls, controls, announce, subscribed } = stubPanel( dom, uiState({ enabled: false }), ); - layer.publish(uiState({ enabled: true })); // saved layer state restored - assert.deepEqual(calls.collapsed, [], 'the collapsed panel stays shut'); - // Later, a user switches the layer off and on: that opens the panel, - // without overwriting the stored preference. - dom.document.body.dispatchEvent({ type: 'pointerdown', bubbles: true }); - layer.publish(uiState({ enabled: false })); - layer.publish(uiState({ enabled: true })); - assert.deepEqual(calls.collapsed, [[false, { persist: false }]]); - layer.publish(uiState({ enabled: true, open: true })); - assert.deepEqual(calls.collapsed.at(-1), [false, { persist: false }]); - controls.destroy(); - })); - -test('without a stored choice, a restored layer still opens its panel, unstored (P2-1)', () => - withDom(async (dom) => { - dom.root.dataset.collapsedPreference = 'default'; - const { layer, calls, controls } = stubPanel( - dom, - uiState({ enabled: false }), - ); - layer.publish(uiState({ enabled: true })); - assert.deepEqual(calls.collapsed, [[false, { persist: false }]]); - controls.destroy(); - })); - -test('with request origins, only a user switch-on opens the panel; a restore never does (P2-1)', () => - withDom(async (dom) => { - // No stored choice: the restore window alone would open the panel. - dom.root.dataset.collapsedPreference = 'default'; - let announce = null; - const { layer, calls, controls } = stubPanel( - dom, - uiState({ enabled: false }), - { - subscribeEnableRequests: (listener) => { - announce = listener; - return () => { - announce = null; - }; - }, - }, - ); announce('restore'); layer.publish(uiState({ enabled: true })); assert.deepEqual(calls.collapsed, [], 'a restore leaves the panel be'); @@ -691,8 +662,10 @@ test('with request origins, only a user switch-on opens the panel; a restore nev announce('user'); layer.publish(uiState({ enabled: true })); assert.deepEqual(calls.collapsed, [[false, { persist: false }]]); + layer.publish(uiState({ enabled: true, open: true })); + assert.equal(calls.collapsed.length, 2, 'a photo opening opens it too'); controls.destroy(); - assert.equal(announce, null, 'destroy unsubscribes'); + assert.equal(subscribed(), false, 'destroy unsubscribes'); })); test('FIT / FILL show the selected render mode (P3)', () =>