Проблема в том, что LoginScreen, Menu и Settings all должны обрабатывать клавиши со стрелками на пульте телевизора (keyCode 38, 40 и т. д.).
Мой текущий подход (см. экран входа в App.js) заключается в том, что каждый компонент добавляет свое собственное глобальное окно.addEventListener("keydown", ...) внутри useEffect и удаляет его при очистке.
import React, { useEffect, useRef, useState } from "react";
import Menu from "./Menu";
import MemorySimulator from "./MemorySimulator";
import LinkVerification from "./LinkVerification";
// User credentials
const USERS = {
admin: { password: "1234", role: "admin" },
user: { password: "5678", role: "user" }
};
export default function App() {
const [currentScreen, setCurrentScreen] = useState("login"); // login, menu, memorySimulator, linkVerification
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [userRole, setUserRole] = useState(null);
const [error, setError] = useState("");
const [focusIndex, setFocusIndex] = useState(0); // 0=username,1=password,2=login
const usernameRef = useRef(null);
const passwordRef = useRef(null);
const loginButtonRef = useRef(null);
// Move DOM focus when focusIndex changes (visual + keyboard)
useEffect(() => {
if (currentScreen !== "login") return;
const refs = [usernameRef, passwordRef, loginButtonRef];
if (refs[focusIndex]?.current) refs[focusIndex].current.focus();
}, [focusIndex, currentScreen]);
// Remote key handling (arrow up/down + enter)
useEffect(() => {
if (currentScreen !== "login") return;
const handler = (e) => {
switch (e.keyCode) {
case 38: // Up
setFocusIndex((i) => Math.max(0, i - 1));
break;
case 40: // Down
setFocusIndex((i) => Math.min(2, i + 1));
break;
case 13: // Enter
if (focusIndex === 0) {
usernameRef.current?.focus();
usernameRef.current?.click();
} else if (focusIndex === 1) {
passwordRef.current?.focus();
passwordRef.current?.click();
} else if (focusIndex === 2) {
handleLogin();
}
break;
default:
break;
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [focusIndex, username, password, currentScreen]);
const handleLogin = (e) => {
if (e && e.preventDefault) e.preventDefault();
// Check if user exists
const user = USERS[username];
if (user && user.password === password) {
setError("");
setUserRole(user.role);
setCurrentScreen("menu");
} else {
setError("
}
};
const handleLogout = () => {
setCurrentScreen("login");
setUsername("");
setPassword("");
setUserRole(null);
setFocusIndex(0);
setError("");
};
const handleSelectFeature = (feature) => {
if (feature === 'memorySimulator') {
setCurrentScreen('memorySimulator');
} else if (feature === 'linkVerification') {
setCurrentScreen('linkVerification');
}
};
const handleBackToMenu = () => {
setCurrentScreen('menu');
};
// Render current screen
if (currentScreen === "menu") {
return ;
}
if (currentScreen === "memorySimulator") {
return ;
}
if (currentScreen === "linkVerification") {
return ;
}
// Login screen
return (
display: "flex",
height: "100vh",
justifyContent: "center",
alignItems: "center",
background: "#f4f4f4",
fontFamily: "sans-serif"
}}>
setUsername(e.target.value)}
onFocus={() => setFocusIndex(0)}
autoComplete="off"
style={{
width: 240,
padding: 10,
marginBottom: 10,
borderRadius: 6,
border: focusIndex === 0 ? "3px solid #007bff" : "1px solid #ccc",
backgroundColor: focusIndex === 0 ? "#e3f2fd" : "white",
outline: "none"
}}
/>
setPassword(e.target.value)}
onFocus={() => setFocusIndex(1)}
autoComplete="off"
style={{
width: 240,
padding: 10,
marginBottom: 12,
borderRadius: 6,
border: focusIndex === 1 ? "3px solid #007bff" : "1px solid #ccc",
backgroundColor: focusIndex === 1 ? "#e3f2fd" : "white",
outline: "none"
}}
/>
setFocusIndex(2)}
style={{
width: 264,
padding: 12,
borderRadius: 6,
background: focusIndex === 2 ? "#0056b3" : "#007bff",
color: "white",
border: focusIndex === 2 ? "3px solid #003d82" : "none",
fontSize: 16,
cursor: "pointer",
fontWeight: "bold"
}}
>
{focusIndex === 2 ? "► Login" : "Login"}
{error &&
{error}
}
TV Remote Controls:
↑ ↓ to navigate | ENTER to select/focus
Current: {focusIndex === 0 ? "Username" : focusIndex === 1 ? "Password" : "Login Button"}
Test Accounts:
);
}
import React, { useEffect, useRef, useState } from "react";
const LinkVerification = ({ onBack, userRole }) => {
const [showRealImage, setShowRealImage] = useState(false);
const [showDeniedMessage, setShowDeniedMessage] = useState(false);
const [focusIndex, setFocusIndex] = useState(0); // 0=image, 1=back button
const imageRef = useRef(null);
const backButtonRef = useRef(null);
// Move DOM focus when focusIndex changes
useEffect(() => {
const refs = [imageRef, backButtonRef];
if (refs[focusIndex]?.current) {
refs[focusIndex].current.focus();
}
}, [focusIndex]);
// Remote key handling
useEffect(() => {
const handler = (e) => {
console.log("LinkVerification key:", e.keyCode);
switch (e.keyCode) {
case 38: // Up arrow
e.preventDefault();
setFocusIndex((i) => Math.max(0, i - 1));
break;
case 40: // Down arrow
e.preventDefault();
setFocusIndex((i) => Math.min(1, i + 1));
break;
case 13: // Enter key
e.preventDefault();
if (focusIndex === 0) {
handleImageClick();
} else if (focusIndex === 1) {
onBack();
}
break;
case 10009: // Return/Back button on Samsung TV remote
e.preventDefault();
onBack();
break;
default:
break;
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [focusIndex, onBack]);
const handleImageClick = () => {
if (userRole === 'admin') {
// Admin can view real image
setShowRealImage(true);
setShowDeniedMessage(false);
} else {
// Regular user gets permission denied
setShowDeniedMessage(true);
setTimeout(() => setShowDeniedMessage(false), 3000);
}
};
const handleCloseRealImage = () => {
setShowRealImage(false);
};
return (
display: "flex",
minHeight: "100vh",
justifyContent: "center",
alignItems: "center",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
fontFamily: "sans-serif",
padding: 20
}}>
{userRole === 'admin'
? "Click the thumbnail to view the full image"
: "Click to verify (access restricted)"}
{/* Thumbnail Image */}
setFocusIndex(0)}
onClick={handleImageClick}
tabIndex={0}
style={{
marginBottom: 20,
cursor: "pointer",
border: focusIndex === 0 ? "4px solid #667eea" : "2px solid #ddd",
borderRadius: 8,
overflow: "hidden",
outline: "none",
transition: "all 0.2s",
backgroundColor: focusIndex === 0 ? "#e3f2fd" : "transparent"
}}
>
alt="Thumbnail"
style={{
width: "100%",
height: "auto",
display: "block"
}}
onError={(e) => {
// Fallback if image not found
e.target.style.display = 'none';
e.target.parentElement.innerHTML = `
Place thumbnail.jpg in public folder
`;
}}
/>
{/* Permission Denied Message */}
{showDeniedMessage && (
padding: 15,
marginBottom: 20,
background: "#fee",
border: "2px solid #fcc",
borderRadius: 8,
color: "#c00",
fontWeight: "bold"
}}>
)}
{/* Role Badge */}
display: "inline-block",
padding: "6px 12px",
marginBottom: 15,
background: userRole === 'admin' ? "#d4edda" : "#fff3cd",
border: userRole === 'admin' ? "1px solid #c3e6cb" : "1px solid #ffeeba",
borderRadius: 6,
color: userRole === 'admin' ? "#155724" : "#856404",
fontSize: 13,
fontWeight: "bold"
}}>
{userRole === 'admin' ? '
{/* Back Button */}
setFocusIndex(1)}
onClick={onBack}
style={{
width: "100%",
padding: 12,
borderRadius: 8,
background: focusIndex === 1 ? "#495057" : "#6c757d",
color: "white",
border: focusIndex === 1 ? "3px solid #212529" : "none",
fontSize: 16,
cursor: "pointer",
fontWeight: "bold"
}}
>
{focusIndex === 1 ? "► " : ""}← Back to Menu
{/* TV Remote Helper */}
marginTop: 20,
padding: 10,
background: "#f8f9fa",
borderRadius: 6,
fontSize: 13,
color: "#666"
}}>
TV Remote: ↑↓ to navigate | ENTER to select
Current: {focusIndex === 0 ? "Thumbnail Image" : "Back Button"}
{/* Full Image Modal (Admin Only) */}
{showRealImage && (
onClick={handleCloseRealImage}
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.9)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 1000,
cursor: "pointer"
}}
>

alt="Real Image"
style={{
maxWidth: "100%",
maxHeight: "90vh",
borderRadius: 8,
boxShadow: "0 0 50px rgba(255,255,255,0.3)"
}}
onError={(e) => {
// Fallback if image not found
e.target.style.display = 'none';
e.target.parentElement.innerHTML = `
Real Image
Place real-image.jpg in public folder
Click anywhere to close
`;
}}
/>
position: "absolute",
top: -40,
right: 0,
color: "white",
fontSize: 14,
background: "rgba(0,0,0,0.5)",
padding: "8px 12px",
borderRadius: 6
}}>
Click anywhere to close
)}
);
};
export default LinkVerification;
import React, { useEffect, useRef, useState } from "react";
const Menu = ({ onSelectFeature, onLogout, userRole }) => {
const [focusIndex, setFocusIndex] = useState(0); // 0=Link Verification, 1=Memory Simulator, 2=Logout
const linkVerificationRef = useRef(null);
const memorySimulatorRef = useRef(null);
const logoutRef = useRef(null);
// Move DOM focus when focusIndex changes
useEffect(() => {
const refs = [linkVerificationRef, memorySimulatorRef, logoutRef];
if (refs[focusIndex]?.current) {
refs[focusIndex].current.focus();
}
}, [focusIndex]);
// Remote key handling (arrow up/down + enter)
useEffect(() => {
const handler = (e) => {
console.log("Menu key:", e.keyCode);
switch (e.keyCode) {
case 38: // Up arrow
e.preventDefault();
setFocusIndex((i) => Math.max(0, i - 1));
break;
case 40: // Down arrow
e.preventDefault();
setFocusIndex((i) => Math.min(2, i + 1));
break;
case 13: // Enter key
e.preventDefault();
if (focusIndex === 0) {
onSelectFeature('linkVerification');
} else if (focusIndex === 1) {
onSelectFeature('memorySimulator');
} else if (focusIndex === 2) {
onLogout();
}
break;
case 10009: // Return/Back button on Samsung TV remote
e.preventDefault();
onLogout();
break;
default:
break;
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [focusIndex, onSelectFeature, onLogout]);
return (
display: "flex",
height: "100vh",
justifyContent: "center",
alignItems: "center",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
fontFamily: "sans-serif"
}}>
margin: "0 0 30px 0",
color: "#666",
fontSize: 14
}}>
Welcome, {userRole === 'admin' ? 'Admin' : 'User'}! Select a feature:
{/* Link Verification Button */}
setFocusIndex(0)}
onClick={() => onSelectFeature('linkVerification')}
style={{
width: "100%",
padding: 16,
marginBottom: 15,
borderRadius: 8,
background: focusIndex === 0 ? "#5a67d8" : "#667eea",
color: "white",
border: focusIndex === 0 ? "3px solid #4c51bf" : "none",
fontSize: 18,
cursor: "pointer",
fontWeight: "bold",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s"
}}
>
{focusIndex === 0 ? "► " : ""}
{/* Memory Simulator Button */}
setFocusIndex(1)}
onClick={() => onSelectFeature('memorySimulator')}
style={{
width: "100%",
padding: 16,
marginBottom: 15,
borderRadius: 8,
background: focusIndex === 1 ? "#38a169" : "#48bb78",
color: "white",
border: focusIndex === 1 ? "3px solid #2f855a" : "none",
fontSize: 18,
cursor: "pointer",
fontWeight: "bold",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s"
}}
>
{focusIndex === 1 ? "► " : ""}
{/* Logout Button */}
setFocusIndex(2)}
onClick={onLogout}
style={{
width: "100%",
padding: 12,
borderRadius: 8,
background: focusIndex === 2 ? "#495057" : "#6c757d",
color: "white",
border: focusIndex === 2 ? "3px solid #212529" : "none",
fontSize: 16,
cursor: "pointer",
fontWeight: "bold",
transition: "all 0.2s"
}}
>
{focusIndex === 2 ? "► " : ""}← Logout
{/* TV Remote Helper */}
TV Remote: ↑↓ to navigate | ENTER to select
Current: {
focusIndex === 0 ? "Link Verification" :
focusIndex === 1 ? "Memory Simulator" :
"Logout"
}
);
};
export default Menu;
Подробнее здесь: https://stackoverflow.com/questions/798 ... h-multiple
Мобильная версия