feat: implement one-shot modifier keys for mobile terminal

- Redesign useSpecialKeys hook with modifier state tracking
- Add one-shot activation for Ctrl and Alt keys
- Visual feedback: active modifiers shown with yellow highlight
- Fix focusInput to use term.focus() instead of hidden input
- Always refocus terminal after sending any special key
- Add requestAnimationFrame for reliable focus restoration
This commit is contained in:
Fusion
2026-05-24 12:28:36 +02:00
parent 4f9aa7e3c2
commit 12378def4d
5 changed files with 121 additions and 25 deletions
+17 -11
View File
@@ -1,5 +1,5 @@
import React from "react";
import { useSpecialKeys, type SpecialKey } from "../hooks/use-special-keys";
import { useSpecialKeys, type SpecialKey, type ModifierKey } from "../hooks/use-special-keys";
interface SpecialKeysStripProps {
onSend: (data: string) => void;
@@ -8,11 +8,11 @@ interface SpecialKeysStripProps {
onKeepFocus?: () => void;
}
const PRIMARY_KEYS: { key: SpecialKey; label: string }[] = [
const PRIMARY_KEYS: { key: SpecialKey; label: string; isModifier?: boolean }[] = [
{ key: "escape", label: "Esc" },
{ key: "tab", label: "Tab" },
{ key: "ctrl", label: "Ctrl" },
{ key: "alt", label: "Alt" },
{ key: "ctrl", label: "Ctrl", isModifier: true },
{ key: "alt", label: "Alt", isModifier: true },
{ key: "up", label: "↑" },
{ key: "down", label: "↓" },
{ key: "left", label: "←" },
@@ -25,31 +25,38 @@ export const SpecialKeysStrip: React.FC<SpecialKeysStripProps> = ({
onMoreClick,
onKeepFocus,
}) => {
const { sendKey } = useSpecialKeys({ onSend });
const { sendKey, activeModifier } = useSpecialKeys({ onSend });
const handlePointerDown = (e: React.PointerEvent, key: SpecialKey) => {
e.preventDefault();
sendKey(key);
onKeepFocus?.();
// Always refocus terminal after sending
requestAnimationFrame(() => {
onKeepFocus?.();
});
};
const handleMorePointerDown = (e: React.PointerEvent) => {
e.preventDefault();
onMoreClick?.();
onKeepFocus?.();
requestAnimationFrame(() => {
onKeepFocus?.();
});
};
return (
<div className={`special-keys-strip ${isVisible ? "visible" : "hidden"}`}>
{PRIMARY_KEYS.map(({ key, label }) => (
{PRIMARY_KEYS.map(({ key, label, isModifier }) => (
<button
key={key}
className="special-key-button"
className={`special-key-button ${
isModifier && activeModifier === key ? "active-modifier" : ""
}`}
onPointerDown={(e) => handlePointerDown(e, key)}
onFocus={(e) => e.target.blur()}
type="button"
tabIndex={-1}
aria-label={`Send ${label}`}
aria-pressed={isModifier && activeModifier === key}
>
{label}
</button>
@@ -58,7 +65,6 @@ export const SpecialKeysStrip: React.FC<SpecialKeysStripProps> = ({
<button
className="special-key-button special-key-more"
onPointerDown={handleMorePointerDown}
onFocus={(e) => e.target.blur()}
type="button"
tabIndex={-1}
aria-label="More special keys"