mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge pull request #816 from kvnloo/fix/context-radio-owner-invariant
fix(ui): dismiss compact Radio on Context rail restoration
This commit is contained in:
5 files changed
+236
-1
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
- Close the compact Radio disclosure when an adaptive tactical right-rail
|
||||||
|
restore directly expands Context, before Space Missions can be covered.
|
||||||
|
A repeated Context-open request also clears stale disclosure state.
|
||||||
|
Real PanelChrome constructor wiring and the rail reflow path are covered
|
||||||
|
by focused regressions. Credits: kvnloo (#816), raysung-2026's original
|
||||||
|
#804 report, asturksever's review, and quanticsoul4772's independent
|
||||||
|
Windows diagnosis.
|
||||||
@@ -1208,6 +1208,10 @@ Automatic collapse remains presentation only, prioritizes the latest explicit
|
|||||||
panel, and honors keyboard focus on the right. Mobile layout still releases
|
panel, and honors keyboard focus on the right. Mobile layout still releases
|
||||||
desktop allocation styles. Stable Display allocation avoids unnecessary style
|
desktop allocation styles. Stable Display allocation avoids unnecessary style
|
||||||
writes. This extraction does not change panel positions or layout defaults.
|
writes. This extraction does not change panel positions or layout defaults.
|
||||||
|
Context expansion through the panel owner or adaptive right-rail restoration
|
||||||
|
closes an open compact Radio disclosure before Context/Space Missions becomes
|
||||||
|
visible. The mini remains available while Context is collapsed. This fixes the
|
||||||
|
Radio-overlap path reported in #804 without changing z-index or layout policy.
|
||||||
The right rail skips panels hidden by other CSS (computed `display: none`), but
|
The right rail skips panels hidden by other CSS (computed `display: none`), but
|
||||||
keeps the collapsed panels its own exclusive presentation hides, so automatic
|
keeps the collapsed panels its own exclusive presentation hides, so automatic
|
||||||
collapse can still be undone.
|
collapse can still be undone.
|
||||||
|
|||||||
@@ -693,6 +693,7 @@
|
|||||||
"src/ui/shareRestoration.js",
|
"src/ui/shareRestoration.js",
|
||||||
"src/ui/visualSettings.js",
|
"src/ui/visualSettings.js",
|
||||||
"src/ui/panelChrome.js",
|
"src/ui/panelChrome.js",
|
||||||
|
"src/ui/panelChrome.radioOwnership.test.mjs",
|
||||||
"src/testSupport/readShellSource.mjs",
|
"src/testSupport/readShellSource.mjs",
|
||||||
"src/ui/stateOwners.test.mjs",
|
"src/ui/stateOwners.test.mjs",
|
||||||
"src/layers/flights/records.js",
|
"src/layers/flights/records.js",
|
||||||
|
|||||||
+24
-1
@@ -72,7 +72,8 @@ export class PanelChrome {
|
|||||||
variant: this.hud.getVariant(),
|
variant: this.hud.getVariant(),
|
||||||
}),
|
}),
|
||||||
scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(),
|
scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(),
|
||||||
syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel),
|
syncPanelCollapseButton: (panel) =>
|
||||||
|
this._syncRailPanelCollapseButton(panel),
|
||||||
readDisplayScrollTop: () =>
|
readDisplayScrollTop: () =>
|
||||||
this._displayPortalScrollRestoreOwner === 'standard'
|
this._displayPortalScrollRestoreOwner === 'standard'
|
||||||
? this._standardDisplayScrollTop
|
? this._standardDisplayScrollTop
|
||||||
@@ -287,6 +288,19 @@ export class PanelChrome {
|
|||||||
return this._panelLayout._scheduleLeftPanelLayout(options0);
|
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) {
|
_syncPanelCollapseButton(panelEl) {
|
||||||
const isRightRail = [
|
const isRightRail = [
|
||||||
'pp-toggles',
|
'pp-toggles',
|
||||||
@@ -501,6 +515,15 @@ export class PanelChrome {
|
|||||||
syncShare,
|
syncShare,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// A repeated Context-open request must clear a stale compact Radio mini
|
||||||
|
// even when no collapsed class changes.
|
||||||
|
if (
|
||||||
|
!nextCollapsed &&
|
||||||
|
panelId === 'global-context-panel' &&
|
||||||
|
this._contextRadioDock?.classList.contains('disclosure-open')
|
||||||
|
) {
|
||||||
|
this._setRadioDisclosure?.(false);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
panelEl.classList.contains('collapsed') === nextCollapsed &&
|
panelEl.classList.contains('collapsed') === nextCollapsed &&
|
||||||
!wasAutoCollapsed
|
!wasAutoCollapsed
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
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);
|
||||||
|
return {
|
||||||
|
contains: (value) => values.has(value),
|
||||||
|
add: (...items) => items.forEach((item) => values.add(item)),
|
||||||
|
remove: (...items) => items.forEach((item) => values.delete(item)),
|
||||||
|
toggle(value, force) {
|
||||||
|
if (force === true) values.add(value);
|
||||||
|
else if (force === false) values.delete(value);
|
||||||
|
else if (values.has(value)) values.delete(value);
|
||||||
|
else values.add(value);
|
||||||
|
return values.has(value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('opening an already-open Context still closes a stale compact Radio disclosure', (t) => {
|
||||||
|
const priorDocument = globalThis.document;
|
||||||
|
const contextPanel = {
|
||||||
|
id: 'global-context-panel',
|
||||||
|
classList: classList(),
|
||||||
|
};
|
||||||
|
globalThis.document = {
|
||||||
|
documentElement: { dataset: { uiTheme: 'tactical' } },
|
||||||
|
getElementById(id) {
|
||||||
|
return id === 'global-context-panel' ? contextPanel : null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
t.after(() => {
|
||||||
|
globalThis.document = priorDocument;
|
||||||
|
});
|
||||||
|
|
||||||
|
const radioDock = { classList: classList(['disclosure-open']) };
|
||||||
|
const calls = [];
|
||||||
|
const owner = {
|
||||||
|
_contextRadioDock: radioDock,
|
||||||
|
_setRadioDisclosure(open) {
|
||||||
|
calls.push(open);
|
||||||
|
radioDock.classList.toggle('disclosure-open', open);
|
||||||
|
},
|
||||||
|
_leftPanelStack: null,
|
||||||
|
_rightPanelStack: null,
|
||||||
|
_panelLayout: {
|
||||||
|
_leftStackPreferredPanelId: null,
|
||||||
|
_rightStackPreferredPanelId: null,
|
||||||
|
},
|
||||||
|
_syncPanelCollapseButton() {},
|
||||||
|
_scheduleLeftPanelLayout() {},
|
||||||
|
_scheduleRightPanelLayout() {},
|
||||||
|
shareLinkManager: null,
|
||||||
|
cockpitView: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
PanelChrome.prototype.setPanelCollapsed.call(
|
||||||
|
owner,
|
||||||
|
'global-context-panel',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('real PanelChrome constructor wires rail restore to compact Radio owner', (t) => {
|
||||||
|
const priorDocument = globalThis.document;
|
||||||
|
const priorRaf = globalThis.requestAnimationFrame;
|
||||||
|
let nextFrame;
|
||||||
|
const contextPanel = {
|
||||||
|
id: 'global-context-panel',
|
||||||
|
classList: classList(['collapsed', 'layout-auto-collapsed']),
|
||||||
|
};
|
||||||
|
const rightRail = {
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
return selector === '.layout-auto-collapsed' ? [contextPanel] : [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
globalThis.document = {
|
||||||
|
getElementById(id) {
|
||||||
|
return id === 'right-context-rail' ? rightRail : null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
globalThis.requestAnimationFrame = (callback) => {
|
||||||
|
nextFrame = callback;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
t.after(() => {
|
||||||
|
globalThis.document = priorDocument;
|
||||||
|
globalThis.requestAnimationFrame = priorRaf;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dock = { classList: classList(['disclosure-open']) };
|
||||||
|
const calls = [];
|
||||||
|
const chrome = new PanelChrome({
|
||||||
|
elements: { _contextRadioDock: dock },
|
||||||
|
operations: {
|
||||||
|
_setRadioDisclosure(open) {
|
||||||
|
calls.push(`disclosure:${open}`);
|
||||||
|
dock.classList.toggle('disclosure-open', open);
|
||||||
|
},
|
||||||
|
_syncPanelCollapseButton(panel) {
|
||||||
|
calls.push(`button:${panel.id}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
readHud: () => ({ visible: true, getVariant: () => 'tactical' }),
|
||||||
|
readCockpit: () => null,
|
||||||
|
readShareLinks: () => null,
|
||||||
|
readInitialShare: () => null,
|
||||||
|
readScrollRestoreOwner: () => null,
|
||||||
|
readDisplayScrollTop: () => 0,
|
||||||
|
});
|
||||||
|
chrome._panelLayout._syncRightPanelAdaptiveLayout = () => {
|
||||||
|
calls.push('layout');
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome._panelLayout._scheduleRightPanelLayout({
|
||||||
|
reconsiderAutoCollapse: true,
|
||||||
|
});
|
||||||
|
assert.equal(typeof nextFrame, 'function');
|
||||||
|
assert.equal(dock.classList.contains('disclosure-open'), true);
|
||||||
|
nextFrame();
|
||||||
|
|
||||||
|
assert.equal(contextPanel.classList.contains('collapsed'), false);
|
||||||
|
assert.equal(dock.classList.contains('disclosure-open'), false);
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'disclosure:false',
|
||||||
|
'button:global-context-panel',
|
||||||
|
'layout',
|
||||||
|
]);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user