fix(studio): timeline seekbar focus blocks NLE keyboard shortcuts (#1137)

* fix(studio): blur seekbar after seek so NLE shortcuts resume

Clicking the timeline seekbar (role=slider) explicitly called
e.currentTarget.focus(), leaving focus on the slider element.
shouldIgnorePlaybackShortcutTarget filters out [role='slider'] targets,
so all playback shortcuts (Space/J/K/L/arrows) were silently blocked
until the user clicked away.

- blur() the seekbar in cleanup() so focus returns after pointer release
- replace the default white focus ring with a focus-visible ring (keyboard-only)
- add tabIndex={-1} + outline-none to the NLE timeline scroll div,
  which Chrome auto-focuses for overflow:auto elements

Fixes #1136

* fix(studio): blur color slider on pointer release (sister bug)

Same pattern as the seekbar: role=slider + tabIndex=0 receives natural
browser focus on click, blocking playback shortcuts while focused.

ColorSlider never had an onPointerUp handler; adding one to blur
immediately after release matches the seekbar's cleanup() blur.
This commit is contained in:
Miguel Ángel
2026-05-30 13:25:43 -04:00
committed by GitHub
parent 100d355e71
commit 194ad6f6d3
4 changed files with 7 additions and 2 deletions
@@ -80,6 +80,9 @@ function ColorSlider({
event.currentTarget.setPointerCapture(event.pointerId);
commitFromClientX(event.clientX);
}}
onPointerUp={(event) => {
event.currentTarget.blur();
}}
onPointerMove={(event) => {
if (disabled || event.buttons !== 1) return;
commitFromClientX(event.clientX);
@@ -282,7 +282,7 @@ const SeekBar = memo(function SeekBar({
aria-valuemin={0}
aria-valuemax={Math.round(duration)}
aria-valuenow={0}
className={`min-w-[96px] flex-1 h-6 flex items-center group ${
className={`min-w-[96px] flex-1 h-6 flex items-center group outline-none focus-visible:ring-1 focus-visible:ring-white/30 focus-visible:rounded ${
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"
}`}
style={{ touchAction: "none" }}
@@ -428,7 +428,8 @@ export const Timeline = memo(function Timeline({
>
<div
ref={scrollRef}
className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full`}
tabIndex={-1}
className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-none`}
onDragOver={handleAssetDragOver}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleAssetDrop}
@@ -110,6 +110,7 @@ export function useSeekBarDrag(
window.removeEventListener("pointercancel", onUp);
document.removeEventListener("visibilitychange", onVisibilityChange);
window.removeEventListener("blur", cleanup);
target.blur();
};
const onUp = (ev: PointerEvent) => {
if (ev.pointerId !== pointerId) return;