Enhance accessibility and keyboard navigation for video controls and overlays
This commit is contained in:
		@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user