fix(studio): use e.key for playback shortcuts so non-QWERTY layouts work

The 7 letter shortcuts (J/K/L/I/O/A/E) in usePlaybackKeyboard were gated
on `e.code === "Key*"`, which is the physical key position on a US-QWERTY
layout. On AZERTY (and other layouts) the physical "KeyA" slot produces
e.key="q", so "Jump to in-point" and the rest of the letter shortcuts
either fired on the wrong character or not at all.

Switch the 7 letter shortcuts to compare `e.key.toLowerCase()` and rename
`pressedCodesRef` → `pressedKeysRef` so the K-hold combo (K+J / K+L for
frame stepping) is also keyed off the typed character. `Space` and
`Arrow*` keep using `e.code` since those codes are layout-independent.

Adds a happy-dom test covering QWERTY happy path, AZERTY (physical KeyQ
produces e.key="a" → in-point seek fires), AZERTY contrapositive (physical
KeyA producing e.key="q" no longer triggers in-point), Shift+I clears
in-point, K-hold combo for frame stepping, K release returning the set
to clean state, and Space passthrough.

Addresses bug #3 in #834. Bugs #1 (loop at out-point) and #2 (Jump to
in-point forcing pause) live outside this hook (player loop and adapter
`seek` respectively) and are left for follow-up PRs.
This commit is contained in:
Carlos Alcaraz
2026-05-14 13:48:55 -03:00
parent e59089bf75
commit eac8808425
2 changed files with 187 additions and 12 deletions
@@ -36,7 +36,7 @@ export function usePlaybackKeyboard({
pause,
seek,
}: UsePlaybackKeyboardParams) {
const pressedCodesRef = useRef(new Set<string>());
const pressedKeysRef = useRef(new Set<string>());
const playbackKeyDownRef = useRef<(e: KeyboardEvent) => void>(() => {});
const playbackKeyUpRef = useRef<(e: KeyboardEvent) => void>(() => {});
@@ -90,7 +90,8 @@ export function usePlaybackKeyboard({
) {
return;
}
pressedCodesRef.current.add(e.code);
const key = e.key.toLowerCase();
pressedKeysRef.current.add(key);
if (e.code === "Space") {
e.preventDefault();
togglePlay();
@@ -107,47 +108,47 @@ export function usePlaybackKeyboard({
return;
}
if (e.repeat) return;
if (e.code === "KeyK") {
if (key === "k") {
e.preventDefault();
pause();
return;
}
if (e.code === "KeyJ") {
if (key === "j") {
e.preventDefault();
if (pressedCodesRef.current.has("KeyK")) {
if (pressedKeysRef.current.has("k")) {
stepFrames(-1);
return;
}
shuttle("backward");
return;
}
if (e.code === "KeyL") {
if (key === "l") {
e.preventDefault();
if (pressedCodesRef.current.has("KeyK")) {
if (pressedKeysRef.current.has("k")) {
stepFrames(1);
return;
}
shuttle("forward");
return;
}
if (e.code === "KeyI") {
if (key === "i") {
e.preventDefault();
const t = getAdapter()?.getTime() ?? usePlayerStore.getState().currentTime;
usePlayerStore.getState().setInPoint(e.shiftKey ? null : t);
return;
}
if (e.code === "KeyO") {
if (key === "o") {
e.preventDefault();
const t = getAdapter()?.getTime() ?? usePlayerStore.getState().currentTime;
usePlayerStore.getState().setOutPoint(e.shiftKey ? null : t);
return;
}
if (e.code === "KeyA") {
if (key === "a") {
e.preventDefault();
seek(usePlayerStore.getState().inPoint ?? 0);
return;
}
if (e.code === "KeyE") {
if (key === "e") {
e.preventDefault();
const { outPoint } = usePlayerStore.getState();
seek(outPoint ?? getAdapter()?.getDuration() ?? usePlayerStore.getState().duration);
@@ -158,7 +159,7 @@ export function usePlaybackKeyboard({
);
const handlePlaybackKeyUp = useCallback((e: KeyboardEvent) => {
pressedCodesRef.current.delete(e.code);
pressedKeysRef.current.delete(e.key.toLowerCase());
}, []);
playbackKeyDownRef.current = handlePlaybackKeyDown;