fix(shortcuts): a shortcut key held with Cmd, Ctrl or Alt runs no shortcut

The application shortcut listener read the key alone, so copying with
Cmd+C or Ctrl+C toggled the CCTV layer and opened the nearest camera,
Cmd+F opened the layers panel and Cmd+1 to 6 changed the visual style.
Chords are now left to the browser. Shift is not a chord, a held key
still repeats, and Escape dismisses search either way.
This commit is contained in:
Said Turksever committed 2026-11-13 01:12:58 +00:00
1 parent 10a788db78
commit 2c4d865ef9
3 files changed
+30 -6

No files matched your search

+5 -2
View File
@@ -2325,8 +2325,11 @@ Browser acceptance: `QA_BASE_URL=http://127.0.0.1:4173 node scripts/qa-segmented
`ui/input` supplies the bubbling application shortcut listener and generated
shader-parameter rows. Number/style keys, H/O/V/F/D/C actions, native form-control
typing and Escape behavior retain their existing mappings. Capture-phase
surfaces continue to arbitrate their own keyboard events first.
typing and Escape behavior retain their existing mappings. A shortcut key held
with Cmd, Ctrl or Alt runs no shortcut, so the browser's own chord (Cmd+C to
copy, Cmd+F to find, Cmd+1 to switch tab) does not also toggle CCTV, the layers
panel or a style; Shift does not count, and Escape dismisses search either way.
Capture-phase surfaces continue to arbitrate their own keyboard events first.
The UI facade retains shader values, share-restore authority, render requests,
search dismissal, visibility and Cockpit portal policy. Parameter rows preserve
+9 -1
View File
@@ -10,7 +10,8 @@ const STYLE_KEYS = Object.freeze({
/**
* Bind the application's existing bubbling keyboard shortcuts.
* Capture-phase surfaces keep first refusal. The caller owns each action and
* persistence; form controls keep native typing except for Escape.
* persistence; form controls keep native typing except for Escape, and a key
* held with Cmd, Ctrl or Alt is left to the browser.
* @param {object} options
* @param {Document} options.documentRef Keyboard event target.
* @param {HTMLElement} options.searchInput Additional editing target.
@@ -27,6 +28,13 @@ export function bindApplicationShortcuts({
event.target?.matches?.('select, input, textarea') ||
event.target === searchInput;
if (isFormControl && event.key !== 'Escape') return;
// Cmd+C, Ctrl+F, Cmd+1 and the like belong to the browser and the system.
// Shift is not a chord here: Shift+C is still C.
if (
(event.metaKey || event.ctrlKey || event.altKey) &&
event.key !== 'Escape'
)
return;
if (STYLE_KEYS[event.key]) actions.setStyle(STYLE_KEYS[event.key]);
if (event.key === 'Escape') actions.dismissSearch();
+16 -3
View File
@@ -117,10 +117,23 @@ test('the supplied search target retains editing even without form markup', () =
assert.deepEqual(f.calls, [['dismissSearch']]);
});
test('shortcut extraction does not change repeat or modifier policy', () => {
test('a held key still repeats its shortcut, and Shift is not a chord', () => {
const f = shortcuts();
f.press('h', new Element(), { repeat: true, ctrlKey: true });
assert.deepEqual(f.calls, [['toggleHud']]);
f.press('h', new Element(), { repeat: true });
f.press('C', new Element(), { shiftKey: true });
assert.deepEqual(f.calls, [['toggleHud'], ['toggleCctv']]);
});
test('browser and system chords on a shortcut key run no shortcut', () => {
const f = shortcuts();
for (const modifier of ['metaKey', 'ctrlKey', 'altKey'])
for (const key of ['1', '6', 'h', 'o', 'v', 'f', 'd', 'c', 'C'])
f.press(key, new Element(), { [modifier]: true });
// Copying (Cmd+C, Ctrl+C) used to turn CCTV on and open the nearest camera.
assert.deepEqual(f.calls, []);
f.press('c');
f.press('Escape', new Element(), { ctrlKey: true });
assert.deepEqual(f.calls, [['toggleCctv'], ['dismissSearch']]);
});
test('destroy synchronously removes shortcuts and a replacement binds once', () => {