Skip to content

OBLS-635 Add native KeyboardModule for reliable soft keyboard control…#382

Open
olewandowski1 wants to merge 1 commit intodevelopfrom
OBLS-635-1
Open

OBLS-635 Add native KeyboardModule for reliable soft keyboard control…#382
olewandowski1 wants to merge 1 commit intodevelopfrom
OBLS-635-1

Conversation

@olewandowski1
Copy link
Copy Markdown
Collaborator

… on Android

https://openboxes.atlassian.net/browse/OBLS-635

Changes:

  • Add native Android KeyboardModule that calls InputMethodManager.showSoftInput() directly, bypassing the unreliable showSoftInputOnFocus prop behavior during navigation transitions
  • The module waits for window focus via OnWindowFocusChangeListener before showing keyboard, with detach guards to prevent memory leaks
  • Simplify ScannerInput component: remove unused forwardRef/useImperativeHandle, show keyboard via onFocus callback + native module, guard handleBlur and keyboardDidHide listener against keyboard mode
  • showSoftInputOnFocus prop now reflects showKeyboard state (preserves iOS behavior)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a native Android KeyboardModule to reliably show/hide the soft keyboard during navigation transitions, and updates ScannerInput to use the native path while simplifying focus/keyboard handling.

Changes:

  • Added Android native module (KeyboardModule) + package registration to control soft keyboard via InputMethodManager.
  • Added KeyboardUtils TS wrapper helpers (showSoftKeyboard / hideSoftKeyboard) for platform-appropriate keyboard control.
  • Refactored ScannerInput to remove forwardRef/useImperativeHandle, and to drive keyboard visibility via onFocus + the new native module.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/utils/KeyboardUtils.ts New helper functions to invoke the native keyboard module (Android) from JS.
src/components/ScannerInput.tsx Simplifies component structure and switches keyboard control to the native module + showSoftInputOnFocus.
android/app/src/main/java/com/openboxes_mobile_o/MainApplication.java Registers KeyboardPackage so the native module is available to JS.
android/app/src/main/java/com/openboxes_mobile_o/KeyboardPackage.java New ReactPackage to expose KeyboardModule.
android/app/src/main/java/com/openboxes_mobile_o/KeyboardModule.java New native module implementing reliable show/hide with window-focus waiting logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +21 to +24
export function hideSoftKeyboard(): void {
if (Platform.OS === 'android' && KeyboardModule) {
KeyboardModule.hideKeyboard();
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hideSoftKeyboard() is Android-only, but ScannerInput uses it to close the keyboard on all platforms. On iOS this becomes a no-op, so tapping the keyboard icon to close the keyboard will no longer dismiss an already-visible keyboard. Consider calling Keyboard.dismiss() on iOS (or unconditionally) and using the native module only as the Android reliability path.

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +209
const handleKeyboardPress = () => {
setShowKeyboard((prevState) => {
const newShowKeyboard = !prevState;

if (newShowKeyboard) {
showSoftKeyboard();
} else {
hideSoftKeyboard();
}

return (
<PaperTextInput
ref={internalInputRef}
mode="outlined"
label={formattedLabel}
value={value}
style={style}
// Keep keyboard hidden
showSoftInputOnFocus={showKeyboard}
autoCorrect={false}
autoCompleteType="off"
importantForAutofill="no"
placeholder={placeholder}
placeholderTextColor={danger ? Theme.colors.danger : Theme.colors.disabled}
blurOnSubmit={false}
returnKeyType="done"
keyboardType={keyboardType || 'default'}
error={danger}
left={
// @ts-ignore
<PaperTextInput.Icon
name={() => leftIcon || <ScanIcon size={24} color={danger ? Theme.colors.danger : undefined} />}
/>
}
right={
// @ts-ignore
<PaperTextInput.Icon
name={() => <KeyboardIcon size={24} color={danger ? Theme.colors.danger : undefined} />}
onPress={handleKeyboardPress}
/>
}
onBlur={handleBlur}
onFocus={() => {}}
onChangeText={handleChangeText}
onSubmitEditing={handleSubmitEditing}
/>
);
}
);
return newShowKeyboard;
});
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setShowKeyboard(prev => { ... }) contains side effects (showSoftKeyboard() / hideSoftKeyboard()). React state updater functions are expected to be pure and may be invoked more than once in development/Concurrent rendering, which can lead to duplicate show/hide calls. Move these effects to a useEffect that runs when showKeyboard changes (or compute the next value outside the updater and call the side effect after setting state).

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +206
const handleKeyboardPress = () => {
setShowKeyboard((prevState) => {
const newShowKeyboard = !prevState;

if (newShowKeyboard) {
showSoftKeyboard();
} else {
hideSoftKeyboard();
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When enabling the keyboard (newShowKeyboard === true) this calls showSoftKeyboard(), but the native module relies on activity.getCurrentFocus(). If the PaperTextInput.Icon press doesn’t guarantee the TextInput is focused (e.g., focus was elsewhere), showKeyboard() can no-op. Consider explicitly focusing inputRef.current (or calling requestFocus()) before invoking showSoftKeyboard() when toggling on.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +87
final ViewTreeObserver.OnWindowFocusChangeListener[] listenerHolder =
new ViewTreeObserver.OnWindowFocusChangeListener[1];

listenerHolder[0] = new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
focusedView.getViewTreeObserver()
.removeOnWindowFocusChangeListener(this);
if (hasFocus) {
focusedView.post(new Runnable() {
@Override
public void run() {
imm.showSoftInput(focusedView,
InputMethodManager.SHOW_IMPLICIT);
}
});
}
}
};

focusedView.getViewTreeObserver().addOnWindowFocusChangeListener(listenerHolder[0]);

// Clean up listener if the view is detached before window focus arrives
focusedView.addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {}

@Override
public void onViewDetachedFromWindow(View v) {
focusedView.getViewTreeObserver()
.removeOnWindowFocusChangeListener(listenerHolder[0]);
focusedView.removeOnAttachStateChangeListener(this);
}
});
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OnAttachStateChangeListener added here is only removed in onViewDetachedFromWindow. If window focus arrives and the view stays attached, the attach listener remains registered indefinitely, and repeated showKeyboard() calls during transitions can accumulate listeners. Keep a reference to the attach listener and remove it in onWindowFocusChanged once focus is handled.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +86
public void onViewDetachedFromWindow(View v) {
focusedView.getViewTreeObserver()
.removeOnWindowFocusChangeListener(listenerHolder[0]);
focusedView.removeOnAttachStateChangeListener(this);
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In onViewDetachedFromWindow, this calls focusedView.getViewTreeObserver().removeOnWindowFocusChangeListener(...) without checking whether the ViewTreeObserver is alive. When a view is detached its observer can be dead, and interacting with it may throw IllegalStateException. Guard with ViewTreeObserver vto = focusedView.getViewTreeObserver(); if (vto.isAlive()) { ... }.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants