mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
fix(mobile): preserve iPad hardware keyboard focus (#12772)
* fix(mobile): preserve iPad terminal input focus * Keep incoming main test formatting unchanged --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
@@ -79,7 +79,7 @@ const HEAD_TIMER_CREATION_SHA256 =
|
||||
'1a31b625e2174c3db77272249843196d2b6b06ab1e654a96d8f7858e3082e66b'
|
||||
const HEAD_TIMER_CLEANUP_SHA256 = 'c73f1d1c2cc89642f3d727d6f3b6b81860a9d6f34234541a2065ec3d1a8cd116'
|
||||
const HEAD_RUNTIME_STRING_SHA256 =
|
||||
'ba52a3ede721bd29acbe8593161e90b216b7f361ff896e085927d4d73fa83b2f'
|
||||
'0c08a53c2cd1e182e1d7edfb7b98bd9e4a313e47c7b93f5a509a89ec3292bc1f'
|
||||
const HEAD_HOST_JSX_SHA256 = '390405926b1695fa3a33686f0bc192b432f5468d8576499d7cafbb4922defbb5'
|
||||
const HEAD_LEAF_JSX_SHA256 = '21dba981875e173f692590bf910d60964660c5f4cbb79f3a377c7e54f6a1f016'
|
||||
const HEAD_STYLE_REFERENCE_SHA256 =
|
||||
@@ -517,7 +517,7 @@ describe('mobile session route extraction parity', () => {
|
||||
|
||||
it('preserves runtime strings, styles, and the expanded JSX tree', () => {
|
||||
const strings = readRuntimeStrings()
|
||||
expect(strings).toHaveLength(546)
|
||||
expect(strings).toHaveLength(547)
|
||||
expect(hash(strings)).toBe(HEAD_RUNTIME_STRING_SHA256)
|
||||
const jsx = readJsxFacts(readDefinitions())
|
||||
expect(jsx.host).toHaveLength(124)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef, useCallback } from 'react'
|
||||
import type { Keyboard, TextInput } from 'react-native'
|
||||
import { Platform, type Keyboard, type TextInput } from 'react-native'
|
||||
import { useFocusEffect } from 'expo-router'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { ConnectionState } from '../transport/types'
|
||||
@@ -142,6 +142,7 @@ export function useMobileSessionTerminalRuntime(scope: MobileSessionScreenStateM
|
||||
lifecycleIdentity: client,
|
||||
lifecycleKey: JSON.stringify([hostId, worktreeId, connState]),
|
||||
liveInputEnabled,
|
||||
reopenFocusedInputWhenKeyboardHidden: Platform.OS === 'android',
|
||||
timerRef: liveInputFocusTimerRef
|
||||
})
|
||||
useFocusEffect(
|
||||
|
||||
@@ -196,18 +196,40 @@ describe('terminal live input', () => {
|
||||
const input = createFocusTarget(() => true)
|
||||
const refocus = vi.fn()
|
||||
|
||||
focusTerminalLiveInputTarget(input, { keyboardHeight: 0, refocus })
|
||||
focusTerminalLiveInputTarget(input, {
|
||||
keyboardHeight: 0,
|
||||
refocus,
|
||||
reopenFocusedInputWhenKeyboardHidden: true
|
||||
})
|
||||
|
||||
expect(input.blur).toHaveBeenCalledTimes(1)
|
||||
expect(input.focus).not.toHaveBeenCalled()
|
||||
expect(refocus).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('keeps an iPad hardware-keyboard responder focused when the software keyboard is absent', () => {
|
||||
const input = createFocusTarget(() => true)
|
||||
const refocus = vi.fn()
|
||||
|
||||
focusTerminalLiveInputTarget(input, {
|
||||
keyboardHeight: 0,
|
||||
refocus,
|
||||
reopenFocusedInputWhenKeyboardHidden: false
|
||||
})
|
||||
|
||||
expect(input.blur).not.toHaveBeenCalled()
|
||||
expect(input.focus).toHaveBeenCalledTimes(1)
|
||||
expect(refocus).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('focuses the capture input directly when the keyboard is open', () => {
|
||||
const input = createFocusTarget(() => true)
|
||||
const refocus = vi.fn()
|
||||
|
||||
focusTerminalLiveInputTarget(input, { keyboardHeight: 240, refocus })
|
||||
focusTerminalLiveInputTarget(input, {
|
||||
keyboardHeight: 240,
|
||||
refocus,
|
||||
reopenFocusedInputWhenKeyboardHidden: true
|
||||
})
|
||||
|
||||
expect(input.blur).not.toHaveBeenCalled()
|
||||
expect(input.focus).toHaveBeenCalledTimes(1)
|
||||
@@ -218,7 +240,11 @@ describe('terminal live input', () => {
|
||||
const input = createFocusTarget(() => false)
|
||||
const refocus = vi.fn()
|
||||
|
||||
focusTerminalLiveInputTarget(input, { keyboardHeight: 0, refocus })
|
||||
focusTerminalLiveInputTarget(input, {
|
||||
keyboardHeight: 0,
|
||||
refocus,
|
||||
reopenFocusedInputWhenKeyboardHidden: true
|
||||
})
|
||||
|
||||
expect(input.blur).not.toHaveBeenCalled()
|
||||
expect(input.focus).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -75,6 +75,7 @@ export type TerminalLiveInputFocusTarget = {
|
||||
type FocusTerminalLiveInputTargetOptions = {
|
||||
readonly keyboardHeight: number
|
||||
readonly refocus: () => void
|
||||
readonly reopenFocusedInputWhenKeyboardHidden: boolean
|
||||
}
|
||||
|
||||
export type TerminalLiveInputDefaultResult = {
|
||||
@@ -230,13 +231,17 @@ export function scheduleTerminalLiveInputFocus(
|
||||
|
||||
export function focusTerminalLiveInputTarget(
|
||||
input: TerminalLiveInputFocusTarget | null,
|
||||
{ keyboardHeight, refocus }: FocusTerminalLiveInputTargetOptions
|
||||
{
|
||||
keyboardHeight,
|
||||
refocus,
|
||||
reopenFocusedInputWhenKeyboardHidden
|
||||
}: FocusTerminalLiveInputTargetOptions
|
||||
): void {
|
||||
if (!input) {
|
||||
return
|
||||
}
|
||||
|
||||
if (keyboardHeight <= 0 && input.isFocused?.()) {
|
||||
if (reopenFocusedInputWhenKeyboardHidden && keyboardHeight <= 0 && input.isFocused?.()) {
|
||||
// Why: Android can keep a hidden TextInput focused after the IME is dismissed;
|
||||
// focus() is then a no-op, so force a new focus session to reopen the keyboard.
|
||||
input.blur()
|
||||
|
||||
@@ -15,6 +15,7 @@ type HarnessProps = {
|
||||
readonly lifecycleIdentity: object | null
|
||||
readonly lifecycleKey: string
|
||||
readonly liveInputEnabled: boolean
|
||||
readonly reopenFocusedInputWhenKeyboardHidden: boolean
|
||||
readonly timerRef: TerminalLiveInputFocusTimerRef
|
||||
}
|
||||
|
||||
@@ -92,6 +93,7 @@ function connectedProps(
|
||||
lifecycleIdentity: null,
|
||||
lifecycleKey: 'host-a:worktree-a:connected',
|
||||
liveInputEnabled: true,
|
||||
reopenFocusedInputWhenKeyboardHidden: true,
|
||||
timerRef
|
||||
}
|
||||
}
|
||||
@@ -128,6 +130,22 @@ describe('terminal live input focus hook', () => {
|
||||
expect(input.focus).toHaveBeenCalledTimes(2)
|
||||
harness.unmount()
|
||||
})
|
||||
it('preserves the focused iPad responder when a hardware keyboard keeps keyboard height at zero', () => {
|
||||
vi.useFakeTimers()
|
||||
const input = createFocusTarget(true)
|
||||
const inputRef = { current: input }
|
||||
const harness = createHarness({
|
||||
...connectedProps(inputRef),
|
||||
reopenFocusedInputWhenKeyboardHidden: false
|
||||
})
|
||||
|
||||
harness.handlers().handleTerminalTap('terminal-a')
|
||||
vi.runAllTimers()
|
||||
|
||||
expect(input.blur).not.toHaveBeenCalled()
|
||||
expect(input.focus).toHaveBeenCalledTimes(1)
|
||||
harness.unmount()
|
||||
})
|
||||
|
||||
it('cancels focus when navigation reuses the mounted route', () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
@@ -11,6 +11,7 @@ type TerminalLiveInputFocusContext = {
|
||||
readonly canSend: boolean
|
||||
readonly keyboardHeight: number
|
||||
readonly liveInputEnabled: boolean
|
||||
readonly reopenFocusedInputWhenKeyboardHidden: boolean
|
||||
}
|
||||
|
||||
type UseTerminalLiveInputFocusOptions<T extends TerminalLiveInputFocusTarget> =
|
||||
@@ -35,17 +36,24 @@ export function useTerminalLiveInputFocus<T extends TerminalLiveInputFocusTarget
|
||||
keyboardHeight,
|
||||
lifecycleIdentity,
|
||||
lifecycleKey,
|
||||
reopenFocusedInputWhenKeyboardHidden,
|
||||
liveInputEnabled,
|
||||
timerRef
|
||||
}: UseTerminalLiveInputFocusOptions<T>): TerminalLiveInputFocusHandlers {
|
||||
const contextRef = useRef<TerminalLiveInputFocusContext>({
|
||||
canSend,
|
||||
keyboardHeight,
|
||||
liveInputEnabled
|
||||
liveInputEnabled,
|
||||
reopenFocusedInputWhenKeyboardHidden
|
||||
})
|
||||
useLayoutEffect(() => {
|
||||
contextRef.current = { canSend, keyboardHeight, liveInputEnabled }
|
||||
}, [canSend, keyboardHeight, liveInputEnabled])
|
||||
contextRef.current = {
|
||||
canSend,
|
||||
keyboardHeight,
|
||||
liveInputEnabled,
|
||||
reopenFocusedInputWhenKeyboardHidden
|
||||
}
|
||||
}, [canSend, keyboardHeight, liveInputEnabled, reopenFocusedInputWhenKeyboardHidden])
|
||||
|
||||
const resetLiveInputFocus = useCallback(() => {
|
||||
clearTerminalLiveInputFocusTimer(timerRef)
|
||||
@@ -62,6 +70,7 @@ export function useTerminalLiveInputFocus<T extends TerminalLiveInputFocusTarget
|
||||
}
|
||||
focusTerminalLiveInputTarget(inputRef.current, {
|
||||
keyboardHeight: context.keyboardHeight,
|
||||
reopenFocusedInputWhenKeyboardHidden: context.reopenFocusedInputWhenKeyboardHidden,
|
||||
refocus: () => scheduleTerminalLiveInputFocus(timerRef, focusLiveInput)
|
||||
})
|
||||
}, [inputRef, timerRef])
|
||||
|
||||
Reference in New Issue
Block a user