Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions resources/android/BareTextInputRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
}
})
)
}
Expand Down
17 changes: 16 additions & 1 deletion resources/android/FilledTextInputRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -79,17 +81,24 @@ object FilledTextInputRenderer {
}

val interactionSource = remember { MutableInteractionSource() }
val focusManager = LocalFocusManager.current
LaunchedEffect(interactionSource) {
val focusStack = mutableListOf<FocusInteraction.Focus>()
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 */ }
Expand Down Expand Up @@ -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(
Expand Down
17 changes: 16 additions & 1 deletion resources/android/OutlinedTextInputRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -91,18 +93,25 @@ 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<FocusInteraction.Focus>()
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 first so the final
// caret lands, then flush any deferred text change.
selectionReporter.flush(value)
dispatcher.onBlur(value.text)
KeyboardFocusPolicy.focusedFieldKeepsFocus = false
}
}
else -> { /* ignore press/hover/drag */ }
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions resources/android/TextInputShared.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"),
Expand Down
9 changes: 9 additions & 0 deletions resources/ios/NativeUITextInputCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading