mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(cockpit): render the utility strip's measured layout from the store
The Cockpit layout pass publishes the utility strip's fit to the gev-cockpit store: the HUD's --cockpit-utility-* lengths, whether only the expanded control fits, and whether a collapsed control beside an expanded one is hidden from assistive technology. CockpitView renders the lengths on the HUD and CockpitUtilityControls the strip's class and each control's aria-hidden, which now also follows a popover opened between passes. Focus leaves a control before the strip hides it. Exit publishes no layout, which also clears the expanded height, class and aria-hidden the old exit left behind. The briefing and Contact cards' positions stay imperative: the same pass measures them straight after writing them.
This commit is contained in:
1 parent
c3c69a91cd
commit
4c92f517db
12 files changed
+232
-66
No files matched your search
@@ -177,6 +177,12 @@
|
|||||||
`signalCollapsed` in its presence, and each card's `data-collapsed` and
|
`signalCollapsed` in its presence, and each card's `data-collapsed` and
|
||||||
its toggle's state, words and icon follow it (samehkhamis).
|
its toggle's state, words and icon follow it (samehkhamis).
|
||||||
|
|
||||||
|
- The Cockpit utility strip's measured layout renders from the
|
||||||
|
`gev-cockpit` store: the layout pass publishes `utilityLayout` (the HUD's
|
||||||
|
`--cockpit-utility-*` lengths, `primaryOnly` and `hideCollapsed`), and a
|
||||||
|
host constructing `CockpitController` passes `publish.utilityLayout`
|
||||||
|
beside its other publishers (samehkhamis).
|
||||||
|
|
||||||
## [0.3.0] — 2026-10-27 — Street Level, On Your Phone, in Your Language
|
## [0.3.0] — 2026-10-27 — Street Level, On Your Phone, in Your Language
|
||||||
|
|
||||||
- Show a Street Level view facing 359.5° or more as 0°. The viewer caption
|
- Show a Street Level view facing 359.5° or more as 0°. The viewer caption
|
||||||
|
|||||||
@@ -314,7 +314,12 @@ a feature supplying only its accent.
|
|||||||
that presence too: each card's `data-collapsed` and its toggle's words
|
that presence too: each card's `data-collapsed` and its toggle's words
|
||||||
and icon render from it. The controls keep the controller's listeners,
|
and icon render from it. The controls keep the controller's listeners,
|
||||||
and the cards' layout stays with the controller, which reads their
|
and the cards' layout stays with the controller, which reads their
|
||||||
visibility when it lays them out.
|
visibility when it lays them out. Its layout pass publishes the utility
|
||||||
|
strip's measured fit (the HUD's anchor and height limits, whether only the
|
||||||
|
expanded control fits and which control is hidden from assistive
|
||||||
|
technology), which `CockpitView` and `CockpitUtilityControls` render; the
|
||||||
|
briefing and Contact cards' positions stay imperative, because the same
|
||||||
|
pass measures them straight after writing them.
|
||||||
`CockpitUtilityControls` renders the Display
|
`CockpitUtilityControls` renders the Display
|
||||||
and Radio launchers, which popover is open and the compact Radio
|
and Radio launchers, which popover is open and the compact Radio
|
||||||
controls from the `gev-radio` store that RadioControls publishes to and
|
controls from the `gev-radio` store that RadioControls publishes to and
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ test('Cockpit owns a focused shared Display portal and compact Radio controls',
|
|||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
syncSignalLayout.toString(),
|
syncSignalLayout.toString(),
|
||||||
/setAttribute\('aria-hidden', String\(hiddenSibling\)\)/,
|
/primaryOnly: utilityLayout\.primaryOnly,\s*hideCollapsed: utilityLayout\.primaryOnly,/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -934,7 +934,7 @@ test('Cockpit panel corridors reserve the owned topline readouts', () => {
|
|||||||
assert.ok(signalLayout, 'Cockpit signal layout method is missing');
|
assert.ok(signalLayout, 'Cockpit signal layout method is missing');
|
||||||
assert.match(
|
assert.match(
|
||||||
signalLayout,
|
signalLayout,
|
||||||
/setProperty\(\s*'--cockpit-utility-top',\s*`\$\{utilityAnchor\.top\.toFixed\(1\)\}px`,?\s*\)/,
|
/publishUtilityLayout\([\s\S]*?top: `\$\{utilityAnchor\.top\.toFixed\(1\)\}px`,/,
|
||||||
'Cockpit owns the utility strip anchor and republishes it every layout tick',
|
'Cockpit owns the utility strip anchor and republishes it every layout tick',
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
@@ -970,7 +970,7 @@ test('Cockpit panel corridors reserve the owned topline readouts', () => {
|
|||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
signalLayout,
|
signalLayout,
|
||||||
/cockpit-utility-controls[\s\S]*?--cockpit-utility-max-height/,
|
/maxHeight: `\$\{availableHeight\.toFixed\(2\)\}px`,/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
css,
|
css,
|
||||||
|
|||||||
@@ -400,6 +400,8 @@ export class StyleManager extends ShellFacade {
|
|||||||
useCockpitStore(applicationPinia()).publishVision(view),
|
useCockpitStore(applicationPinia()).publishVision(view),
|
||||||
signals: (rows) =>
|
signals: (rows) =>
|
||||||
useCockpitStore(applicationPinia()).publishSignals(rows),
|
useCockpitStore(applicationPinia()).publishSignals(rows),
|
||||||
|
utilityLayout: (layout) =>
|
||||||
|
useCockpitStore(applicationPinia()).publishUtilityLayout(layout),
|
||||||
},
|
},
|
||||||
_layoutRightPanels: (...args) => this._layoutRightPanels(...args),
|
_layoutRightPanels: (...args) => this._layoutRightPanels(...args),
|
||||||
_setCockpitVision: (...args) => this._setCockpitVision(...args),
|
_setCockpitVision: (...args) => this._setCockpitVision(...args),
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export class CockpitViewController {
|
|||||||
this.publishRoute = publish.route ?? (() => {});
|
this.publishRoute = publish.route ?? (() => {});
|
||||||
this.publishVision = publish.vision ?? (() => {});
|
this.publishVision = publish.vision ?? (() => {});
|
||||||
this.publishSignals = publish.signals ?? (() => {});
|
this.publishSignals = publish.signals ?? (() => {});
|
||||||
|
this.publishUtilityLayout = publish.utilityLayout ?? (() => {});
|
||||||
this.presence = {
|
this.presence = {
|
||||||
active: false,
|
active: false,
|
||||||
entryAvailable: false,
|
entryAvailable: false,
|
||||||
|
|||||||
@@ -301,7 +301,14 @@ test('re-renders keep the visibility and classes the controller publishes on the
|
|||||||
const hud = $('#cockpit-hud');
|
const hud = $('#cockpit-hud');
|
||||||
store.publishPresence({ active: true, context: false });
|
store.publishPresence({ active: true, context: false });
|
||||||
hud.classList.add('voice-highlight');
|
hud.classList.add('voice-highlight');
|
||||||
hud.style.setProperty('--cockpit-utility-top', '120px');
|
hud.style.setProperty('--voice-glow', '1');
|
||||||
|
store.publishUtilityLayout({
|
||||||
|
top: '120px',
|
||||||
|
maxHeight: null,
|
||||||
|
expandedMaxHeight: null,
|
||||||
|
primaryOnly: false,
|
||||||
|
hideCollapsed: false,
|
||||||
|
});
|
||||||
$('#cockpit-speed-rim').classList.add('external');
|
$('#cockpit-speed-rim').classList.add('external');
|
||||||
updateHud.call(owner, INFO, 1000, true);
|
updateHud.call(owner, INFO, 1000, true);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -310,7 +317,13 @@ test('re-renders keep the visibility and classes the controller publishes on the
|
|||||||
assert.equal(hud.hidden, false);
|
assert.equal(hud.hidden, false);
|
||||||
assert.equal(hud.classList.contains('signals-active'), true);
|
assert.equal(hud.classList.contains('signals-active'), true);
|
||||||
assert.equal(hud.classList.contains('voice-highlight'), true);
|
assert.equal(hud.classList.contains('voice-highlight'), true);
|
||||||
|
assert.equal(hud.style.getPropertyValue('--voice-glow'), '1');
|
||||||
|
// The utility strip's measured anchor renders from the store.
|
||||||
assert.equal(hud.style.getPropertyValue('--cockpit-utility-top'), '120px');
|
assert.equal(hud.style.getPropertyValue('--cockpit-utility-top'), '120px');
|
||||||
|
store.publishUtilityLayout(null);
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(hud.style.getPropertyValue('--cockpit-utility-top'), '');
|
||||||
|
assert.equal(hud.style.getPropertyValue('--voice-glow'), '1');
|
||||||
assert.equal($('#cockpit-speed-rim').classList.contains('external'), true);
|
assert.equal($('#cockpit-speed-rim').classList.contains('external'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+35
-52
@@ -133,20 +133,8 @@ export function syncSignalLayout() {
|
|||||||
const utilityControls = document.getElementById('cockpit-utility-controls');
|
const utilityControls = document.getElementById('cockpit-utility-controls');
|
||||||
if (window.matchMedia('(max-width: 760px)').matches) {
|
if (window.matchMedia('(max-width: 760px)').matches) {
|
||||||
this.signalStream.dataset.layoutMode = 'compact-top';
|
this.signalStream.dataset.layoutMode = 'compact-top';
|
||||||
utilityControls?.classList.remove('layout-primary-only');
|
// The compact strip shows only an expanded control.
|
||||||
utilityControls
|
this.publishUtilityLayout(utilityLayoutView({ hideCollapsed: true }));
|
||||||
?.querySelectorAll('.cockpit-utility-control')
|
|
||||||
.forEach((control) => {
|
|
||||||
const hiddenSibling = Boolean(
|
|
||||||
utilityControls.querySelector(
|
|
||||||
'.cockpit-utility-control.is-expanded',
|
|
||||||
) && !control.classList.contains('is-expanded'),
|
|
||||||
);
|
|
||||||
control.setAttribute('aria-hidden', String(hiddenSibling));
|
|
||||||
});
|
|
||||||
this.hud?.style.removeProperty('--cockpit-utility-top');
|
|
||||||
this.hud?.style.removeProperty('--cockpit-utility-max-height');
|
|
||||||
this.hud?.style.removeProperty('--cockpit-utility-expanded-max-height');
|
|
||||||
this.signalStream.style.removeProperty('--cockpit-signal-right');
|
this.signalStream.style.removeProperty('--cockpit-signal-right');
|
||||||
this.signalStream.style.removeProperty('--cockpit-signal-top');
|
this.signalStream.style.removeProperty('--cockpit-signal-top');
|
||||||
this.signalStream.style.removeProperty('--cockpit-signal-max-height');
|
this.signalStream.style.removeProperty('--cockpit-signal-max-height');
|
||||||
@@ -231,22 +219,13 @@ export function syncSignalLayout() {
|
|||||||
'--cyber-cockpit-panel-height',
|
'--cyber-cockpit-panel-height',
|
||||||
`${lane.panelHeight.toFixed(2)}px`,
|
`${lane.panelHeight.toFixed(2)}px`,
|
||||||
);
|
);
|
||||||
this.hud?.style.setProperty(
|
this.publishUtilityLayout(
|
||||||
'--cockpit-utility-top',
|
utilityLayoutView({
|
||||||
`${alignedTop.toFixed(2)}px`,
|
top: `${alignedTop.toFixed(2)}px`,
|
||||||
|
maxHeight: `${lane.utilityHeight.toFixed(2)}px`,
|
||||||
|
expandedMaxHeight: `${lane.panelHeight.toFixed(2)}px`,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
this.hud?.style.setProperty(
|
|
||||||
'--cockpit-utility-max-height',
|
|
||||||
`${lane.utilityHeight.toFixed(2)}px`,
|
|
||||||
);
|
|
||||||
this.hud?.style.setProperty(
|
|
||||||
'--cockpit-utility-expanded-max-height',
|
|
||||||
`${lane.panelHeight.toFixed(2)}px`,
|
|
||||||
);
|
|
||||||
utilityControls.classList.remove('layout-primary-only');
|
|
||||||
utilityControls
|
|
||||||
.querySelectorAll('.cockpit-utility-control')
|
|
||||||
.forEach((control) => control.setAttribute('aria-hidden', 'false'));
|
|
||||||
if (expandedControl) {
|
if (expandedControl) {
|
||||||
this.signalStream.style.setProperty(
|
this.signalStream.style.setProperty(
|
||||||
'--cockpit-signal-top',
|
'--cockpit-signal-top',
|
||||||
@@ -289,14 +268,6 @@ export function syncSignalLayout() {
|
|||||||
: 0,
|
: 0,
|
||||||
});
|
});
|
||||||
const availableHeight = utilityAnchor.maxHeight;
|
const availableHeight = utilityAnchor.maxHeight;
|
||||||
this.hud?.style.setProperty(
|
|
||||||
'--cockpit-utility-top',
|
|
||||||
`${utilityAnchor.top.toFixed(1)}px`,
|
|
||||||
);
|
|
||||||
this.hud?.style.setProperty(
|
|
||||||
'--cockpit-utility-max-height',
|
|
||||||
`${availableHeight.toFixed(2)}px`,
|
|
||||||
);
|
|
||||||
const utilityLayout =
|
const utilityLayout =
|
||||||
expandedControl && collapsedControl
|
expandedControl && collapsedControl
|
||||||
? resolveCockpitUtilityLayout({
|
? resolveCockpitUtilityLayout({
|
||||||
@@ -305,25 +276,37 @@ export function syncSignalLayout() {
|
|||||||
collapsedHeight,
|
collapsedHeight,
|
||||||
})
|
})
|
||||||
: { primaryOnly: false, expandedMaxHeight: availableHeight };
|
: { primaryOnly: false, expandedMaxHeight: availableHeight };
|
||||||
utilityControls?.classList.toggle(
|
this.publishUtilityLayout(
|
||||||
'layout-primary-only',
|
utilityLayoutView({
|
||||||
utilityLayout.primaryOnly,
|
top: `${utilityAnchor.top.toFixed(1)}px`,
|
||||||
|
maxHeight: `${availableHeight.toFixed(2)}px`,
|
||||||
|
expandedMaxHeight: `${utilityLayout.expandedMaxHeight.toFixed(2)}px`,
|
||||||
|
primaryOnly: utilityLayout.primaryOnly,
|
||||||
|
hideCollapsed: utilityLayout.primaryOnly,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
this.hud?.style.setProperty(
|
// Focus leaves a control the strip is about to hide.
|
||||||
'--cockpit-utility-expanded-max-height',
|
if (
|
||||||
`${utilityLayout.expandedMaxHeight.toFixed(2)}px`,
|
utilityLayout.primaryOnly &&
|
||||||
);
|
collapsedControl?.contains(document.activeElement)
|
||||||
utilityControls
|
) {
|
||||||
?.querySelectorAll('.cockpit-utility-control')
|
|
||||||
.forEach((control) => {
|
|
||||||
const hiddenSibling =
|
|
||||||
utilityLayout.primaryOnly && control === collapsedControl;
|
|
||||||
control.setAttribute('aria-hidden', String(hiddenSibling));
|
|
||||||
if (hiddenSibling && control.contains(document.activeElement)) {
|
|
||||||
expandedControl
|
expandedControl
|
||||||
?.querySelector('.cockpit-utility-glyph')
|
?.querySelector('.cockpit-utility-glyph')
|
||||||
?.focus({ preventScroll: true });
|
?.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The utility strip's layout as CockpitView and CockpitUtilityControls
|
||||||
|
* render it: its anchor and height limits on the HUD, and which controls fit.
|
||||||
|
*/
|
||||||
|
function utilityLayoutView({
|
||||||
|
top = null,
|
||||||
|
maxHeight = null,
|
||||||
|
expandedMaxHeight = null,
|
||||||
|
primaryOnly = false,
|
||||||
|
hideCollapsed = false,
|
||||||
|
} = {}) {
|
||||||
|
return { top, maxHeight, expandedMaxHeight, primaryOnly, hideCollapsed };
|
||||||
|
}
|
||||||
@@ -232,8 +232,7 @@ export function exit({ restoreTracking = true } = {}) {
|
|||||||
this.regionalBriefSubjectId = null;
|
this.regionalBriefSubjectId = null;
|
||||||
document.body.classList.remove('cockpit-mode');
|
document.body.classList.remove('cockpit-mode');
|
||||||
this.onExited?.();
|
this.onExited?.();
|
||||||
this.hud?.style.removeProperty('--cockpit-utility-top');
|
this.publishUtilityLayout(null);
|
||||||
this.hud?.style.removeProperty('--cockpit-utility-max-height');
|
|
||||||
this.presence.active = false;
|
this.presence.active = false;
|
||||||
this.publishPresenceView();
|
this.publishPresenceView();
|
||||||
this.routeView.visible = false;
|
this.routeView.visible = false;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { setLocale } from '../i18n/index.js';
|
|||||||
import { onKeyDown as cockpitKeyDown } from './cockpitInput.js';
|
import { onKeyDown as cockpitKeyDown } from './cockpitInput.js';
|
||||||
import { RadioControls } from './radioControls.js';
|
import { RadioControls } from './radioControls.js';
|
||||||
import { renderRadioState } from './radioPresentation.js';
|
import { renderRadioState } from './radioPresentation.js';
|
||||||
|
import { syncSignalLayout } from './cockpitLayout.js';
|
||||||
|
import { useCockpitStore } from './stores/cockpit.js';
|
||||||
import { useRadioStore } from './stores/radio.js';
|
import { useRadioStore } from './stores/radio.js';
|
||||||
import CockpitUtilityControls from './views/panels/CockpitUtilityControls.vue';
|
import CockpitUtilityControls from './views/panels/CockpitUtilityControls.vue';
|
||||||
|
|
||||||
@@ -336,3 +338,115 @@ test('Cockpit Escape defers to a popover the moment its disclosure opens', async
|
|||||||
assert.equal(exits, 1);
|
assert.equal(exits, 1);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the layout pass’s measured fit hides the collapsed control and marks the strip', async () => {
|
||||||
|
document.body.replaceChildren();
|
||||||
|
const host = document.body.appendChild(document.createElement('div'));
|
||||||
|
const pinia = createPinia();
|
||||||
|
const app = createApp(CockpitUtilityControls).use(pinia);
|
||||||
|
app.mount(host);
|
||||||
|
const radio = useRadioStore(pinia);
|
||||||
|
const cockpit = useCockpitStore(pinia);
|
||||||
|
const strip = document.getElementById('cockpit-utility-controls');
|
||||||
|
const [display, radioControl] = strip.querySelectorAll(
|
||||||
|
'.cockpit-utility-control',
|
||||||
|
);
|
||||||
|
const layout = (fit) => ({
|
||||||
|
top: '96.0px',
|
||||||
|
maxHeight: '300.00px',
|
||||||
|
expandedMaxHeight: '300.00px',
|
||||||
|
primaryOnly: false,
|
||||||
|
hideCollapsed: false,
|
||||||
|
...fit,
|
||||||
|
});
|
||||||
|
// Until the layout pass runs, neither control carries aria-hidden.
|
||||||
|
assert.equal(display.hasAttribute('aria-hidden'), false);
|
||||||
|
radio.publishUtility({ display: false, radio: true });
|
||||||
|
cockpit.publishUtilityLayout(
|
||||||
|
layout({ primaryOnly: true, hideCollapsed: true }),
|
||||||
|
);
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(strip.classList.contains('layout-primary-only'), true);
|
||||||
|
assert.equal(display.getAttribute('aria-hidden'), 'true');
|
||||||
|
assert.equal(radioControl.getAttribute('aria-hidden'), 'false');
|
||||||
|
// With nothing expanded there is no collapsed sibling to hide.
|
||||||
|
radio.publishUtility({ display: false, radio: false });
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(display.getAttribute('aria-hidden'), 'false');
|
||||||
|
cockpit.publishUtilityLayout(layout());
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(strip.classList.contains('layout-primary-only'), false);
|
||||||
|
app.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the real layout pass drives the mounted controls: compact hides the collapsed sibling, Cyber shows both', async (t) => {
|
||||||
|
document.body.replaceChildren();
|
||||||
|
const host = document.body.appendChild(document.createElement('div'));
|
||||||
|
const pinia = createPinia();
|
||||||
|
const app = createApp(CockpitUtilityControls).use(pinia);
|
||||||
|
app.mount(host);
|
||||||
|
const radio = useRadioStore(pinia);
|
||||||
|
const cockpit = useCockpitStore(pinia);
|
||||||
|
const signalStream = document.body.appendChild(
|
||||||
|
document.createElement('aside'),
|
||||||
|
);
|
||||||
|
const owner = {
|
||||||
|
signalStream,
|
||||||
|
signalCollapsed: false,
|
||||||
|
contextCollapsed: false,
|
||||||
|
context: null,
|
||||||
|
publishUtilityLayout: (layout) => cockpit.publishUtilityLayout(layout),
|
||||||
|
syncContextLayout() {},
|
||||||
|
};
|
||||||
|
const originalMatchMedia = window.matchMedia;
|
||||||
|
let compact = true;
|
||||||
|
window.matchMedia = (query) => ({ matches: compact, media: query });
|
||||||
|
t.after(() => {
|
||||||
|
window.matchMedia = originalMatchMedia;
|
||||||
|
delete document.documentElement.dataset.uiTheme;
|
||||||
|
document.body.style.removeProperty('opacity');
|
||||||
|
document.documentElement.style.removeProperty('opacity');
|
||||||
|
app.unmount();
|
||||||
|
});
|
||||||
|
const strip = document.getElementById('cockpit-utility-controls');
|
||||||
|
const [display, radioControl] = strip.querySelectorAll(
|
||||||
|
'.cockpit-utility-control',
|
||||||
|
);
|
||||||
|
radio.publishUtility({ display: false, radio: true });
|
||||||
|
await nextTick();
|
||||||
|
syncSignalLayout.call(owner);
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(display.getAttribute('aria-hidden'), 'true');
|
||||||
|
assert.equal(radioControl.getAttribute('aria-hidden'), 'false');
|
||||||
|
assert.equal(cockpit.utilityLayout.top, null);
|
||||||
|
// Compact hides the sibling without marking the strip primary-only.
|
||||||
|
assert.equal(strip.classList.contains('layout-primary-only'), false);
|
||||||
|
// Cyber aligns the strip with the Data panel and keeps both controls.
|
||||||
|
compact = false;
|
||||||
|
document.documentElement.dataset.uiTheme = 'cyber';
|
||||||
|
const stack = document.body.appendChild(document.createElement('div'));
|
||||||
|
stack.id = 'left-panel-stack';
|
||||||
|
const dataPanel = stack.appendChild(document.createElement('section'));
|
||||||
|
dataPanel.id = 'data-panel';
|
||||||
|
for (const node of [
|
||||||
|
dataPanel,
|
||||||
|
stack,
|
||||||
|
document.body,
|
||||||
|
document.documentElement,
|
||||||
|
])
|
||||||
|
node.style.opacity = '1';
|
||||||
|
dataPanel.getBoundingClientRect = () => ({
|
||||||
|
top: 120,
|
||||||
|
bottom: 400,
|
||||||
|
left: 0,
|
||||||
|
right: 200,
|
||||||
|
width: 200,
|
||||||
|
height: 280,
|
||||||
|
});
|
||||||
|
syncSignalLayout.call(owner);
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(display.getAttribute('aria-hidden'), 'false');
|
||||||
|
assert.equal(radioControl.getAttribute('aria-hidden'), 'false');
|
||||||
|
assert.equal(cockpit.utilityLayout.top, '120.00px');
|
||||||
|
assert.equal(strip.classList.contains('layout-primary-only'), false);
|
||||||
|
});
|
||||||
@@ -66,6 +66,26 @@ export const useCockpitStore = defineStore('gev-cockpit', () => {
|
|||||||
signals.value = next && markRaw(next);
|
signals.value = next && markRaw(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The utility strip's measured layout: its top and height limits as CSS
|
||||||
|
* lengths (null leaves the stylesheet's), whether only the expanded control
|
||||||
|
* fits, and whether a collapsed control beside an expanded one is hidden
|
||||||
|
* from assistive technology. Null until the first layout pass.
|
||||||
|
*/
|
||||||
|
const utilityLayout = shallowRef(null);
|
||||||
|
|
||||||
|
/** @param {?object} next A fresh layout; one equal to the current is dropped. */
|
||||||
|
function publishUtilityLayout(next) {
|
||||||
|
const current = utilityLayout.value;
|
||||||
|
if (
|
||||||
|
next &&
|
||||||
|
current &&
|
||||||
|
Object.keys(next).every((key) => next[key] === current[key])
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
utilityLayout.value = next && markRaw(next);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
presence,
|
presence,
|
||||||
publishPresence,
|
publishPresence,
|
||||||
@@ -81,5 +101,7 @@ export const useCockpitStore = defineStore('gev-cockpit', () => {
|
|||||||
publishVision,
|
publishVision,
|
||||||
signals,
|
signals,
|
||||||
publishSignals,
|
publishSignals,
|
||||||
|
utilityLayout,
|
||||||
|
publishUtilityLayout,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -18,9 +18,10 @@ import CockpitUtilityControls from './panels/CockpitUtilityControls.vue';
|
|||||||
// The Cockpit HUD. The Cockpit controller presents the instruments, the
|
// The Cockpit HUD. The Cockpit controller presents the instruments, the
|
||||||
// destination arrow and the vision selector on every HUD update; until it
|
// destination arrow and the vision selector on every HUD update; until it
|
||||||
// does, they show the state they start in. The controller also publishes
|
// does, they show the state they start in. The controller also publishes
|
||||||
// which surfaces are shown; the HUD's layout stays with it, and the controls
|
// which surfaces are shown and the utility strip's measured layout; the
|
||||||
// keep its listeners.
|
// layout pass stays with it, and the controls keep its listeners.
|
||||||
const { presence, instruments, route, vision } = storeToRefs(useCockpitStore());
|
const { presence, instruments, route, vision, utilityLayout } =
|
||||||
|
storeToRefs(useCockpitStore());
|
||||||
const active = computed(() => Boolean(presence.value?.active));
|
const active = computed(() => Boolean(presence.value?.active));
|
||||||
const globe = useGlobeActionsStore();
|
const globe = useGlobeActionsStore();
|
||||||
const { resetBusy } = storeToRefs(globe);
|
const { resetBusy } = storeToRefs(globe);
|
||||||
@@ -38,6 +39,14 @@ const visionLabel = computed(() =>
|
|||||||
const visionTitle = computed(() =>
|
const visionTitle = computed(() =>
|
||||||
t('Current style: {style} — click for next', { style: v.value.name }),
|
t('Current style: {style} — click for next', { style: v.value.name }),
|
||||||
);
|
);
|
||||||
|
// The utility strip's measured anchor and height limits; a null value leaves
|
||||||
|
// the stylesheet's fallback.
|
||||||
|
const utilityStyle = computed(() => ({
|
||||||
|
'--cockpit-utility-top': utilityLayout.value?.top ?? null,
|
||||||
|
'--cockpit-utility-max-height': utilityLayout.value?.maxHeight ?? null,
|
||||||
|
'--cockpit-utility-expanded-max-height':
|
||||||
|
utilityLayout.value?.expandedMaxHeight ?? null,
|
||||||
|
}));
|
||||||
const tickStyle = (tick) =>
|
const tickStyle = (tick) =>
|
||||||
tick.slot === null
|
tick.slot === null
|
||||||
? undefined
|
? undefined
|
||||||
@@ -46,7 +55,7 @@ const tickStyle = (tick) =>
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Aircraft first-person view. It can be entered from the visible view switcher. -->
|
<!-- Aircraft first-person view. It can be entered from the visible view switcher. -->
|
||||||
<section id="cockpit-hud" aria-label="Aircraft cockpit view" :hidden="!active" v-class-toggle="{ 'signals-active': active, 'context-active': Boolean(presence?.context) }" data-i18n-aria-label :data-layer="hud.layer ?? undefined">
|
<section id="cockpit-hud" aria-label="Aircraft cockpit view" :hidden="!active" :style="utilityStyle" v-class-toggle="{ 'signals-active': active, 'context-active': Boolean(presence?.context) }" data-i18n-aria-label :data-layer="hud.layer ?? undefined">
|
||||||
<!-- One optical coordinate system: every targeting mark shares this center.
|
<!-- One optical coordinate system: every targeting mark shares this center.
|
||||||
Decorative only; telemetry and interactive panels remain separate. -->
|
Decorative only; telemetry and interactive panels remain separate. -->
|
||||||
<svg class="cockpit-frame-lines" viewBox="0 0 1200 720" preserveAspectRatio="none" aria-hidden="true" focusable="false">
|
<svg class="cockpit-frame-lines" viewBox="0 0 1200 720" preserveAspectRatio="none" aria-hidden="true" focusable="false">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { t } from '../../../i18n/index.js';
|
|||||||
import { vClassToggle } from '../../rail/classToggle.js';
|
import { vClassToggle } from '../../rail/classToggle.js';
|
||||||
import UiSlider from '../../kit/UiSlider.js';
|
import UiSlider from '../../kit/UiSlider.js';
|
||||||
import { useRadioStore } from '../../stores/radio.js';
|
import { useRadioStore } from '../../stores/radio.js';
|
||||||
|
import { useCockpitStore } from '../../stores/cockpit.js';
|
||||||
import UiButton from '../../kit/UiButton.vue';
|
import UiButton from '../../kit/UiButton.vue';
|
||||||
|
|
||||||
// The Cockpit's Display and Radio launchers and their popovers. RadioControls
|
// The Cockpit's Display and Radio launchers and their popovers. RadioControls
|
||||||
@@ -18,6 +19,17 @@ const { act } = store;
|
|||||||
const { view: radio, utility } = storeToRefs(store);
|
const { view: radio, utility } = storeToRefs(store);
|
||||||
const open = computed(() => utility.value ?? { display: false, radio: false });
|
const open = computed(() => utility.value ?? { display: false, radio: false });
|
||||||
const cockpit = computed(() => radio.value?.cockpit ?? null);
|
const cockpit = computed(() => radio.value?.cockpit ?? null);
|
||||||
|
// The Cockpit's layout pass measures which controls fit; until it runs, the
|
||||||
|
// controls carry no aria-hidden.
|
||||||
|
const { utilityLayout: layout } = storeToRefs(useCockpitStore());
|
||||||
|
const ariaHidden = (expanded) =>
|
||||||
|
layout.value
|
||||||
|
? String(
|
||||||
|
layout.value.hideCollapsed &&
|
||||||
|
!expanded &&
|
||||||
|
(open.value.display || open.value.radio),
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
const displayLabel = computed(() =>
|
const displayLabel = computed(() =>
|
||||||
open.value.display
|
open.value.display
|
||||||
? t('Collapse Cockpit display options')
|
? t('Collapse Cockpit display options')
|
||||||
@@ -31,8 +43,8 @@ const radioLabel = computed(() =>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside id="cockpit-utility-controls" class="cockpit-utility-controls" aria-label="Cockpit display and Radio controls" data-i18n-aria-label>
|
<aside id="cockpit-utility-controls" class="cockpit-utility-controls" v-class-toggle="{ 'layout-primary-only': !!layout?.primaryOnly }" aria-label="Cockpit display and Radio controls" data-i18n-aria-label>
|
||||||
<div class="cockpit-utility-control ui-scroll" v-class-toggle="{ 'is-expanded': open.display }">
|
<div class="cockpit-utility-control ui-scroll" v-class-toggle="{ 'is-expanded': open.display }" :aria-hidden="ariaHidden(open.display)">
|
||||||
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
|
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
|
||||||
<span data-i18n>DISPLAY</span>
|
<span data-i18n>DISPLAY</span>
|
||||||
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
||||||
@@ -45,7 +57,7 @@ const radioLabel = computed(() =>
|
|||||||
<div class="cockpit-display-slot" data-cockpit-display-slot="models3d"></div>
|
<div class="cockpit-display-slot" data-cockpit-display-slot="models3d"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cockpit-utility-control ui-scroll" v-class-toggle="{ 'is-expanded': open.radio }">
|
<div class="cockpit-utility-control ui-scroll" v-class-toggle="{ 'is-expanded': open.radio }" :aria-hidden="ariaHidden(open.radio)">
|
||||||
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
|
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
|
||||||
<span data-i18n>RADIO</span>
|
<span data-i18n>RADIO</span>
|
||||||
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
||||||
|
|||||||
Reference in new issue
Block a user