mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix(ui): let revealed panels own their side and keep Cockpit's left strip
A panel a feature opens (Street Level's viewer, a selected radio station) now becomes its side's open panel instead of being collapsed again by the next layout pass. Cockpit keeps showing its collapsed Data Layers strip, which Cyber's Cockpit layout aligns to, and Cyber's focus layout no longer re-shows collapsed left panels. Both sidebars share one accordion helper.
This commit is contained in:
1 parent
2035c767ce
commit
eed7657516
9 files changed
+124
-58
No files matched your search
@@ -426,7 +426,8 @@
|
||||
"src/ui/rightPanelRail.js",
|
||||
"src/ui/panelMeasurement.js",
|
||||
"src/ui/panelRailGeometry.js",
|
||||
"src/panelStackLayout.js"
|
||||
"src/panelStackLayout.js",
|
||||
"src/ui/panelAccordion.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -874,7 +875,8 @@
|
||||
"src/ui/statusText.js",
|
||||
"src/ui/statusBadge.js",
|
||||
"src/scopeMaskPolicy.js",
|
||||
"src/visualStylePolicy.js"
|
||||
"src/visualStylePolicy.js",
|
||||
"src/ui/panelAccordion.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -2810,7 +2812,8 @@
|
||||
"src/ui/statusBadge.js",
|
||||
"src/sceneCommandRelease.js",
|
||||
"src/scopeMaskPolicy.js",
|
||||
"src/visualStylePolicy.js"
|
||||
"src/visualStylePolicy.js",
|
||||
"src/ui/panelAccordion.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -3603,7 +3606,8 @@
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json"
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/ui/panelAccordion.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
|
||||
@@ -4,16 +4,21 @@ import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { shouldExpandGlobalContextPanel } from './rightRailPolicy.js';
|
||||
|
||||
test('desktop side rails hide collapsed panels in every HUD layout, never on phones', () => {
|
||||
test('desktop side rails hide collapsed panels in every HUD layout, never on phones or beside Cockpit', () => {
|
||||
const right = readFileSync(new URL('./ui/rightPanelRail.js', import.meta.url), 'utf8');
|
||||
const left = readFileSync(new URL('./ui/leftPanelRail.js', import.meta.url), 'utf8');
|
||||
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
||||
for (const ui of [right, left]) {
|
||||
assert.match(ui, /const isMobile = windowRef\.matchMedia\('\(max-width: 720px\)'\)\.matches/);
|
||||
assert.match(ui, /const exclusive = !isMobile;/);
|
||||
assert.match(ui, /if \(exclusive && panel\.classList\.contains\('collapsed'\)\)\s*panel\.setAttribute\('aria-hidden', 'true'\)/);
|
||||
assert.doesNotMatch(ui, /hud\.variant === 'cyber'/);
|
||||
}
|
||||
assert.match(right, /const exclusive = !isMobile;/);
|
||||
// Cockpit keeps its collapsed Data Layers strip, which its layout aligns to.
|
||||
assert.match(
|
||||
left,
|
||||
/const exclusive =\s*!isMobile && !documentRef\?\.body\?\.classList\?\.contains\('cockpit-mode'\);/,
|
||||
);
|
||||
assert.match(css, /#right-context-rail\.layout-exclusive > \[data-panel-id\]\.collapsed \{/);
|
||||
assert.match(css, /#left-panel-stack\.layout-exclusive > \[data-panel-id\]\.collapsed \{/);
|
||||
});
|
||||
|
||||
+14
-22
@@ -5,6 +5,7 @@ import {
|
||||
resolvePanelStackCorridor,
|
||||
} from '../panelStackLayout.js';
|
||||
import { measurePanelNaturalHeight } from './panelMeasurement.js';
|
||||
import { settleSideAccordion } from './panelAccordion.js';
|
||||
|
||||
/**
|
||||
* Measure and place the left panel rail for one synchronous layout pass.
|
||||
@@ -60,35 +61,26 @@ export function layoutLeftPanelRail({
|
||||
stack.style.removeProperty('--left-stack-centered-height');
|
||||
stack.classList.remove('layout-exclusive');
|
||||
stack.dataset.layoutMode = 'mobile';
|
||||
settleSideAccordion({ panels, enabled: false, onCollapse });
|
||||
for (const panel of panels) {
|
||||
if (panel.classList.contains('accordion-collapsed')) {
|
||||
panel.classList.remove('collapsed', 'accordion-collapsed');
|
||||
onCollapse(panel);
|
||||
}
|
||||
panel.removeAttribute('aria-hidden');
|
||||
panel.style.removeProperty('--left-panel-allocated-height');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// One panel is open per desktop side and the panel rail lists the others,
|
||||
// so collapsed panels are hidden. Normalize restored state without
|
||||
// overwriting saved panel preferences.
|
||||
const expandedBeforeAccordion = panels.filter(
|
||||
(panel) => !panel.classList.contains('collapsed'),
|
||||
);
|
||||
const accordionOwner =
|
||||
expandedBeforeAccordion.find((panel) => panel.id === preferredPanelId) ||
|
||||
expandedBeforeAccordion.find((panel) =>
|
||||
panel.contains(documentRef?.activeElement),
|
||||
) ||
|
||||
expandedBeforeAccordion[0];
|
||||
for (const panel of expandedBeforeAccordion) {
|
||||
if (panel === accordionOwner) continue;
|
||||
panel.classList.add('collapsed', 'accordion-collapsed');
|
||||
onCollapse(panel);
|
||||
}
|
||||
const exclusive = !isMobile;
|
||||
// One panel is open per desktop side, and the panel rail lists the others,
|
||||
// so collapsed panels are hidden. Cockpit keeps its collapsed Data Layers
|
||||
// strip, which its layout aligns to, and shows no rail.
|
||||
settleSideAccordion({
|
||||
panels,
|
||||
enabled: true,
|
||||
preferredPanelId,
|
||||
documentRef,
|
||||
onCollapse,
|
||||
});
|
||||
const exclusive =
|
||||
!isMobile && !documentRef?.body?.classList?.contains('cockpit-mode');
|
||||
stack.classList.toggle('layout-exclusive', exclusive);
|
||||
const laidOutPanels = panels.filter(
|
||||
(panel) => !exclusive || !panel.classList.contains('collapsed'),
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Keep one side panel open on desktop, and on phones put back the collapse
|
||||
* this applied, since a sheet shows several panels at once. The owner is the
|
||||
* most recently opened panel, then the one holding keyboard focus, then the
|
||||
* first. `accordion-collapsed` marks the collapse as presentation only, so
|
||||
* saved and shared preferences still report the panel as the user left it.
|
||||
* @param {object} options
|
||||
* @param {HTMLElement[]} options.panels The side's laid-out panels.
|
||||
* @param {boolean} options.enabled Whether the one-panel rule applies.
|
||||
* @param {string} [options.preferredPanelId] Most recently opened panel.
|
||||
* @param {Document} [options.documentRef] Document supplying keyboard focus.
|
||||
* @param {Function} options.onCollapse Update a panel's disclosure chrome.
|
||||
*/
|
||||
export function settleSideAccordion({
|
||||
panels,
|
||||
enabled,
|
||||
preferredPanelId,
|
||||
documentRef,
|
||||
onCollapse,
|
||||
}) {
|
||||
if (!enabled) {
|
||||
for (const panel of panels) {
|
||||
if (!panel.classList.contains('accordion-collapsed')) continue;
|
||||
panel.classList.remove('collapsed', 'accordion-collapsed');
|
||||
onCollapse(panel);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const expanded = panels.filter(
|
||||
(panel) => !panel.classList.contains('collapsed'),
|
||||
);
|
||||
const owner =
|
||||
expanded.find((panel) => panel.id === preferredPanelId) ||
|
||||
expanded.find((panel) => panel.contains(documentRef?.activeElement)) ||
|
||||
expanded[0];
|
||||
for (const panel of expanded) {
|
||||
if (panel === owner) continue;
|
||||
panel.classList.add('collapsed', 'accordion-collapsed');
|
||||
onCollapse(panel);
|
||||
}
|
||||
}
|
||||
@@ -421,14 +421,14 @@ export class PanelChrome {
|
||||
const wasAutoCollapsed =
|
||||
panelEl.classList.contains('layout-auto-collapsed') ||
|
||||
panelEl.classList.contains('accordion-collapsed');
|
||||
// One panel is open per desktop side: a user opening a side panel owns
|
||||
// the whole accordion, including peers that were only
|
||||
// presentation-collapsed during a saved-state restore.
|
||||
// One panel is open per desktop side: opening a side panel, whether the
|
||||
// user asked or a feature revealed it (Street Level's viewer, a selected
|
||||
// radio station), owns the whole accordion, including peers that were
|
||||
// only presentation-collapsed during a saved-state restore.
|
||||
const sideStack = [this._leftPanelStack, this._rightPanelStack].find(
|
||||
(stack) => stack && panelEl.parentElement === stack,
|
||||
);
|
||||
if (
|
||||
explicit &&
|
||||
!restore &&
|
||||
!nextCollapsed &&
|
||||
sideStack &&
|
||||
@@ -460,7 +460,7 @@ export class PanelChrome {
|
||||
: null;
|
||||
const priorLeftOwner = this._panelLayout._leftStackPreferredPanelId;
|
||||
const priorRightOwner = this._panelLayout._rightStackPreferredPanelId;
|
||||
if (explicit && !restore && !nextCollapsed && leftOwnerPanel) {
|
||||
if (!restore && !nextCollapsed && leftOwnerPanel) {
|
||||
this._panelLayout._leftStackPreferredPanelId = leftOwnerPanel.id;
|
||||
} else if (
|
||||
explicit &&
|
||||
@@ -470,7 +470,7 @@ export class PanelChrome {
|
||||
) {
|
||||
this._panelLayout._leftStackPreferredPanelId = null;
|
||||
}
|
||||
if (explicit && !restore && !nextCollapsed && rightOwnerPanel) {
|
||||
if (!restore && !nextCollapsed && rightOwnerPanel) {
|
||||
this._panelLayout._rightStackPreferredPanelId = rightOwnerPanel.id;
|
||||
} else if (
|
||||
explicit &&
|
||||
|
||||
@@ -777,3 +777,17 @@ test('right layout ignores a hidden panel: no lane, no gap, no auto-collapse', (
|
||||
) <= alone,
|
||||
);
|
||||
});
|
||||
|
||||
test('left rail keeps collapsed panels shown beside Cockpit, still one open', () => {
|
||||
const f = fixture('left');
|
||||
f.options.documentRef.body = {
|
||||
classList: { contains: (name) => name === 'cockpit-mode' },
|
||||
};
|
||||
f.expand(f.first, 250);
|
||||
f.expand(f.second, 250);
|
||||
f.options.preferredPanelId = f.second.id;
|
||||
f.run();
|
||||
assert.equal(f.first.classList.contains('accordion-collapsed'), true);
|
||||
assert.equal(f.stack.classList.contains('layout-exclusive'), false);
|
||||
assert.equal(f.first.getAttribute('aria-hidden'), undefined);
|
||||
});
|
||||
+10
-24
@@ -5,6 +5,7 @@ import {
|
||||
import { resolveHudRailLayout } from './panelRailGeometry.js';
|
||||
|
||||
import { displayPanelScroller } from './displayPanelScroll.js';
|
||||
import { settleSideAccordion } from './panelAccordion.js';
|
||||
|
||||
const pendingCollapseRetries = new WeakSet();
|
||||
|
||||
@@ -86,31 +87,16 @@ export function layoutRightPanelRail({
|
||||
onCollapse(panel);
|
||||
}
|
||||
}
|
||||
// One panel is open per desktop side. Normalize restored state without
|
||||
// overwriting saved panel preferences; this marker is distinct from
|
||||
// Tactical's space-based collapse. Phones present the rail as tab sheets
|
||||
// that show several panels at once.
|
||||
// One panel is open per desktop side; this marker is distinct from
|
||||
// Tactical's space-based collapse.
|
||||
const isMobile = windowRef.matchMedia('(max-width: 720px)').matches;
|
||||
if (!isMobile) {
|
||||
const expanded = panels.filter(
|
||||
(panel) => !panel.classList.contains('collapsed'),
|
||||
);
|
||||
const owner =
|
||||
expanded.find((panel) => panel.id === preferredPanelId) ||
|
||||
expanded.find((panel) => panel.contains(documentRef?.activeElement)) ||
|
||||
expanded[0];
|
||||
for (const panel of expanded) {
|
||||
if (panel === owner) continue;
|
||||
panel.classList.add('collapsed', 'accordion-collapsed');
|
||||
onCollapse(panel);
|
||||
}
|
||||
} else {
|
||||
for (const panel of panels) {
|
||||
if (!panel.classList.contains('accordion-collapsed')) continue;
|
||||
panel.classList.remove('collapsed', 'accordion-collapsed');
|
||||
onCollapse(panel);
|
||||
}
|
||||
}
|
||||
settleSideAccordion({
|
||||
panels,
|
||||
enabled: !isMobile,
|
||||
preferredPanelId,
|
||||
documentRef,
|
||||
onCollapse,
|
||||
});
|
||||
// The desktop panel rail lists every panel, so collapsed ones are hidden.
|
||||
// Phones present the rail as tab sheets (mobileShell.js), which choose what
|
||||
// shows; hiding collapsed siblings would empty a sheet and its a11y tree.
|
||||
|
||||
@@ -97,3 +97,20 @@ test('restored preferences keep every saved panel open for the layout to normali
|
||||
} finally { globalThis.document = prior; }
|
||||
}
|
||||
});
|
||||
|
||||
test('a feature revealing a side panel makes it the side owner', () => {
|
||||
const f = fixture('tactical'), prior = globalThis.document;
|
||||
globalThis.document = f.doc;
|
||||
try {
|
||||
f.owner.setPanelCollapsed('pp-toggles', false, { explicit: true });
|
||||
f.claimed.length = 0;
|
||||
// Street Level's viewer or a selected radio station opens its panel
|
||||
// without an explicit user disclosure.
|
||||
f.owner.setPanelCollapsed('radio-panel', false);
|
||||
assert.equal(f.context.classList.contains('collapsed'), false);
|
||||
assert.equal(f.display.classList.contains('collapsed'), true);
|
||||
assert.equal(f.owner._panelLayout._rightStackPreferredPanelId, 'global-context-panel');
|
||||
// Only a user's choice claims restoration lanes.
|
||||
assert.deepEqual(f.claimed, []);
|
||||
} finally { globalThis.document = prior; }
|
||||
});
|
||||
@@ -1674,6 +1674,13 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* The panel rail lists the collapsed panels. */
|
||||
:root[data-ui-theme='cyber']
|
||||
#left-panel-stack.layout-focus.layout-exclusive
|
||||
> [data-panel-id].collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber']
|
||||
#left-panel-stack
|
||||
> :is(#data-panel, #scene-panel) {
|
||||
|
||||
Reference in new issue
Block a user