fix: preserve keyboard focus and separate Space activation from voice

Keep focus visible across controls and stable during Contacts, Cockpit and mission roster updates. Preserve native short Space activation, and claim push-to-talk only after a 500 ms hold. Add panel Escape handling and keyboard-accessible attribution controls.

Retains the Map Source foundation contributed in https://github.com/bilawalsidhu/gods-eye-view/pull/171.
This commit is contained in:
manjunath22466 authored and Sameh Khamis committed 2026-09-10 23:48:12 -07:00
1 parent c82fdd7478
commit 5228ab9e40
22 files changed
+3500 -157

No files matched your search

+158
View File
@@ -2757,6 +2757,7 @@ function makeControlElement() {
focus() { if (globalThis.document) globalThis.document.activeElement = this; },
addEventListener(name, handler) { this.listeners[name] = handler; },
setAttribute(name, value) { this.attributes[name] = String(value); },
getAttribute(name) { return this.attributes[name] ?? null; },
closest(selector) {
const className = selector.slice(1);
return String(this.className).split(/\s+/).includes(className) ? this : null;
@@ -2783,6 +2784,163 @@ function makeControlElement() {
return element;
}
test('keyboard focus survives Data Layer enabling and disabling transitions', async () => {
const originalDocument = globalThis.document;
globalThis.document = { createElement: makeControlElement, activeElement: null };
const mgr = new DataLayerManager({});
let releaseEnable;
let releaseDisable;
const calls = { enable: 0, disable: 0 };
mgr.register({
id: 'focus-layer',
name: 'Focus layer',
icon: '',
source: 'Local focus fixture',
updateInterval: -1,
init() {},
enable() {
calls.enable += 1;
return new Promise((resolve) => { releaseEnable = resolve; });
},
disable() {
calls.disable += 1;
return new Promise((resolve) => { releaseDisable = resolve; });
},
update() {},
destroy() {},
getStats() { return { count: 1, lastUpdate: Date.now() }; },
});
const container = makeControlElement();
const waitFor = async (predicate, label) => {
for (let attempt = 0; attempt < 20; attempt += 1) {
if (predicate()) return;
await Promise.resolve();
}
assert.fail(`timed out waiting for ${label}`);
};
try {
mgr.buildTogglePanel(container);
const row = container.querySelector('[data-layer-id="focus-layer"]');
const button = row.querySelector('.data-toggle-btn');
button.focus();
const enablingClick = button.listeners.click();
await waitFor(() => typeof releaseEnable === 'function', 'enable transition');
assert.equal(globalThis.document.activeElement, button);
assert.equal(button.disabled, false, 'ENABLING never uses native disabled');
assert.equal(button.getAttribute('aria-disabled'), 'true');
assert.equal(button.getAttribute('aria-busy'), 'true');
assert.equal(button.textContent, 'ENABLING');
await button.listeners.click();
assert.equal(calls.enable, 1, 'repeat activation is inert while enabling');
assert.equal(mgr.layers.get('focus-layer').visibilityIntentEpoch, 1);
releaseEnable();
await enablingClick;
assert.equal(globalThis.document.activeElement, button);
assert.equal(button.disabled, false);
assert.equal(button.getAttribute('aria-disabled'), 'false');
assert.equal(button.getAttribute('aria-busy'), 'false');
assert.equal(mgr.isEnabled('focus-layer'), true);
const disablingClick = button.listeners.click();
await waitFor(() => typeof releaseDisable === 'function', 'disable transition');
assert.equal(globalThis.document.activeElement, button);
assert.equal(button.disabled, false, 'DISABLING never uses native disabled');
assert.equal(button.getAttribute('aria-disabled'), 'true');
assert.equal(button.getAttribute('aria-busy'), 'true');
assert.equal(button.textContent, 'DISABLING');
await button.listeners.click();
assert.equal(calls.disable, 1, 'repeat activation is inert while disabling');
assert.equal(mgr.layers.get('focus-layer').visibilityIntentEpoch, 2);
releaseDisable();
await disablingClick;
assert.equal(globalThis.document.activeElement, button);
assert.equal(button.disabled, false);
assert.equal(button.getAttribute('aria-disabled'), 'false');
assert.equal(button.getAttribute('aria-busy'), 'false');
assert.equal(mgr.isEnabled('focus-layer'), false);
} finally {
releaseEnable?.();
releaseDisable?.();
await mgr.destroyAll();
if (originalDocument === undefined) delete globalThis.document;
else globalThis.document = originalDocument;
}
});
test('a failed Data Layer transition clears busy state without losing keyboard focus', async () => {
const originalDocument = globalThis.document;
const originalWarn = console.warn;
globalThis.document = { createElement: makeControlElement, activeElement: null };
console.warn = () => {};
const mgr = new DataLayerManager({});
let rejectEnable;
let enableCalls = 0;
mgr.register({
id: 'focus-failure-layer',
name: 'Focus failure layer',
icon: '',
source: 'Local focus fixture',
updateInterval: -1,
init() {},
enable() {
enableCalls += 1;
if (enableCalls > 1) return true;
return new Promise((resolve, reject) => { rejectEnable = reject; });
},
disable() { return true; },
update() {},
destroy() {},
getStats() { return { count: 0, lastUpdate: null }; },
});
const container = makeControlElement();
try {
mgr.buildTogglePanel(container);
const button = container
.querySelector('[data-layer-id="focus-failure-layer"]')
.querySelector('.data-toggle-btn');
button.focus();
const failedClick = button.listeners.click();
for (let attempt = 0; attempt < 20 && typeof rejectEnable !== 'function'; attempt += 1) {
await Promise.resolve();
}
assert.equal(typeof rejectEnable, 'function');
assert.equal(globalThis.document.activeElement, button);
assert.equal(button.getAttribute('aria-disabled'), 'true');
assert.equal(button.getAttribute('aria-busy'), 'true');
rejectEnable(new Error('QA enable rejected'));
await failedClick;
assert.equal(globalThis.document.activeElement, button, 'failure settlement preserves focus');
assert.equal(button.disabled, false);
assert.equal(button.getAttribute('aria-disabled'), 'false');
assert.equal(button.getAttribute('aria-busy'), 'false');
assert.equal(button.textContent, 'OFF');
assert.deepEqual(mgr.getLayerLifecycleState('focus-failure-layer'), {
enabled: false,
lifecycleState: 'disabled',
uncertain: false,
});
await button.listeners.click();
assert.equal(enableCalls, 2, 'the settled control accepts a later retry');
assert.equal(globalThis.document.activeElement, button);
assert.equal(mgr.isEnabled('focus-failure-layer'), true);
} finally {
rejectEnable?.(new Error('test cleanup'));
console.warn = originalWarn;
await mgr.destroyAll();
if (originalDocument === undefined) delete globalThis.document;
else globalThis.document = originalDocument;
}
});
/** Collect every node in a rendered subtree carrying `className`. */
function collectByClass(node, className) {
const found = [];