diff --git a/resources/android/BareTextInputRenderer.kt b/resources/android/BareTextInputRenderer.kt index a52a7ef..f42325c 100644 --- a/resources/android/BareTextInputRenderer.kt +++ b/resources/android/BareTextInputRenderer.kt @@ -18,9 +18,11 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.sp +import com.nativephp.mobile.ui.nativerender.KeyboardFocusPolicy import com.nativephp.mobile.ui.nativerender.NativeUINode import com.nativephp.mobile.ui.nativerender.argbToComposeColor import com.nativephp.plugins.native_ui.NativeUITheme @@ -79,6 +81,7 @@ object BareTextInputRenderer { var value by remember { mutableStateOf(TextFieldValue(props.serverValue, TextRange(props.serverValue.length))) } var lastSentValue by remember { mutableStateOf(props.serverValue) } var wasFocused by remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current // Caret / selection reporter — independent of the direct change/submit // dispatchers; no-op unless `on_selection_change` is wired and the @@ -123,11 +126,15 @@ object BareTextInputRenderer { // layout mode) still wins since it comes later in the chain. // onFocusChanged is Bare's blur hook (no InteractionSource here): // flush the pending selection on the focused → unfocused edge. + // The KeyboardFocusPolicy flag lets interactive taps elsewhere + // honor this field's keep-focus-on-submit (mobile-air #335). modifier = Modifier .fillMaxWidth() .onFocusChanged { state -> if (wasFocused && !state.isFocused) selectionReporter.flush(value) wasFocused = state.isFocused + KeyboardFocusPolicy.focusedFieldKeepsFocus = + state.isFocused && props.keepFocusOnSubmit } .then(modifier), enabled = !props.disabled, @@ -160,6 +167,12 @@ object BareTextInputRenderer { // Flush the settled caret before the submit event fires. selectionReporter.flush(value) props.dispatchSubmit?.invoke(value.text) + // Supplying KeyboardActions replaces Compose's default + // hide-on-Done, so dismissal is restored here to match + // the iOS renderer and the documented default (#335). + if (!props.keepFocusOnSubmit) { + focusManager.clearFocus() + } }) ) } diff --git a/resources/android/FilledTextInputRenderer.kt b/resources/android/FilledTextInputRenderer.kt index 0231c9c..76ac84c 100644 --- a/resources/android/FilledTextInputRenderer.kt +++ b/resources/android/FilledTextInputRenderer.kt @@ -20,11 +20,13 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import com.nativephp.mobile.ui.nativerender.KeyboardFocusPolicy import com.nativephp.mobile.ui.nativerender.NativeUINode import com.nativephp.plugins.native_ui.NativeUITheme @@ -79,17 +81,24 @@ object FilledTextInputRenderer { } val interactionSource = remember { MutableInteractionSource() } + val focusManager = LocalFocusManager.current LaunchedEffect(interactionSource) { val focusStack = mutableListOf() interactionSource.interactions.collect { interaction: Interaction -> when (interaction) { - is FocusInteraction.Focus -> focusStack += interaction + is FocusInteraction.Focus -> { + focusStack += interaction + // Lets interactive taps elsewhere honor this + // field's keep-focus-on-submit (mobile-air #335). + KeyboardFocusPolicy.focusedFieldKeepsFocus = props.keepFocusOnSubmit + } is FocusInteraction.Unfocus -> { focusStack.remove(interaction.focus) if (focusStack.isEmpty()) { // Flush the pending selection, then any deferred text. selectionReporter.flush(value) dispatcher.onBlur(value.text) + KeyboardFocusPolicy.focusedFieldKeepsFocus = false } } else -> { /* ignore */ } @@ -147,6 +156,12 @@ object FilledTextInputRenderer { // Flush the settled caret before the submit event fires. selectionReporter.flush(value) dispatcher.onSubmit(value.text) + // Supplying KeyboardActions replaces Compose's default + // hide-on-Done, so dismissal is restored here to match + // the iOS renderer and the documented default (#335). + if (!props.keepFocusOnSubmit) { + focusManager.clearFocus() + } }), textStyle = TextStyle(fontSize = textSize, color = theme.onSurface, fontFamily = customFontFamily, lineHeight = lineHeight), colors = TextFieldDefaults.colors( diff --git a/resources/android/OutlinedTextInputRenderer.kt b/resources/android/OutlinedTextInputRenderer.kt index 4013484..eeda43a 100644 --- a/resources/android/OutlinedTextInputRenderer.kt +++ b/resources/android/OutlinedTextInputRenderer.kt @@ -20,11 +20,13 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import com.nativephp.mobile.ui.nativerender.KeyboardFocusPolicy import com.nativephp.mobile.ui.nativerender.NativeUINode import com.nativephp.plugins.native_ui.NativeUITheme @@ -91,11 +93,17 @@ object OutlinedTextInputRenderer { // modes. Passing our own source also means we don't pay for M3's // default ripple-focus-hover machinery elsewhere. val interactionSource = remember { MutableInteractionSource() } + val focusManager = LocalFocusManager.current LaunchedEffect(interactionSource) { val focusStack = mutableListOf() interactionSource.interactions.collect { interaction: Interaction -> when (interaction) { - is FocusInteraction.Focus -> focusStack += interaction + is FocusInteraction.Focus -> { + focusStack += interaction + // Lets interactive taps elsewhere honor this + // field's keep-focus-on-submit (mobile-air #335). + KeyboardFocusPolicy.focusedFieldKeepsFocus = props.keepFocusOnSubmit + } is FocusInteraction.Unfocus -> { focusStack.remove(interaction.focus) if (focusStack.isEmpty()) { @@ -103,6 +111,7 @@ object OutlinedTextInputRenderer { // caret lands, then flush any deferred text change. selectionReporter.flush(value) dispatcher.onBlur(value.text) + KeyboardFocusPolicy.focusedFieldKeepsFocus = false } } else -> { /* ignore press/hover/drag */ } @@ -160,6 +169,12 @@ object OutlinedTextInputRenderer { // Flush the settled caret before the submit event fires. selectionReporter.flush(value) dispatcher.onSubmit(value.text) + // Supplying KeyboardActions replaces Compose's default + // hide-on-Done, so dismissal is restored here to match + // the iOS renderer and the documented default (#335). + if (!props.keepFocusOnSubmit) { + focusManager.clearFocus() + } }), textStyle = TextStyle(fontSize = textSize, color = theme.onSurface, fontFamily = customFontFamily, lineHeight = lineHeight), colors = OutlinedTextFieldDefaults.colors( diff --git a/resources/android/TextInputShared.kt b/resources/android/TextInputShared.kt index d8eb644..1da1b32 100644 --- a/resources/android/TextInputShared.kt +++ b/resources/android/TextInputShared.kt @@ -53,6 +53,7 @@ internal data class TextInputProps( val keyboard: KeyboardType, val capitalization: KeyboardCapitalization?, val disabled: Boolean, + val keepFocusOnSubmit: Boolean, val readOnly: Boolean, val isError: Boolean, val loading: Boolean, @@ -117,6 +118,7 @@ internal fun parseTextInputProps(node: NativeUINode): TextInputProps { keyboard = resolveKeyboardType(p.getString("keyboard")), capitalization = resolveCapitalization(p.getString("autocapitalize"), p.getString("keyboard")), disabled = p.getBool("disabled"), + keepFocusOnSubmit = p.getBool("keep_focus_on_submit"), readOnly = p.getBool("read_only"), isError = p.getBool("is_error"), loading = p.getBool("loading"), diff --git a/resources/ios/NativeUITextInputCore.swift b/resources/ios/NativeUITextInputCore.swift index 018d9a4..f7ac7ac 100644 --- a/resources/ios/NativeUITextInputCore.swift +++ b/resources/ios/NativeUITextInputCore.swift @@ -213,6 +213,15 @@ struct NativeUITextInputCore: View { scheduleSelectionEmit(text: text, cb: onSelectionCb, debounceMs: selDebounceMs) } .onChange(of: isFocused) { _, focused in + // Lets interactive taps elsewhere honor this field's + // keep-focus-on-submit, and gives press dispatch a + // flush hook so a tap-committed autocorrection's + // change reaches PHP first (mobile-air #335). + KeyboardFocusPolicy.focusedFieldKeepsFocus = focused && keepFocus + KeyboardFocusPolicy.focusedFieldActive = focused + KeyboardFocusPolicy.flushFocusedField = focused + ? { flushPending(onChangeCb: onChangeCb) } + : nil // On blur, flush any pending change — covers both `blur` mode // (never dispatched mid-typing) and `debounce` mode (in-flight // timer that should commit immediately rather than race with