fix(calls): hotkey rebind denylist; ignore modifiers; keep Space on buttons

- useKeyBind refuses Tab/Enter/arrows/Home/End/Page*/Escape and bare
  modifier codes, and refuses a code equal to the other call key, with an
  inline message (isBindableCallKey, unit-tested).
- PTT and deafen handlers ignore events with Ctrl/Alt/Meta held (deafen
  also Shift), so Cmd+M / Ctrl+M no longer toggle deafen.
- PTT only preventDefault()s when the target is not an interactive
  control, so Space still activates focused buttons during a call.

Fixes #23

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-12 14:48:34 -04:00
co-authored by Claude Opus 5
parent 6e4c4bc795
commit 2344c8273e
4 changed files with 129 additions and 9 deletions
+25 -1
View File
@@ -171,6 +171,10 @@ export function CallControls({ callEmbed }: CallControlsProps) {
const onKeyDown = (e: KeyboardEvent) => {
if (e.code !== pttKey || e.repeat) return;
// [Gitea #23] Ignore the PTT key with Ctrl/Alt/Meta held so it doesn't
// hijack OS/app chords (e.g. Cmd+Space) that happen to share the code.
// Shift is allowed through — Shift+Space is a harmless combo for PTT.
if (e.ctrlKey || e.altKey || e.metaKey) return;
const target = e.target as HTMLElement;
// BUG-7: use ownerDocument.body so isEditable works inside the EC iframe
const isEditable = (el: HTMLElement): boolean => {
@@ -185,7 +189,23 @@ export function CallControls({ callEmbed }: CallControlsProps) {
return false;
};
if (isEditable(target)) return;
e.preventDefault();
// [Gitea #23] Don't swallow Space on a focused button/link/etc — PTT still
// engages the mic, but the key's default action (activating the control)
// is left alone so keyboard users can still Tab+Space the call buttons.
const isInteractive = (el: HTMLElement): boolean => {
const tag = el.tagName;
if (tag === 'BUTTON' || tag === 'A' || tag === 'SELECT') return true;
let node: HTMLElement | null = el;
while (node && node !== el.ownerDocument.body) {
const role = node.getAttribute('role');
if (role === 'button' || role === 'link' || role === 'menuitem' || role === 'tab') {
return true;
}
node = node.parentElement;
}
return false;
};
if (!isInteractive(target)) e.preventDefault();
// C-M5: mark PTT active BEFORE unmuting so the mic echo (onMediaState)
// doesn't treat this transient unmute as a user-initiated undeafen.
callEmbed.control.pttActive = true;
@@ -256,6 +276,10 @@ export function CallControls({ callEmbed }: CallControlsProps) {
const onKeyDown = (e: KeyboardEvent) => {
if (e.code !== deafenKey) return;
if (e.repeat) return;
// [Gitea #23] Ignore the deafen key with any modifier held — with the
// default 'KeyM', Ctrl+M / Alt+M / Cmd+M are common OS/app chords that
// shouldn't also toggle deafen (and previously got preventDefault()ed).
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
if (isEditable(e.target as HTMLElement)) return;
e.preventDefault();
callEmbed.control.toggleSound();