From a3fc475cbe4dd9e95495749a17ea725d10fcd0aa Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 08:07:19 -0800 Subject: [PATCH] feat(cockpit): render the Contact and briefing cards' collapse from the store Cockpit presence now carries contextCollapsed and signalCollapsed. setContextCollapsed and setSignalCollapsed update presence and republish it only when the value changes, instead of writing the dataset and the toggle attributes. CockpitContext and CockpitBriefing bind data-collapsed and their toggles' aria-expanded, aria-label, title and icon from presence. The Cockpit utility QA script waits a frame for Vue to render before it reads the toggles. --- CHANGELOG.md | 5 +++ docs/VUE-MIGRATION.md | 8 +++-- scripts/qa-cockpit-utility.mjs | 8 +++-- src/cockpitMarkup.test.mjs | 8 +++-- src/ui/cockpitBriefing.test.mjs | 28 +++++++++++++++++ src/ui/cockpitContext.test.mjs | 34 ++++++++++++++++++-- src/ui/cockpitController.js | 3 ++ src/ui/cockpitLayout.js | 42 +++++-------------------- src/ui/views/panels/CockpitBriefing.vue | 8 +++-- src/ui/views/panels/CockpitContext.vue | 12 ++++--- 10 files changed, 106 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 198b15a5f..44bbcc175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,11 @@ that there is no panel to open instead of writing its classes (samehkhamis). +- The Cockpit Contact and briefing cards' collapse renders from the + `gev-cockpit` store: the controller publishes `contextCollapsed` and + `signalCollapsed` in its presence, and each card's `data-collapsed` and + its toggle's state, words and icon follow it (samehkhamis). + ## [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 diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index b0b9448ae..402010caa 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -240,9 +240,11 @@ a feature supplying only its accent. publishes which surfaces are shown: the entry button and TR-3B chip in the Context panel, the HUD with its exit and reset buttons and briefing card, and the Contact card. Focus moves to the exit button, and back to - the entry, only after Vue renders them. The controls keep the - controller's listeners, and the cards' collapse and layout stay with the - controller, which reads their visibility when it lays them out. + the entry, only after Vue renders them. The cards' collapse is part of + 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 the cards' layout stays with the controller, which reads their + visibility when it lays them out. `CockpitUtilityControls` renders the Display and Radio launchers, which popover is open and the compact Radio controls from the `gev-radio` store that RadioControls publishes to; the diff --git a/scripts/qa-cockpit-utility.mjs b/scripts/qa-cockpit-utility.mjs index e8b78e4bc..c5a94018f 100644 --- a/scripts/qa-cockpit-utility.mjs +++ b/scripts/qa-cockpit-utility.mjs @@ -2087,7 +2087,7 @@ try { } await page.evaluate(() => window.__godsEyeView.styleManager._setHudVariant('tactical')); - const signalTransition = await page.evaluate(() => { + const signalTransition = await page.evaluate(async () => { const manager = window.__godsEyeView.styleManager; const cockpit = manager.cockpitView; const signalToggle = document.getElementById('cockpit-signal-toggle'); @@ -2114,6 +2114,8 @@ try { manager._setCockpitDisclosure('radio', true); manager._setCockpitDisclosure('radio', false); const manualCollapseRetained = cockpit.signalCollapsed && cockpit.signalUserCollapsed; + // The toggle renders the published collapse on Vue's next flush. + await new Promise((resolve) => requestAnimationFrame(resolve)); return { afterExpansion, afterRepeatedExpansion, @@ -2140,7 +2142,7 @@ try { && signalTransition.signalAriaExpanded === 'false', JSON.stringify(signalTransition), ); - const contextTransition = await page.evaluate(() => { + const contextTransition = await page.evaluate(async () => { const cockpit = window.__godsEyeView.styleManager.cockpitView; const priorCollapsed = cockpit.contextCollapsed; const contextToggle = document.getElementById('cockpit-context-toggle'); @@ -2152,6 +2154,8 @@ try { cockpit.setContextCollapsed(false); const afterExpansion = expandedEvents; cockpit.setContextCollapsed(false); + // The toggle renders the published collapse on Vue's next flush. + await new Promise((resolve) => requestAnimationFrame(resolve)); return { afterExpansion, afterRepeatedExpansion: expandedEvents, diff --git a/src/cockpitMarkup.test.mjs b/src/cockpitMarkup.test.mjs index 9d1b5973c..5845ca9c2 100644 --- a/src/cockpitMarkup.test.mjs +++ b/src/cockpitMarkup.test.mjs @@ -1197,9 +1197,13 @@ test('cockpit summary presents the focused item as Contact', () => { ); assert.match(match[0], /aria-label="Collapse Contact panel"/); assert.doesNotMatch(match[0], />GLOBAL CONTEXT { await nextTick(); assert.equal(text('#cockpit-brief-kicker'), 'НОВОСТИ РЕГИОНА'); }); + +test('the card and its toggle render the collapse the controller presents', async (t) => { + const { $, owner } = fixture(t); + const store = useCockpitStore(); + Object.assign(owner, { + active: true, + signalCollapsed: false, + presence: { active: true, signalCollapsed: false }, + publishPresenceView: () => store.publishPresence({ ...owner.presence }), + }); + const card = $('#cockpit-signal-stream'); + const toggle = $('#cockpit-signal-toggle'); + assert.equal(card.hasAttribute('data-collapsed'), false); + assert.equal(toggle.getAttribute('aria-expanded'), 'true'); + setSignalCollapsed.call(owner, true, { user: true }); + await nextTick(); + assert.equal(card.dataset.collapsed, 'true'); + assert.equal(toggle.getAttribute('aria-expanded'), 'false'); + assert.equal(toggle.getAttribute('aria-label'), 'Expand cockpit briefing panel'); + assert.equal(toggle.title, 'Expand briefing panel'); + assert.equal(toggle.textContent.trim(), 'right_panel_open'); + assert.equal(owner.signalUserCollapsed, true); + setSignalCollapsed.call(owner, false); + await nextTick(); + assert.equal(card.dataset.collapsed, 'false'); + assert.equal(toggle.textContent.trim(), 'right_panel_close'); +}); diff --git a/src/ui/cockpitContext.test.mjs b/src/ui/cockpitContext.test.mjs index 7b0ad20f3..47d3719c7 100644 --- a/src/ui/cockpitContext.test.mjs +++ b/src/ui/cockpitContext.test.mjs @@ -13,6 +13,7 @@ import { } from './cockpitContext.js'; import { createContextState } from './cockpitContextState.js'; import { syncWeatherToggle } from './cockpitInstruments.js'; +import { setContextCollapsed } from './cockpitLayout.js'; import { useCockpitStore } from './stores/cockpit.js'; import CockpitContext from './views/panels/CockpitContext.vue'; @@ -218,11 +219,12 @@ test('the weather toggle renders its state and the action it offers', async (t) assert.equal(owner.contextView.weather, true); }); -test('re-renders leave the card visibility and layout to the controller', async (t) => { +test('re-renders keep the collapse the controller presents and leave it the layout', async (t) => { const { $, owner } = fixture(t, snapshot()); updateContext.call(owner, INFO, 0); const card = $('#cockpit-context'); - card.dataset.collapsed = 'true'; + owner.presence.contextCollapsed = true; + owner.publishPresenceView(); card.dataset.layoutMode = 'compact-bottom'; owner.snapshot = snapshot({ evaluatedAt: Date.UTC(2026, 9, 31, 12, 35) }); updateContext.call(owner, INFO, 0); @@ -230,6 +232,11 @@ test('re-renders leave the card visibility and layout to the controller', async assert.equal(card.hidden, false); assert.equal(card.dataset.collapsed, 'true'); assert.equal(card.dataset.layoutMode, 'compact-bottom'); + const toggle = $('#cockpit-context-toggle'); + assert.equal(toggle.getAttribute('aria-expanded'), 'false'); + assert.equal(toggle.getAttribute('aria-label'), 'Expand Contact panel'); + assert.equal(toggle.title, 'Expand contact panel'); + assert.equal(toggle.textContent.trim(), 'chevron_right'); }); test('the card is labelled in the UI language', async (t) => { @@ -248,3 +255,26 @@ test('the card is labelled in the UI language', async (t) => { translate('Disable cockpit weather effects'), ); }); + +test('collapsing the card publishes once per change', async (t) => { + const { $, owner } = fixture(t); + let published = 0; + const publish = owner.publishPresenceView; + Object.assign(owner, { + contextCollapsed: false, + presence: { context: true, contextCollapsed: false }, + publishPresenceView: () => { + published += 1; + publish(); + }, + }); + setContextCollapsed.call(owner, true); + setContextCollapsed.call(owner, true); + assert.equal(published, 1); + await nextTick(); + assert.equal($('#cockpit-context').dataset.collapsed, 'true'); + assert.equal( + $('#cockpit-context-toggle').getAttribute('aria-expanded'), + 'false', + ); +}); diff --git a/src/ui/cockpitController.js b/src/ui/cockpitController.js index 8b62e3b51..d8cb65dd2 100644 --- a/src/ui/cockpitController.js +++ b/src/ui/cockpitController.js @@ -156,6 +156,9 @@ export class CockpitViewController { entryAvailable: false, tr3b: null, context: false, + // The Contact and briefing cards' collapse, which their toggles show. + contextCollapsed: false, + signalCollapsed: false, }; this.instruments = createInstrumentState(); this.routeView = createRouteState(); diff --git a/src/ui/cockpitLayout.js b/src/ui/cockpitLayout.js index 4f0759d6a..9e1dad26f 100644 --- a/src/ui/cockpitLayout.js +++ b/src/ui/cockpitLayout.js @@ -1,5 +1,4 @@ /** Measure Cockpit panels against visible HUD and viewport obstacles. */ -import { t, msg } from '../i18n/index.js'; import { resolveCockpitUtilityAnchor, resolveCockpitUtilityLayout, @@ -35,20 +34,10 @@ export function scheduleContextLayout() { export function setContextCollapsed(collapsed) { const wasCollapsed = this.contextCollapsed; this.contextCollapsed = Boolean(collapsed); - if (this.context) - this.context.dataset.collapsed = String(this.contextCollapsed); - if (this.contextToggle) { - const expanded = !this.contextCollapsed; - this.contextToggle.setAttribute('aria-expanded', String(expanded)); - this.contextToggle.setAttribute( - 'aria-label', - expanded ? t('Collapse Contact panel') : t('Expand Contact panel'), - ); - this.contextToggle.title = t( - expanded ? msg('Collapse contact panel') : msg('Expand contact panel'), - ); - const icon = this.contextToggle.querySelector('.material-symbols-outlined'); - if (icon) icon.textContent = expanded ? 'chevron_left' : 'chevron_right'; + // The Contact card and its toggle render the collapse from the presence. + if (this.presence.contextCollapsed !== this.contextCollapsed) { + this.presence.contextCollapsed = this.contextCollapsed; + this.publishPresenceView(); } if (this.active && wasCollapsed && !this.contextCollapsed) { window.dispatchEvent(new CustomEvent('gev:cockpit-context-expanded')); @@ -60,25 +49,10 @@ export function setSignalCollapsed(collapsed, { user = false } = {}) { const wasCollapsed = this.signalCollapsed; this.signalCollapsed = Boolean(collapsed); if (user) this.signalUserCollapsed = this.signalCollapsed; - if (this.signalStream) - this.signalStream.dataset.collapsed = String(this.signalCollapsed); - if (this.signalToggle) { - const expanded = !this.signalCollapsed; - this.signalToggle.setAttribute('aria-expanded', String(expanded)); - this.signalToggle.setAttribute( - 'aria-label', - t( - expanded - ? msg('Collapse cockpit briefing panel') - : msg('Expand cockpit briefing panel'), - ), - ); - this.signalToggle.title = t( - expanded ? msg('Collapse briefing panel') : msg('Expand briefing panel'), - ); - const icon = this.signalToggle.querySelector('.material-symbols-outlined'); - if (icon) - icon.textContent = expanded ? 'right_panel_close' : 'right_panel_open'; + // The briefing card and its toggle render the collapse from the presence. + if (this.presence.signalCollapsed !== this.signalCollapsed) { + this.presence.signalCollapsed = this.signalCollapsed; + this.publishPresenceView(); } if (this.signalCollapsed) this.stopBriefRotation(); else this.startBriefRotation({ reset: true }); diff --git a/src/ui/views/panels/CockpitBriefing.vue b/src/ui/views/panels/CockpitBriefing.vue index b13956f94..da0165603 100644 --- a/src/ui/views/panels/CockpitBriefing.vue +++ b/src/ui/views/panels/CockpitBriefing.vue @@ -1,6 +1,7 @@