diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0c5f754..802407a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +- Prevent a compact Radio disclosure from covering Space Missions when the + adaptive right rail restores Context without calling the panel owner. + Repeated Context-open calls and rail-driven expansion both close the mini, + with a regression for auto-collapse restoration (raysung-2026, #804). + - CCTV cameras whose bearing is a guess now say so. Packs mark bearings derived from a hash of the camera id as `headingConfidence: 'low'`, but nothing read the flag, so roughly 70% of a default catalog rendered like surveyed facings. The HUD diff --git a/src/ui/panelChrome.js b/src/ui/panelChrome.js index 6464f1588..0717a1313 100644 --- a/src/ui/panelChrome.js +++ b/src/ui/panelChrome.js @@ -68,7 +68,8 @@ export class PanelChrome { variant: this.hud.getVariant(), }), scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(), - syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel), + syncPanelCollapseButton: (panel) => + this._syncRailPanelCollapseButton(panel), readDisplayScrollTop: () => this._displayPortalScrollRestoreOwner === 'standard' ? this._standardDisplayScrollTop @@ -282,6 +283,19 @@ export class PanelChrome { return this._panelLayout._scheduleLeftPanelLayout(options0); } + _syncRailPanelCollapseButton(panelEl) { + // Adaptive rail layout may directly restore an auto-collapsed Context + // without invoking setPanelCollapsed(). Enforce Radio ownership there too. + if ( + panelEl?.id === 'global-context-panel' && + !panelEl.classList.contains('collapsed') && + this._contextRadioDock?.classList.contains('disclosure-open') + ) { + this._setRadioDisclosure?.(false); + } + this._syncPanelCollapseButton(panelEl); + } + _syncPanelCollapseButton(panelEl) { const isRightRail = [ 'pp-toggles', diff --git a/src/ui/panelChrome.radioOwnership.test.mjs b/src/ui/panelChrome.radioOwnership.test.mjs index c52a4888e..1ecc1c7b9 100644 --- a/src/ui/panelChrome.radioOwnership.test.mjs +++ b/src/ui/panelChrome.radioOwnership.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { PanelChrome } from './panelChrome.js'; +import { PanelLayoutController } from './panelLayoutController.js'; function classList(initial = []) { const values = new Set(initial); @@ -65,3 +66,71 @@ test('opening an already-open Context still closes a stale compact Radio disclos assert.deepEqual(calls, [false]); assert.equal(radioDock.classList.contains('disclosure-open'), false); }); + +test('adaptive rail restore hides compact Radio before revealing Context', (t) => { + const priorRaf = globalThis.requestAnimationFrame; + let nextFrame; + globalThis.requestAnimationFrame = (callback) => { + nextFrame = callback; + return 1; + }; + t.after(() => { + globalThis.requestAnimationFrame = priorRaf; + }); + + // Valid starting state: the tactical rail automatically collapsed Context, + // and the user opened the compact Radio controls from its header. + const contextPanel = { + id: 'global-context-panel', + classList: classList(['collapsed', 'layout-auto-collapsed']), + }; + const radioDock = { classList: classList(['disclosure-open']) }; + const radioMini = { hidden: false }; + const calls = []; + const chrome = { + _contextRadioDock: radioDock, + _setRadioDisclosure(open) { + calls.push(`disclosure:${open}`); + radioDock.classList.toggle('disclosure-open', open); + radioMini.hidden = !open; + }, + _syncPanelCollapseButton(panel) { + calls.push(`button:${panel.id}`); + }, + }; + const layout = { + destroyed: false, + _rightStackLayoutFrame: null, + _rightStackReconsiderAutoCollapse: false, + _rightPanelStack: { + querySelectorAll: (selector) => + selector === '.layout-auto-collapsed' ? [contextPanel] : [], + }, + _syncPanelCollapseButton(panel) { + PanelChrome.prototype._syncRailPanelCollapseButton.call(chrome, panel); + }, + _syncRightPanelAdaptiveLayout() { + calls.push('layout'); + }, + }; + + PanelLayoutController.prototype._scheduleRightPanelLayout.call(layout, { + reconsiderAutoCollapse: true, + }); + assert.equal(contextPanel.classList.contains('collapsed'), true); + assert.equal(radioMini.hidden, false); + nextFrame(); + + assert.equal(contextPanel.classList.contains('collapsed'), false); + assert.equal( + contextPanel.classList.contains('layout-auto-collapsed'), + false, + ); + assert.equal(radioDock.classList.contains('disclosure-open'), false); + assert.equal(radioMini.hidden, true); + assert.deepEqual(calls, [ + 'disclosure:false', + 'button:global-context-panel', + 'layout', + ]); +});