Enhance accessibility and keyboard navigation for video controls and overlays

This commit is contained in:
Sebastiaan de Schaetzen 2025-06-17 10:36:39 +02:00
parent e0297afe0f
commit 3d603e1b68

View File

@ -151,6 +151,17 @@
cursor: pointer; cursor: pointer;
accent-color: #3498db; /* Optional: color for the seek bar thumb and progress */ accent-color: #3498db; /* Optional: color for the seek bar thumb and progress */
} }
/* Styles for TV remote focus indication */
.video-thumbnail:focus,
#back-button:focus {
outline: 3px solid #3498db !important; /* A clear blue outline */
box-shadow: 0 0 15px #3498db; /* A glow effect */
}
.nofocus:focus {
outline: none !important; /* Remove focus outline for elements with nofocus class */
}
</style> </style>
<script> <script>
function hideLoader() { function hideLoader() {
@ -166,6 +177,7 @@
let customControls = null; let customControls = null;
let playPauseButton = null; let playPauseButton = null;
let seekBar = null; let seekBar = null;
let videoPlaying = false;
function exitVideoPlayback() { function exitVideoPlayback() {
if (videoElement && videoElement.pause) { if (videoElement && videoElement.pause) {
@ -178,14 +190,14 @@
overlayContainer.hide(); // Hide instead of remove overlayContainer.hide(); // Hide instead of remove
} }
$(document).off('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozfullscreenchange.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange); $(document).off('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozFullScreen.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange);
if (videoElement) { if (videoElement) {
$(videoElement).off('.currentVideo'); // Remove all namespaced video events $(videoElement).off('.currentVideo'); // Remove all namespaced video events
videoElement.src = ""; // Clear the source videoElement.src = ""; // Clear the source
videoElement.load(); // Important to ensure the video unloads videoElement.load(); // Important to ensure the video unloads
} }
if (seekBar) { if (seekBar) { // seekBar is a jQuery object
seekBar.val(0); seekBar.val(0);
seekBar.css('background', ''); // Reset seek bar style seekBar.css('background', ''); // Reset seek bar style
} }
@ -203,6 +215,12 @@
document.msExitFullscreen(); document.msExitFullscreen();
} }
} }
// FOCUS: Return focus to the main view's play button
if ($(".video-container").is(":visible") && $('#play-button').length) {
$('#play-button').focus();
}
videoPlaying = false;
} }
function showControlsAndResetTimer() { function showControlsAndResetTimer() {
@ -284,6 +302,10 @@
currentVideo = data.currentVideo; currentVideo = data.currentVideo;
$("#current-thumb").attr("src", currentVideo.thumbnail); $("#current-thumb").attr("src", currentVideo.thumbnail);
hideLoader(); hideLoader();
// FOCUS: Set initial focus on the main screen
if ($(".video-container").is(":visible") && $('#play-button').length) {
$('#play-button').focus();
}
}) })
// Attach event listeners that can be set up once for static controls // Attach event listeners that can be set up once for static controls
@ -316,8 +338,13 @@
} else if (containerEl.msRequestFullscreen) { } else if (containerEl.msRequestFullscreen) {
containerEl.msRequestFullscreen(); containerEl.msRequestFullscreen();
} }
// FOCUS: Set focus in the video overlay
if (playPauseButton && playPauseButton.length) {
playPauseButton.focus();
}
videoElement.play().catch(err => console.error("Error attempting to play video:", err)); videoElement.play().catch(err => console.error("Error attempting to play video:", err));
videoPlaying = true;
} }
}); });
@ -329,29 +356,135 @@
setInterval(handleVideoTimeUpdate, 1000/30); setInterval(handleVideoTimeUpdate, 1000/30);
// --- Setup Fullscreen Change Listener (session-specific) --- // --- Setup Fullscreen Change Listener (session-specific) ---
$(document).off('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozfullscreenchange.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange); $(document).off('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozFullScreen.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange);
$(document).on('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozfullscreenchange.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange); $(document).on('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozFullScreen.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange);
overlayContainer.on('mousemove.inactivityControls', showControlsAndResetTimer); overlayContainer.on('mousemove.inactivityControls', showControlsAndResetTimer);
// --- TV Remote/Keyboard Navigation ---
const mainViewFocusable = [$('#prev-thumb'), $('#play-button'), $('#next-thumb')].filter(el => el && el.length > 0);
const getOverlayViewFocusableElements = () => [backButton, playPauseButton, seekBar].filter(el => el && el.length > 0 && el.is(':visible'));
$(document).on('keydown', function(e) {
let isOverlayVisible = overlayContainer.is(':visible');
let activeFocusableSet;
if (isOverlayVisible) {
activeFocusableSet = getOverlayViewFocusableElements();
} else {
activeFocusableSet = mainViewFocusable;
}
if (!activeFocusableSet || activeFocusableSet.length === 0) return;
const focusedElement = $(document.activeElement);
let currentIndex = -1;
for (let i = 0; i < activeFocusableSet.length; i++) {
if (focusedElement.is(activeFocusableSet[i])) {
currentIndex = i;
break;
}
}
// Global key handling for overlay
if (isOverlayVisible) {
if (e.key === 'Backspace' || e.keyCode === 8) { // Backspace
e.preventDefault();
if (backButton && backButton.length) backButton.click();
return;
}
if (e.keyCode === 179) { // MediaPlayPause
e.preventDefault();
if (playPauseButton && playPauseButton.length) playPauseButton.click();
return;
}
if (e.keyCode === 178) { // MediaStop
e.preventDefault();
exitVideoPlayback();
return;
}
}
// If focus is not on a managed element, and not body/html, usually let it be.
// However, for certain keys like Enter, we might want a default action.
if (currentIndex === -1 && !focusedElement.is('body') && !focusedElement.is('html')) {
return;
}
let keyHandled = false;
switch (e.key || e.keyCode) {
case 'ArrowLeft':
case 37: // ArrowLeft
if (videoPlaying) {
videoElement.currentTime = Math.max(0, videoElement.currentTime - 5); // Seek back 5s
handleVideoTimeUpdate();
showControlsAndResetTimer();
} else if (currentIndex !== -1) {
let nextIndex = (currentIndex - 1 + activeFocusableSet.length) % activeFocusableSet.length;
activeFocusableSet[nextIndex].focus();
} else if (activeFocusableSet.length > 0) { // If nothing specific focused, focus last
activeFocusableSet[activeFocusableSet.length - 1].focus();
}
keyHandled = true;
break;
case 'ArrowRight':
case 39: // ArrowRight
if (videoPlaying) {
videoElement.currentTime = Math.min(videoElement.duration, videoElement.currentTime + 5); // Seek forward 5s
handleVideoTimeUpdate();
showControlsAndResetTimer();
} else if (currentIndex !== -1) {
let nextIndex = (currentIndex + 1) % activeFocusableSet.length;
activeFocusableSet[nextIndex].focus();
} else if (activeFocusableSet.length > 0) { // If nothing specific focused, focus first
activeFocusableSet[0].focus();
}
keyHandled = true;
break;
case 'Enter':
case 13: // Enter
if (videoPlaying) {
$('#play-button').click();
} else if (currentIndex !== -1) {
activeFocusableSet[currentIndex].click();
}
keyHandled = true;
break;
case 179: // MediaPlayPause (if not handled by global overlay section)
if (!isOverlayVisible && $('#play-button').length) {
$('#play-button').click();
keyHandled = true;
}
break;
}
if (keyHandled) {
e.preventDefault();
}
});
}) })
</script> </script>
</head> </head>
<body> <body>
<div class="loader" id="loader"></div> <div class="loader" id="loader"></div>
<div class="video-container"> <div class="video-container">
<img id="prev-thumb" class="video-thumbnail side-video" alt="Previous Video"> <img id="prev-thumb" class="video-thumbnail side-video" alt="Previous Video" tabindex="0">
<img id="current-thumb" class="video-thumbnail current-video" alt="Current Video"> <img id="current-thumb" class="video-thumbnail current-video" alt="Current Video" tabindex="0">
<img id="next-thumb" class="video-thumbnail side-video" alt="Next Video"> <img id="next-thumb" class="video-thumbnail side-video" alt="Next Video" tabindex="0">
<div class="play-button" id="play-button"></div> <div class="play-button" id="play-button" tabindex="0" role="button" aria-label="Play video"></div>
</div> </div>
<!-- STATIC VIDEO OVERLAY STRUCTURE --> <!-- STATIC VIDEO OVERLAY STRUCTURE -->
<div id="video-overlay-container"> <div id="video-overlay-container">
<video class="video-player"></video> <!-- no src, no autoplay, no controls initially --> <video class="video-player"></video> <!-- no src, no autoplay, no controls initially -->
<button id="back-button">Back</button> <button id="back-button" tabindex="0">Back</button>
<div id="custom-video-controls"> <div id="custom-video-controls">
<button id="custom-play-pause-button"></button> <!-- Default to Play icon --> <button id="custom-play-pause-button" class="nofocus" tabindex="0"></button> <!-- Default to Play icon -->
<input type="range" id="custom-seek-bar" value="0" step="any" /> <input type="range" id="custom-seek-bar" class="nofocus" value="0" step="any" tabindex="0" aria-label="Video seek bar" />
</div> </div>
</div> </div>
</body> </body>