mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Open the cockpit panels from the rail in one step and give focus back to it
The rail's buttons are disclosures (aria-expanded, aria-controls) and go through one toggleCockpitUtility action, which already keeps one panel open. Collapsing returns focus to the rail when the launcher's toggle is hidden. The edge switch is gone: edge mode does not apply in the cockpit. Co-authored-by: Manjunath Paparaju <[email protected]>
This commit is contained in:
1 parent
0c295ea770
commit
95a0be13b6
4 files changed
+112
-48
No files matched your search
+39
-18
@@ -26,10 +26,7 @@ import {
|
|||||||
setSignalCollapsed,
|
setSignalCollapsed,
|
||||||
} from './ui/cockpitLayout.js';
|
} from './ui/cockpitLayout.js';
|
||||||
import { createBriefState } from './ui/cockpitBriefPages.js';
|
import { createBriefState } from './ui/cockpitBriefPages.js';
|
||||||
import {
|
import { publishBriefView, setBriefAutoRotate } from './ui/cockpitBriefing.js';
|
||||||
publishBriefView,
|
|
||||||
setBriefAutoRotate,
|
|
||||||
} from './ui/cockpitBriefing.js';
|
|
||||||
import { CockpitDisplayPortal } from './ui/cockpitDisplayPortal.js';
|
import { CockpitDisplayPortal } from './ui/cockpitDisplayPortal.js';
|
||||||
import { _handleContextLayerChange } from './ui/contextLayerChanges.js';
|
import { _handleContextLayerChange } from './ui/contextLayerChanges.js';
|
||||||
import { clearSelectedLayers } from './ui/contextActions.js';
|
import { clearSelectedLayers } from './ui/contextActions.js';
|
||||||
@@ -438,13 +435,10 @@ test('Cockpit owns a focused shared Display portal and compact Radio controls',
|
|||||||
/data-cockpit-launcher="radio"[\s\S]*?id="cockpit-radio-toggle-btn"/,
|
/data-cockpit-launcher="radio"[\s\S]*?id="cockpit-radio-toggle-btn"/,
|
||||||
);
|
);
|
||||||
const cockpitRadio = html.match(
|
const cockpitRadio = html.match(
|
||||||
/id="cockpit-radio-panel"[\s\S]*?<\/div>\s*<\/div>\s*<\/aside>/,
|
/id="cockpit-radio-panel"[\s\S]*?<\/div>\s*<\/div>\s*<div class="cockpit-utility-rail/,
|
||||||
);
|
);
|
||||||
assert.ok(cockpitRadio, 'Cockpit compact Radio controls are missing');
|
assert.ok(cockpitRadio, 'Cockpit compact Radio controls are missing');
|
||||||
assert.doesNotMatch(
|
assert.doesNotMatch(cockpitRadio[0], /radio-filter|radio-tuner/);
|
||||||
cockpitRadio[0],
|
|
||||||
/radio-filter|radio-tuner/,
|
|
||||||
);
|
|
||||||
assert.match(
|
assert.match(
|
||||||
css,
|
css,
|
||||||
/\.cockpit-utility-controls[\s\S]*?top:\s*var\(\s*--cockpit-utility-top/,
|
/\.cockpit-utility-controls[\s\S]*?top:\s*var\(\s*--cockpit-utility-top/,
|
||||||
@@ -811,18 +805,27 @@ test('Reset releases Contact camera ownership through its selection-preserving r
|
|||||||
);
|
);
|
||||||
assert.match(ui, /onReset: \(\) => this\.resetToGlobeView\(\)/);
|
assert.match(ui, /onReset: \(\) => this\.resetToGlobeView\(\)/);
|
||||||
assert.match(ui, /this\._connectGlobeReset\?\.\(this\._locationControls\)/);
|
assert.match(ui, /this\._connectGlobeReset\?\.\(this\._locationControls\)/);
|
||||||
assert.match(controls, /reset\(\) \{\s*if \(!this\.destroyed\) return this\.onReset\(\);/);
|
assert.match(
|
||||||
|
controls,
|
||||||
|
/reset\(\) \{\s*if \(!this\.destroyed\) return this\.onReset\(\);/,
|
||||||
|
);
|
||||||
// Both reset buttons ask the one reset owner.
|
// Both reset buttons ask the one reset owner.
|
||||||
const globeActions = fs.readFileSync(
|
const globeActions = fs.readFileSync(
|
||||||
path.join(ROOT, 'src', 'ui', 'views', 'panels', 'GlobeActions.vue'),
|
path.join(ROOT, 'src', 'ui', 'views', 'panels', 'GlobeActions.vue'),
|
||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
assert.match(globeActions, /id="reset-globe-view"[^>]*@click="act\('reset'\)"/);
|
assert.match(
|
||||||
|
globeActions,
|
||||||
|
/id="reset-globe-view"[^>]*@click="act\('reset'\)"/,
|
||||||
|
);
|
||||||
const cockpitView = readRadioSource(
|
const cockpitView = readRadioSource(
|
||||||
new URL('./ui/views/CockpitView.vue', import.meta.url),
|
new URL('./ui/views/CockpitView.vue', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
assert.match(cockpitView, /id="cockpit-reset-globe"[^>]*@click="globe\.act\('reset'\)"/);
|
assert.match(
|
||||||
|
cockpitView,
|
||||||
|
/id="cockpit-reset-globe"[^>]*@click="globe\.act\('reset'\)"/,
|
||||||
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
cockpitEnter.toString(),
|
cockpitEnter.toString(),
|
||||||
/Object\.assign\(this\.presence, \{\s*active: true,/,
|
/Object\.assign\(this\.presence, \{\s*active: true,/,
|
||||||
@@ -1193,7 +1196,10 @@ test('cockpit summary presents the focused item as Contact', () => {
|
|||||||
const match = html.match(/<aside id="cockpit-context"[\s\S]*?<\/aside>/);
|
const match = html.match(/<aside id="cockpit-context"[\s\S]*?<\/aside>/);
|
||||||
assert.ok(match, 'cockpit Contact summary is missing');
|
assert.ok(match, 'cockpit Contact summary is missing');
|
||||||
assert.match(match[0], /aria-label="Contact cockpit summary"/);
|
assert.match(match[0], /aria-label="Contact cockpit summary"/);
|
||||||
assert.match(match[0], /class="cockpit-card-kicker cockpit-context-kicker">CONTACT</);
|
assert.match(
|
||||||
|
match[0],
|
||||||
|
/class="cockpit-card-kicker cockpit-context-kicker">CONTACT</,
|
||||||
|
);
|
||||||
assert.match(match[0], /aria-label="Contact navigation"/);
|
assert.match(match[0], /aria-label="Contact navigation"/);
|
||||||
assert.match(
|
assert.match(
|
||||||
match[0],
|
match[0],
|
||||||
@@ -1408,15 +1414,30 @@ test('Contact folded for one Cockpit left panel comes back only once neither is
|
|||||||
const previous = globalThis.document;
|
const previous = globalThis.document;
|
||||||
const panels = {};
|
const panels = {};
|
||||||
globalThis.document = { getElementById: (id) => panels[id] ?? null };
|
globalThis.document = { getElementById: (id) => panels[id] ?? null };
|
||||||
const panel = (...classes) => ({ classList: { contains: (name) => classes.includes(name) } });
|
const panel = (...classes) => ({
|
||||||
|
classList: { contains: (name) => classes.includes(name) },
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const otherOpen = (id) => PanelChrome.prototype._otherCockpitLeftPanelOpen(id);
|
const otherOpen = (id) =>
|
||||||
|
PanelChrome.prototype._otherCockpitLeftPanelOpen(id);
|
||||||
panels['data-panel'] = panel('collapsed');
|
panels['data-panel'] = panel('collapsed');
|
||||||
assert.equal(otherOpen('utilities-panel'), false, 'Data Layers closed: restore');
|
assert.equal(
|
||||||
|
otherOpen('utilities-panel'),
|
||||||
|
false,
|
||||||
|
'Data Layers closed: restore',
|
||||||
|
);
|
||||||
panels['data-panel'] = panel('collapsed', 'layout-auto-collapsed');
|
panels['data-panel'] = panel('collapsed', 'layout-auto-collapsed');
|
||||||
assert.equal(otherOpen('utilities-panel'), true, 'Data Layers comes back: keep Contact folded');
|
assert.equal(
|
||||||
|
otherOpen('utilities-panel'),
|
||||||
|
true,
|
||||||
|
'Data Layers comes back: keep Contact folded',
|
||||||
|
);
|
||||||
panels['data-panel'] = panel();
|
panels['data-panel'] = panel();
|
||||||
assert.equal(otherOpen('utilities-panel'), true, 'Data Layers open: keep it folded');
|
assert.equal(
|
||||||
|
otherOpen('utilities-panel'),
|
||||||
|
true,
|
||||||
|
'Data Layers open: keep it folded',
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.document = previous;
|
globalThis.document = previous;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -450,3 +450,42 @@ test('the real layout pass drives the mounted controls: compact hides the collap
|
|||||||
assert.equal(cockpit.utilityLayout.top, '120.00px');
|
assert.equal(cockpit.utilityLayout.top, '120.00px');
|
||||||
assert.equal(strip.classList.contains('layout-primary-only'), false);
|
assert.equal(strip.classList.contains('layout-primary-only'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the rail opens one panel at a time and reads as a disclosure', async (t) => {
|
||||||
|
const { $, controls } = fixture(t);
|
||||||
|
const item = (kind) =>
|
||||||
|
document.querySelector(`[data-cockpit-rail-item="${kind}"]`);
|
||||||
|
item('display').click();
|
||||||
|
await nextTick();
|
||||||
|
assert.equal($('cockpit-display-panel').hidden, false);
|
||||||
|
assert.equal(item('display').getAttribute('aria-expanded'), 'true');
|
||||||
|
assert.equal(
|
||||||
|
item('display').getAttribute('aria-controls'),
|
||||||
|
'cockpit-display-panel',
|
||||||
|
);
|
||||||
|
item('radio').click();
|
||||||
|
await nextTick();
|
||||||
|
assert.equal($('cockpit-display-panel').hidden, true);
|
||||||
|
assert.equal($('cockpit-radio-panel').hidden, false);
|
||||||
|
assert.equal(item('display').getAttribute('aria-expanded'), 'false');
|
||||||
|
assert.equal(item('radio').getAttribute('aria-expanded'), 'true');
|
||||||
|
item('radio').click();
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(controls._cockpitDisclosureOpen('radio'), false);
|
||||||
|
assert.equal(controls._cockpitDisclosureOpen('display'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collapsing an open panel hands focus to the rail when its toggle is hidden', async (t) => {
|
||||||
|
const { $, controls } = fixture(t);
|
||||||
|
const railItem = document.querySelector('[data-cockpit-rail-item="radio"]');
|
||||||
|
// happy-dom lays nothing out: show the rail and hide the launcher's toggle
|
||||||
|
// as the desktop look does.
|
||||||
|
railItem.getClientRects = () => [{ width: 32, height: 32 }];
|
||||||
|
$('cockpit-radio-toggle-btn').getClientRects = () => [];
|
||||||
|
controls._setCockpitDisclosure('radio', true);
|
||||||
|
await nextTick();
|
||||||
|
document.querySelector('.cockpit-utility-collapse').click();
|
||||||
|
await nextTick();
|
||||||
|
assert.equal($('cockpit-radio-panel').hidden, true);
|
||||||
|
assert.equal(document.activeElement, railItem);
|
||||||
|
});
|
||||||
+22
-4
@@ -27,10 +27,18 @@ export function bindRadioControls() {
|
|||||||
this.store?.publishUtility({ ...this._cockpitDisclosure });
|
this.store?.publishUtility({ ...this._cockpitDisclosure });
|
||||||
if (displayOpen) this.actions.revealStyleParameters();
|
if (displayOpen) this.actions.revealStyleParameters();
|
||||||
if (!expanded && returnFocus) {
|
if (!expanded && returnFocus) {
|
||||||
(kind === 'display'
|
// The desktop cockpit hides the launchers' toggles behind its rail, so
|
||||||
? this._cockpitDisplayToggleBtn
|
// focus goes to whichever of the two is shown.
|
||||||
: this._cockpitRadioToggleBtn
|
const toggle =
|
||||||
)?.focus({ preventScroll: true });
|
kind === 'display'
|
||||||
|
? this._cockpitDisplayToggleBtn
|
||||||
|
: this._cockpitRadioToggleBtn;
|
||||||
|
const railItem = toggle?.ownerDocument?.querySelector?.(
|
||||||
|
`[data-cockpit-rail-item="${kind}"]`,
|
||||||
|
);
|
||||||
|
(toggle?.getClientRects?.().length ? toggle : railItem || toggle)?.focus({
|
||||||
|
preventScroll: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!displayOpen &&
|
!displayOpen &&
|
||||||
@@ -444,6 +452,16 @@ export function bindRadioControls() {
|
|||||||
),
|
),
|
||||||
toggleCockpitRadio: () =>
|
toggleCockpitRadio: () =>
|
||||||
setCockpitDisclosure('radio', !this._cockpitDisclosureOpen('radio')),
|
setCockpitDisclosure('radio', !this._cockpitDisclosureOpen('radio')),
|
||||||
|
/**
|
||||||
|
* Open or close the cockpit's Display or Radio panel; opening one closes
|
||||||
|
* the other.
|
||||||
|
* @param {'display'|'radio'} kind
|
||||||
|
* @param {{ returnFocus?: boolean }} [options]
|
||||||
|
*/
|
||||||
|
toggleCockpitUtility: (kind, { returnFocus = false } = {}) => {
|
||||||
|
const open = !this._cockpitDisclosureOpen(kind);
|
||||||
|
setCockpitDisclosure(kind, open, { returnFocus: !open && returnFocus });
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @param {string} filter
|
* @param {string} filter
|
||||||
* @returns {string} The station tag the filter shows afterwards.
|
* @returns {string} The station tag the filter shows afterwards.
|
||||||
|
|||||||
@@ -8,25 +8,13 @@ import { useRadioStore } from '../../stores/radio.js';
|
|||||||
import { useCockpitStore } from '../../stores/cockpit.js';
|
import { useCockpitStore } from '../../stores/cockpit.js';
|
||||||
import { useDisplayStore } from '../../stores/display.js';
|
import { useDisplayStore } from '../../stores/display.js';
|
||||||
import UiButton from '../../kit/UiButton.vue';
|
import UiButton from '../../kit/UiButton.vue';
|
||||||
import UiPanelEdge from '../../kit/UiPanelEdge.vue';
|
|
||||||
|
|
||||||
// The open panel's header carries the same edge switch and collapse button as
|
// The Cockpit's Display and Radio launchers and their popovers, and the
|
||||||
// the main panels; collapsing goes through each launcher's own toggle.
|
// desktop look's rail, whose icons stay at the edge and open one panel at a
|
||||||
function collapse(which) {
|
// time as the main page's rails do. RadioControls presents which popover is
|
||||||
if (which === 'radio') act('toggleCockpitRadio');
|
// open and the compact Radio controls; until it does, both are closed and the
|
||||||
else document.getElementById('cockpit-display-toggle-btn')?.click();
|
// controls show their initial state. The controls call RadioControls through
|
||||||
}
|
// the store. The Display popover's slots
|
||||||
// The rail opens one panel at a time, as the main page's rails do.
|
|
||||||
function railToggle(which) {
|
|
||||||
const other = which === 'radio' ? 'display' : 'radio';
|
|
||||||
if (open.value[other]) collapse(other);
|
|
||||||
collapse(which);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Cockpit's Display and Radio launchers and their popovers. RadioControls
|
|
||||||
// presents which popover is open and the compact Radio controls; until it
|
|
||||||
// does, both are closed and the controls show their initial state. The
|
|
||||||
// controls call RadioControls through the store. The Display popover's slots
|
|
||||||
// are static: the Display panel teleports its real controls into them while
|
// are static: the Display panel teleports its real controls into them while
|
||||||
// the Cockpit display portal publishes them.
|
// the Cockpit display portal publishes them.
|
||||||
const store = useRadioStore();
|
const store = useRadioStore();
|
||||||
@@ -66,8 +54,7 @@ const radioLabel = computed(() =>
|
|||||||
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
|
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
|
||||||
<span>{{ t('DISPLAY') }}</span>
|
<span>{{ t('DISPLAY') }}</span>
|
||||||
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
||||||
<UiPanelEdge v-if="open.display" />
|
<button v-if="open.display" type="button" class="cockpit-utility-collapse" :aria-label="t('Collapse panel')" :title="t('Collapse panel')" @click="act('toggleCockpitUtility', 'display', { returnFocus: true })">−</button>
|
||||||
<button v-if="open.display" type="button" class="cockpit-utility-collapse" :aria-label="t('Collapse panel')" :title="t('Collapse panel')" @click="collapse('display')">−</button>
|
|
||||||
<button id="cockpit-display-toggle-btn" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.display)" aria-controls="cockpit-display-panel" :aria-label="displayLabel" :title="displayLabel">{{ open.display ? '▶' : '◀' }}</button>
|
<button id="cockpit-display-toggle-btn" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.display)" aria-controls="cockpit-display-panel" :aria-label="displayLabel" :title="displayLabel">{{ open.display ? '▶' : '◀' }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="cockpit-display-panel" class="cockpit-utility-popover ui-scroll" v-class-toggle="{ 'uses-shared-display-controls': !!cockpitPortal }" role="group" :aria-label="t('Cockpit display options')" :hidden="!open.display">
|
<div id="cockpit-display-panel" class="cockpit-utility-popover ui-scroll" v-class-toggle="{ 'uses-shared-display-controls': !!cockpitPortal }" role="group" :aria-label="t('Cockpit display options')" :hidden="!open.display">
|
||||||
@@ -81,8 +68,7 @@ const radioLabel = computed(() =>
|
|||||||
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
|
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
|
||||||
<span>{{ t('RADIO') }}</span>
|
<span>{{ t('RADIO') }}</span>
|
||||||
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
||||||
<UiPanelEdge v-if="open.radio" />
|
<button v-if="open.radio" type="button" class="cockpit-utility-collapse" :aria-label="t('Collapse panel')" :title="t('Collapse panel')" @click="act('toggleCockpitUtility', 'radio', { returnFocus: true })">−</button>
|
||||||
<button v-if="open.radio" type="button" class="cockpit-utility-collapse" :aria-label="t('Collapse panel')" :title="t('Collapse panel')" @click="collapse('radio')">−</button>
|
|
||||||
<button id="cockpit-radio-toggle-btn" @click="act('toggleCockpitRadio')" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.radio)" aria-controls="cockpit-radio-panel" :aria-label="radioLabel" :title="radioLabel">{{ open.radio ? '▶' : '◀' }}</button>
|
<button id="cockpit-radio-toggle-btn" @click="act('toggleCockpitRadio')" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.radio)" aria-controls="cockpit-radio-panel" :aria-label="radioLabel" :title="radioLabel">{{ open.radio ? '▶' : '◀' }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="cockpit-radio-panel" class="cockpit-utility-popover cockpit-radio-compact" role="group" :aria-label="t('Cockpit compact Radio controls')" :hidden="!open.radio">
|
<div id="cockpit-radio-panel" class="cockpit-utility-popover cockpit-radio-compact" role="group" :aria-label="t('Cockpit compact Radio controls')" :hidden="!open.radio">
|
||||||
@@ -100,15 +86,15 @@ const radioLabel = computed(() =>
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav class="cockpit-utility-rail" :class="{ 'is-open': open.display || open.radio }" :aria-label="t('Cockpit display and Radio controls')">
|
<div class="cockpit-utility-rail" :class="{ 'is-open': open.display || open.radio }">
|
||||||
<span class="cockpit-utility-rail__labels" aria-hidden="true">
|
<span class="cockpit-utility-rail__labels" aria-hidden="true">
|
||||||
<span>{{ t('DISPLAY') }}</span>
|
<span>{{ t('DISPLAY') }}</span>
|
||||||
<span>{{ t('RADIO') }}</span>
|
<span>{{ t('RADIO') }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="cockpit-utility-rail__icons">
|
<span class="cockpit-utility-rail__icons">
|
||||||
<button type="button" class="cockpit-utility-rail__item" :class="{ 'is-on': open.display }" :aria-pressed="String(open.display)" :aria-label="displayLabel" :title="displayLabel" @click="railToggle('display')"><span class="material-symbols-outlined" aria-hidden="true">tune</span></button>
|
<button type="button" class="cockpit-utility-rail__item" data-cockpit-rail-item="display" :class="{ 'is-on': open.display }" :aria-expanded="String(open.display)" aria-controls="cockpit-display-panel" :aria-label="displayLabel" :title="displayLabel" @click="act('toggleCockpitUtility', 'display')"><span class="material-symbols-outlined" aria-hidden="true">tune</span></button>
|
||||||
<button type="button" class="cockpit-utility-rail__item" :class="{ 'is-on': open.radio }" :aria-pressed="String(open.radio)" :aria-label="radioLabel" :title="radioLabel" @click="railToggle('radio')"><span class="material-symbols-outlined" aria-hidden="true">radio</span></button>
|
<button type="button" class="cockpit-utility-rail__item" data-cockpit-rail-item="radio" :class="{ 'is-on': open.radio }" :aria-expanded="String(open.radio)" aria-controls="cockpit-radio-panel" :aria-label="radioLabel" :title="radioLabel" @click="act('toggleCockpitUtility', 'radio')"><span class="material-symbols-outlined" aria-hidden="true">radio</span></button>
|
||||||
</span>
|
</span>
|
||||||
</nav>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
Reference in new issue
Block a user