OBLS-635 Add native KeyboardModule for reliable soft keyboard control…#382
OBLS-635 Add native KeyboardModule for reliable soft keyboard control…#382olewandowski1 wants to merge 1 commit intodevelopfrom
Conversation
There was a problem hiding this comment.
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 viaInputMethodManager. - Added
KeyboardUtilsTS wrapper helpers (showSoftKeyboard/hideSoftKeyboard) for platform-appropriate keyboard control. - Refactored
ScannerInputto removeforwardRef/useImperativeHandle, and to drive keyboard visibility viaonFocus+ 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.
| export function hideSoftKeyboard(): void { | ||
| if (Platform.OS === 'android' && KeyboardModule) { | ||
| KeyboardModule.hideKeyboard(); | ||
| } |
There was a problem hiding this comment.
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.
| 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; | ||
| }); |
There was a problem hiding this comment.
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).
| const handleKeyboardPress = () => { | ||
| setShowKeyboard((prevState) => { | ||
| const newShowKeyboard = !prevState; | ||
|
|
||
| if (newShowKeyboard) { | ||
| showSoftKeyboard(); | ||
| } else { | ||
| hideSoftKeyboard(); | ||
| } |
There was a problem hiding this comment.
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.
| 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); | ||
| } | ||
| }); |
There was a problem hiding this comment.
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.
| public void onViewDetachedFromWindow(View v) { | ||
| focusedView.getViewTreeObserver() | ||
| .removeOnWindowFocusChangeListener(listenerHolder[0]); | ||
| focusedView.removeOnAttachStateChangeListener(this); | ||
| } |
There was a problem hiding this comment.
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()) { ... }.
… on Android
https://openboxes.atlassian.net/browse/OBLS-635
Changes: