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
+19 -6
View File
@@ -39,7 +39,7 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
onClose,
onKeepFocus,
}) => {
const { sendKey } = useSpecialKeys({ onSend });
const { sendKey, clearModifier } = useSpecialKeys({ onSend });
if (!isOpen) return null;
@@ -47,14 +47,29 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
e.preventDefault();
sendKey(key);
onClose();
onKeepFocus?.();
// Always refocus terminal after sending
requestAnimationFrame(() => {
onKeepFocus?.();
});
};
const handleOverlayPointerDown = (e: React.PointerEvent) => {
e.preventDefault();
clearModifier();
onClose();
requestAnimationFrame(() => {
onKeepFocus?.();
});
};
return (
<div className="special-keys-panel-overlay" onClick={onClose}>
<div
className="special-keys-panel-overlay"
onPointerDown={handleOverlayPointerDown}
>
<div
className="special-keys-panel"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<div className="special-keys-panel-section">
{EXPANDED_KEYS.map(({ key, label }) => (
@@ -62,7 +77,6 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
key={key}
className="special-key-button"
onPointerDown={(e) => handlePointerDown(e, key)}
onFocus={(e) => e.target.blur()}
type="button"
tabIndex={-1}
>
@@ -77,7 +91,6 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
key={key}
className="special-key-button"
onPointerDown={(e) => handlePointerDown(e, key)}
onFocus={(e) => e.target.blur()}
type="button"
tabIndex={-1}
>
+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"
+2 -2
View File
@@ -198,7 +198,7 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
}
};
const focusInput = () => {
hiddenInputRef.current?.focus();
termRef.current?.focus();
};
onTerminalReadyRef.current(sendData, status, focusInput);
}
@@ -230,7 +230,7 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
}
};
const focusInput = () => {
hiddenInputRef.current?.focus();
termRef.current?.focus();
};
onTerminalReady(sendData, status, focusInput);
}