Files
iptvnator/libs/ui/playback/src/lib/player-controls/pointer-focus-release.ts
T
4grayandClaude Opus 4.8 fe3c86394c fix(playback): keep Video.js vendor-chrome shortcuts after a mouse click on a control (#1523)
Follow-up to #1516 for the vendor-chrome path (shared controls opted out). With
Video.js's own controls, Chromium leaves a clicked control-bar button focused,
and a focused Video.js component captures the keyboard entirely, so after
clicking fullscreen Space left fullscreen instead of pausing and the seek,
volume and mute keys did nothing until the user clicked the video. ArtPlayer
and the native HTML5 controls were verified unaffected.

The legacy Video.js chrome now releases the focus a pointer interaction leaves
on a control (vjs-pointer-focus-release.ts). The release is scoped to the
.vjs-control-bar and pointer-attributed, and runs on both focusin (focus
landing on a control, e.g. a menu handing focus to its button) and click (a
control clicked while already focused, which fires no focusin); keyboard Tab
focus and modal-dialog focus traps are preserved. The eligibility helper is
shared with ControlsSurface via pointer-focus-release.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-09-04 16:36:29 +02:00

53 lines
2.1 KiB
TypeScript

/**
* Controls whose pointer-originated focus is released once the click that
* produced it completes. Text entry is deliberately absent: a click into a
* field must not end typing. Neither the shared controls bar nor the vendor
* chrome holds one today.
*/
export const POINTER_FOCUS_RELEASE_SELECTOR =
'button, input[type="range"], [role="slider"]';
export interface BlurFocusedControlOptions {
/** Controls eligible for release; defaults to the shared selector. */
selector?: string;
}
/**
* What a click's own `pointerType` says about its origin: `true` for a
* pointer (mouse, touch, pen); `false` for keyboard activation or script,
* since current engines dispatch Enter/Space on a focused button and
* `element.click()` as a PointerEvent with an empty `pointerType`; `null`
* for a legacy MouseEvent click that carries no pointer type at all, which
* the caller must attribute some other way or leave alone.
*/
export function clickPointerOrigin(event: MouseEvent): boolean | null {
const pointerType = (event as Partial<PointerEvent>).pointerType;
return typeof pointerType === 'string' ? pointerType !== '' : null;
}
/**
* Blur the focused control inside `root` when it is eligible. Chromium
* focuses a clicked `<button>`, and a focused control captures the keyboard:
* Space and Enter activate it again and the playback shortcuts yield to it.
* Focus a pointer click left behind was never the keyboard's, so it is
* dropped once the click completes; Chromium keeps its sequential focus
* navigation starting point at the blurred control, so a later Tab
* continues from it exactly as if it were still focused. Returns whether
* focus was released.
*/
export function blurFocusedControl(
root: HTMLElement,
options: BlurFocusedControlOptions = {}
): boolean {
const active = root.ownerDocument.activeElement;
if (
!(active instanceof HTMLElement) ||
!root.contains(active) ||
!active.matches(options.selector ?? POINTER_FOCUS_RELEASE_SELECTOR)
) {
return false;
}
active.blur();
return true;
}