fix: keep virtual keyboard open when tapping special keys

- Use onPointerDown with preventDefault() instead of onClick
- Add onKeepFocus callback to SpecialKeysStrip and SpecialKeysPanel
- Expose focusInput via onTerminalReady in TerminalComponent
- MobileTerminalWrapper passes focus callback to keep keyboard open
This commit is contained in:
Fusion
2026-05-24 11:57:55 +02:00
parent aea1ff95f6
commit 7e0df57f8c
4 changed files with 42 additions and 15 deletions
@@ -29,6 +29,7 @@ export const MobileTerminalWrapper: React.FC<MobileTerminalWrapperProps> = ({
const [terminalRef, setTerminalRef] = useState<{ const [terminalRef, setTerminalRef] = useState<{
sendData: (data: string) => void; sendData: (data: string) => void;
connectionStatus: "connecting" | "connected" | "disconnected" | "error"; connectionStatus: "connecting" | "connected" | "disconnected" | "error";
focusInput: () => void;
} | null>(null); } | null>(null);
const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile }); const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile });
@@ -40,8 +41,8 @@ export const MobileTerminalWrapper: React.FC<MobileTerminalWrapperProps> = ({
}, [headerAutoHide, keysAutoHide]); }, [headerAutoHide, keysAutoHide]);
const handleTerminalReady = useCallback( const handleTerminalReady = useCallback(
(sendData: (data: string) => void, connectionStatus: "connecting" | "connected" | "disconnected" | "error") => { (sendData: (data: string) => void, connectionStatus: "connecting" | "connected" | "disconnected" | "error", focusInput: () => void) => {
setTerminalRef({ sendData, connectionStatus }); setTerminalRef({ sendData, connectionStatus, focusInput });
}, },
[] []
); );
@@ -93,12 +94,14 @@ export const MobileTerminalWrapper: React.FC<MobileTerminalWrapperProps> = ({
onSend={handleSendKey} onSend={handleSendKey}
isVisible={keysAutoHide.isVisible && !showPanel} isVisible={keysAutoHide.isVisible && !showPanel}
onMoreClick={() => setShowPanel(true)} onMoreClick={() => setShowPanel(true)}
onKeepFocus={() => terminalRef?.focusInput()}
/> />
<SpecialKeysPanel <SpecialKeysPanel
onSend={handleSendKey} onSend={handleSendKey}
isOpen={showPanel} isOpen={showPanel}
onClose={() => setShowPanel(false)} onClose={() => setShowPanel(false)}
onKeepFocus={() => terminalRef?.focusInput()}
/> />
</div> </div>
); );
+11 -8
View File
@@ -5,6 +5,7 @@ interface SpecialKeysPanelProps {
onSend: (data: string) => void; onSend: (data: string) => void;
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onKeepFocus?: () => void;
} }
const EXPANDED_KEYS: { key: SpecialKey; label: string }[] = [ const EXPANDED_KEYS: { key: SpecialKey; label: string }[] = [
@@ -36,11 +37,19 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
onSend, onSend,
isOpen, isOpen,
onClose, onClose,
onKeepFocus,
}) => { }) => {
const { sendKey } = useSpecialKeys({ onSend }); const { sendKey } = useSpecialKeys({ onSend });
if (!isOpen) return null; if (!isOpen) return null;
const handlePointerDown = (e: React.PointerEvent, key: SpecialKey) => {
e.preventDefault();
sendKey(key);
onClose();
onKeepFocus?.();
};
return ( return (
<div className="special-keys-panel-overlay" onClick={onClose}> <div className="special-keys-panel-overlay" onClick={onClose}>
<div <div
@@ -52,10 +61,7 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
<button <button
key={key} key={key}
className="special-key-button" className="special-key-button"
onClick={() => { onPointerDown={(e) => handlePointerDown(e, key)}
sendKey(key);
onClose();
}}
type="button" type="button"
> >
{label} {label}
@@ -68,10 +74,7 @@ export const SpecialKeysPanel: React.FC<SpecialKeysPanelProps> = ({
<button <button
key={key} key={key}
className="special-key-button" className="special-key-button"
onClick={() => { onPointerDown={(e) => handlePointerDown(e, key)}
sendKey(key);
onClose();
}}
type="button" type="button"
> >
{label} {label}
+16 -2
View File
@@ -5,6 +5,7 @@ interface SpecialKeysStripProps {
onSend: (data: string) => void; onSend: (data: string) => void;
isVisible: boolean; isVisible: boolean;
onMoreClick?: () => void; onMoreClick?: () => void;
onKeepFocus?: () => void;
} }
const PRIMARY_KEYS: { key: SpecialKey; label: string }[] = [ const PRIMARY_KEYS: { key: SpecialKey; label: string }[] = [
@@ -22,16 +23,29 @@ export const SpecialKeysStrip: React.FC<SpecialKeysStripProps> = ({
onSend, onSend,
isVisible, isVisible,
onMoreClick, onMoreClick,
onKeepFocus,
}) => { }) => {
const { sendKey } = useSpecialKeys({ onSend }); const { sendKey } = useSpecialKeys({ onSend });
const handlePointerDown = (e: React.PointerEvent, key: SpecialKey) => {
e.preventDefault();
sendKey(key);
onKeepFocus?.();
};
const handleMorePointerDown = (e: React.PointerEvent) => {
e.preventDefault();
onMoreClick?.();
onKeepFocus?.();
};
return ( return (
<div className={`special-keys-strip ${isVisible ? "visible" : "hidden"}`}> <div className={`special-keys-strip ${isVisible ? "visible" : "hidden"}`}>
{PRIMARY_KEYS.map(({ key, label }) => ( {PRIMARY_KEYS.map(({ key, label }) => (
<button <button
key={key} key={key}
className="special-key-button" className="special-key-button"
onClick={() => sendKey(key)} onPointerDown={(e) => handlePointerDown(e, key)}
type="button" type="button"
aria-label={`Send ${label}`} aria-label={`Send ${label}`}
> >
@@ -41,7 +55,7 @@ export const SpecialKeysStrip: React.FC<SpecialKeysStripProps> = ({
{onMoreClick && ( {onMoreClick && (
<button <button
className="special-key-button special-key-more" className="special-key-button special-key-more"
onClick={onMoreClick} onPointerDown={handleMorePointerDown}
type="button" type="button"
aria-label="More special keys" aria-label="More special keys"
> >
+10 -3
View File
@@ -10,7 +10,8 @@ interface TerminalProps {
isMobile?: boolean; isMobile?: boolean;
onTerminalReady?: ( onTerminalReady?: (
sendData: (data: string) => void, sendData: (data: string) => void,
connectionStatus: "connecting" | "connected" | "disconnected" | "error" connectionStatus: "connecting" | "connected" | "disconnected" | "error",
focusInput: () => void
) => void; ) => void;
} }
@@ -196,7 +197,10 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
ws.send(data); ws.send(data);
} }
}; };
onTerminalReadyRef.current(sendData, status); const focusInput = () => {
hiddenInputRef.current?.focus();
};
onTerminalReadyRef.current(sendData, status, focusInput);
} }
// Visibility API for reconnection // Visibility API for reconnection
@@ -225,7 +229,10 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
wsRef.current.send(data); wsRef.current.send(data);
} }
}; };
onTerminalReady(sendData, status); const focusInput = () => {
hiddenInputRef.current?.focus();
};
onTerminalReady(sendData, status, focusInput);
} }
}, [status, onTerminalReady]); }, [status, onTerminalReady]);