mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
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)
53 lines
2.1 KiB
TypeScript
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;
|
|
}
|