mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +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/rightPanelRail.js",
|
||||||
"src/ui/panelMeasurement.js",
|
"src/ui/panelMeasurement.js",
|
||||||
"src/ui/panelRailGeometry.js",
|
"src/ui/panelRailGeometry.js",
|
||||||
"src/panelStackLayout.js"
|
"src/panelStackLayout.js",
|
||||||
|
"src/ui/panelAccordion.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -874,7 +875,8 @@
|
|||||||
"src/ui/statusText.js",
|
"src/ui/statusText.js",
|
||||||
"src/ui/statusBadge.js",
|
"src/ui/statusBadge.js",
|
||||||
"src/scopeMaskPolicy.js",
|
"src/scopeMaskPolicy.js",
|
||||||
"src/visualStylePolicy.js"
|
"src/visualStylePolicy.js",
|
||||||
|
"src/ui/panelAccordion.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -2810,7 +2812,8 @@
|
|||||||
"src/ui/statusBadge.js",
|
"src/ui/statusBadge.js",
|
||||||
"src/sceneCommandRelease.js",
|
"src/sceneCommandRelease.js",
|
||||||
"src/scopeMaskPolicy.js",
|
"src/scopeMaskPolicy.js",
|
||||||
"src/visualStylePolicy.js"
|
"src/visualStylePolicy.js",
|
||||||
|
"src/ui/panelAccordion.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
@@ -3603,7 +3606,8 @@
|
|||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
"src/i18n/locales/uk.json",
|
"src/i18n/locales/uk.json",
|
||||||
"src/i18n/locales/zh-CN.json",
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/i18n/locales/zh-Hant.json"
|
"src/i18n/locales/zh-Hant.json",
|
||||||
|
"src/ui/panelAccordion.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,16 +4,21 @@ import { readFileSync } from 'node:fs';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { shouldExpandGlobalContextPanel } from './rightRailPolicy.js';
|
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 right = readFileSync(new URL('./ui/rightPanelRail.js', import.meta.url), 'utf8');
|
||||||
const left = readFileSync(new URL('./ui/leftPanelRail.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));
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
||||||
for (const ui of [right, left]) {
|
for (const ui of [right, left]) {
|
||||||
assert.match(ui, /const isMobile = windowRef\.matchMedia\('\(max-width: 720px\)'\)\.matches/);
|
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.match(ui, /if \(exclusive && panel\.classList\.contains\('collapsed'\)\)\s*panel\.setAttribute\('aria-hidden', 'true'\)/);
|
||||||
assert.doesNotMatch(ui, /hud\.variant === 'cyber'/);
|
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, /#right-context-rail\.layout-exclusive > \[data-panel-id\]\.collapsed \{/);
|
||||||
assert.match(css, /#left-panel-stack\.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,
|
resolvePanelStackCorridor,
|
||||||
} from '../panelStackLayout.js';
|
} from '../panelStackLayout.js';
|
||||||
import { measurePanelNaturalHeight } from './panelMeasurement.js';
|
import { measurePanelNaturalHeight } from './panelMeasurement.js';
|
||||||
|
import { settleSideAccordion } from './panelAccordion.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Measure and place the left panel rail for one synchronous layout pass.
|
* 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.style.removeProperty('--left-stack-centered-height');
|
||||||
stack.classList.remove('layout-exclusive');
|
stack.classList.remove('layout-exclusive');
|
||||||
stack.dataset.layoutMode = 'mobile';
|
stack.dataset.layoutMode = 'mobile';
|
||||||
|
settleSideAccordion({ panels, enabled: false, onCollapse });
|
||||||
for (const panel of panels) {
|
for (const panel of panels) {
|
||||||
if (panel.classList.contains('accordion-collapsed')) {
|
|
||||||
panel.classList.remove('collapsed', 'accordion-collapsed');
|
|
||||||
onCollapse(panel);
|
|
||||||
}
|
|
||||||
panel.removeAttribute('aria-hidden');
|
panel.removeAttribute('aria-hidden');
|
||||||
panel.style.removeProperty('--left-panel-allocated-height');
|
panel.style.removeProperty('--left-panel-allocated-height');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One panel is open per desktop side and the panel rail lists the others,
|
// One panel is open per desktop side, and the panel rail lists the others,
|
||||||
// so collapsed panels are hidden. Normalize restored state without
|
// so collapsed panels are hidden. Cockpit keeps its collapsed Data Layers
|
||||||
// overwriting saved panel preferences.
|
// strip, which its layout aligns to, and shows no rail.
|
||||||
const expandedBeforeAccordion = panels.filter(
|
settleSideAccordion({
|
||||||
(panel) => !panel.classList.contains('collapsed'),
|
panels,
|
||||||
);
|
enabled: true,
|
||||||
const accordionOwner =
|
preferredPanelId,
|
||||||
expandedBeforeAccordion.find((panel) => panel.id === preferredPanelId) ||
|
documentRef,
|
||||||
expandedBeforeAccordion.find((panel) =>
|
onCollapse,
|
||||||
panel.contains(documentRef?.activeElement),
|
});
|
||||||
) ||
|
const exclusive =
|
||||||
expandedBeforeAccordion[0];
|
!isMobile && !documentRef?.body?.classList?.contains('cockpit-mode');
|
||||||
for (const panel of expandedBeforeAccordion) {
|
|
||||||
if (panel === accordionOwner) continue;
|
|
||||||
panel.classList.add('collapsed', 'accordion-collapsed');
|
|
||||||
onCollapse(panel);
|
|
||||||
}
|
|
||||||
const exclusive = !isMobile;
|
|
||||||
stack.classList.toggle('layout-exclusive', exclusive);
|
stack.classList.toggle('layout-exclusive', exclusive);
|
||||||
const laidOutPanels = panels.filter(
|
const laidOutPanels = panels.filter(
|
||||||
(panel) => !exclusive || !panel.classList.contains('collapsed'),
|
(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 =
|
const wasAutoCollapsed =
|
||||||
panelEl.classList.contains('layout-auto-collapsed') ||
|
panelEl.classList.contains('layout-auto-collapsed') ||
|
||||||
panelEl.classList.contains('accordion-collapsed');
|
panelEl.classList.contains('accordion-collapsed');
|
||||||
// One panel is open per desktop side: a user opening a side panel owns
|
// One panel is open per desktop side: opening a side panel, whether the
|
||||||
// the whole accordion, including peers that were only
|
// user asked or a feature revealed it (Street Level's viewer, a selected
|
||||||
// presentation-collapsed during a saved-state restore.
|
// 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(
|
const sideStack = [this._leftPanelStack, this._rightPanelStack].find(
|
||||||
(stack) => stack && panelEl.parentElement === stack,
|
(stack) => stack && panelEl.parentElement === stack,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
explicit &&
|
|
||||||
!restore &&
|
!restore &&
|
||||||
!nextCollapsed &&
|
!nextCollapsed &&
|
||||||
sideStack &&
|
sideStack &&
|
||||||
@@ -460,7 +460,7 @@ export class PanelChrome {
|
|||||||
: null;
|
: null;
|
||||||
const priorLeftOwner = this._panelLayout._leftStackPreferredPanelId;
|
const priorLeftOwner = this._panelLayout._leftStackPreferredPanelId;
|
||||||
const priorRightOwner = this._panelLayout._rightStackPreferredPanelId;
|
const priorRightOwner = this._panelLayout._rightStackPreferredPanelId;
|
||||||
if (explicit && !restore && !nextCollapsed && leftOwnerPanel) {
|
if (!restore && !nextCollapsed && leftOwnerPanel) {
|
||||||
this._panelLayout._leftStackPreferredPanelId = leftOwnerPanel.id;
|
this._panelLayout._leftStackPreferredPanelId = leftOwnerPanel.id;
|
||||||
} else if (
|
} else if (
|
||||||
explicit &&
|
explicit &&
|
||||||
@@ -470,7 +470,7 @@ export class PanelChrome {
|
|||||||
) {
|
) {
|
||||||
this._panelLayout._leftStackPreferredPanelId = null;
|
this._panelLayout._leftStackPreferredPanelId = null;
|
||||||
}
|
}
|
||||||
if (explicit && !restore && !nextCollapsed && rightOwnerPanel) {
|
if (!restore && !nextCollapsed && rightOwnerPanel) {
|
||||||
this._panelLayout._rightStackPreferredPanelId = rightOwnerPanel.id;
|
this._panelLayout._rightStackPreferredPanelId = rightOwnerPanel.id;
|
||||||
} else if (
|
} else if (
|
||||||
explicit &&
|
explicit &&
|
||||||
|
|||||||
@@ -777,3 +777,17 @@ test('right layout ignores a hidden panel: no lane, no gap, no auto-collapse', (
|
|||||||
) <= alone,
|
) <= 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 { resolveHudRailLayout } from './panelRailGeometry.js';
|
||||||
|
|
||||||
import { displayPanelScroller } from './displayPanelScroll.js';
|
import { displayPanelScroller } from './displayPanelScroll.js';
|
||||||
|
import { settleSideAccordion } from './panelAccordion.js';
|
||||||
|
|
||||||
const pendingCollapseRetries = new WeakSet();
|
const pendingCollapseRetries = new WeakSet();
|
||||||
|
|
||||||
@@ -86,31 +87,16 @@ export function layoutRightPanelRail({
|
|||||||
onCollapse(panel);
|
onCollapse(panel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// One panel is open per desktop side. Normalize restored state without
|
// One panel is open per desktop side; this marker is distinct from
|
||||||
// overwriting saved panel preferences; this marker is distinct from
|
// Tactical's space-based collapse.
|
||||||
// Tactical's space-based collapse. Phones present the rail as tab sheets
|
|
||||||
// that show several panels at once.
|
|
||||||
const isMobile = windowRef.matchMedia('(max-width: 720px)').matches;
|
const isMobile = windowRef.matchMedia('(max-width: 720px)').matches;
|
||||||
if (!isMobile) {
|
settleSideAccordion({
|
||||||
const expanded = panels.filter(
|
panels,
|
||||||
(panel) => !panel.classList.contains('collapsed'),
|
enabled: !isMobile,
|
||||||
);
|
preferredPanelId,
|
||||||
const owner =
|
documentRef,
|
||||||
expanded.find((panel) => panel.id === preferredPanelId) ||
|
onCollapse,
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The desktop panel rail lists every panel, so collapsed ones are hidden.
|
// The desktop panel rail lists every panel, so collapsed ones are hidden.
|
||||||
// Phones present the rail as tab sheets (mobileShell.js), which choose what
|
// Phones present the rail as tab sheets (mobileShell.js), which choose what
|
||||||
// shows; hiding collapsed siblings would empty a sheet and its a11y tree.
|
// 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; }
|
} 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;
|
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']
|
:root[data-ui-theme='cyber']
|
||||||
#left-panel-stack
|
#left-panel-stack
|
||||||
> :is(#data-panel, #scene-panel) {
|
> :is(#data-panel, #scene-panel) {
|
||||||
|
|||||||
Reference in new issue
Block a user