Skip to content

feat(a11y): Physical Keyboard Navigation#7010

Open
OtavioStasiak wants to merge 126 commits intodevelopfrom
fix.android-inverted-keyboard-navigation
Open

feat(a11y): Physical Keyboard Navigation#7010
OtavioStasiak wants to merge 126 commits intodevelopfrom
fix.android-inverted-keyboard-navigation

Conversation

@OtavioStasiak
Copy link
Copy Markdown
Contributor

@OtavioStasiak OtavioStasiak commented Feb 26, 2026

Proposed changes

Users should be able to navigate through the app using an external keyboard.

Issue(s)

https://rocketchat.atlassian.net/browse/MA-266

How to test or reproduce

  • Login navigation;
  • Basic navigation in RoomsListView (open the drawer, navigate to DirectoryView);
  • In RoomsListView, open the ServersList and navigate through the content;
  • Navigate to RoomView (the list must not be inverted on Android or iOS);
  • Navigate through reactions and focus on the thread button;
  • Navigate to A11y and Appearance;
  • In A11y and Appearance, test switches and pickers.

What we should ensure:
1 - The RoomView message list must work as expected on both platforms;
2 - In master-detail, we must be able to focus on items inside the modal (Android issue);
3 - ActionSheet navigation must work as expected on both platforms.

What is not covered here:
1 - Keyboard navigation for room swipe actions.

PS: Must be tested on android, iOS, iPad and Android tablet.

Navigation tips:
On iOS, keyboard navigation is more hierarchical. You typically use the Tab key to move between elements, such as entering a list, and then use the arrow keys to navigate within that element (for example, moving between items inside the list).

On Android, keyboard navigation is more flexible. You can use both the Tab key and the arrow keys to move between and within elements, allowing for a more fluid navigation experience without needing to switch modes.

Screenshots

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Summary by CodeRabbit

  • New Features

    • Added external hardware keyboard navigation and detection support
    • Improved keyboard focus management for the room header and message composer
  • Accessibility

    • Enhanced accessibility labels and roles for message reactions and thread navigation
    • Better keyboard navigation across the application

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR introduces keyboard and external input navigation improvements for Android and iOS, including inverted scroll view focus handling, external keyboard detection, imperative focus management via refs, and comprehensive Maestro test flows validating keyboard navigation across the app's UI components.

Changes

Cohort / File(s) Summary
Android Scroll View Inversion & Focus Navigation
android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java, android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java, android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java, android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java
Replaces accessibility traversal reordering with keyboard-focused navigation logic. InvertedScrollView now intercepts Tab/DPAD key events and performs custom focus traversal across inverted cells; includes exit focus boundary via native ID. InvertedScrollContentView adds inversion flag and focus/accessibility customization. Both managers expose new React props (isInvertedContent, exitFocusNativeId).
Android Native Modules & Resources
android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt, android/app/src/main/java/chat/rocket/reactnative/input/ExternalInputModule.kt, android/app/src/main/java/chat/rocket/reactnative/input/ExternalInputPackage.kt, android/app/src/main/res/values/ids.xml, android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
Added ExternalInputModule to detect external keyboard connectivity via native Android Configuration API; integrated via ExternalInputPackage. Cleaned up import ordering in MainActivity. Created empty resource IDs file for future use.
React Native Focus Management & Refs
app/containers/RoomHeader/RoomHeader.tsx, app/containers/RoomHeader/index.tsx, app/containers/MessageComposer/interfaces.ts, app/containers/MessageComposer/context.tsx, app/containers/MessageComposer/components/BaseButton.tsx, app/views/RoomView/index.tsx
Introduced imperative focus() ref methods for RoomHeader using accessibility info and for MessageComposer via interfaces. Implemented focus timeout management in composer context with cancelBlur(). BaseButton now integrates composer API to set focused state on focus events. RoomView wires focus listeners to trigger header or composer focus on navigation focus.
Scroll View Component Integration
app/views/RoomView/List/components/InvertedScrollView.tsx, app/views/RoomView/List/components/InvertedScrollViewAdapter.tsx, app/views/RoomView/List/components/List.tsx, app/containers/MessageComposer/components/MessageComposerContent.tsx
Refactored InvertedScrollView to delegate to new InvertedScrollViewAdapter class component wrapping native scroll/content views. Adapter exposes scroll imperative methods (scrollTo, scrollToEnd, flashScrollIndicators, etc.) and wires exitFocusNativeId prop. List now passes MESSAGE_COMPOSER_EXIT_FOCUS_NATIVE_ID to inverted scroll view on non-iOS platforms. MessageComposerContent assigns native ID for focus boundary exit.
External Keyboard Detection
app/lib/methods/helpers/externalInput.ts, app/containers/MessageComposer/components/ComposerInput.tsx, ios/ExternalInputModule.h, ios/ExternalInputModule.m, jest.setup.js, package.json
Added native iOS keyboard detection module (GCKeyboard-based) and JS helper. ComposerInput now prevents blur state reset when external keyboard is connected (alongside iOS back-swipe check). Added react-native-external-keyboard dependency. Jest setup mocks external input helper for tests.
Accessibility & User Interaction
app/containers/message/Reactions.tsx, app/containers/message/Thread.tsx, app/containers/message/index.tsx, app/containers/Button/index.tsx, app/containers/Button/Button.test.tsx, app/views/DisplayPrefsView.tsx, app/views/SecurityPrivacyView.tsx
Enhanced accessibility attributes (accessibilityRole='button', labels, state indicators) and Android ripple feedback for message reactions, thread button, and security/display toggles. Updated IButtonProps to exclude children and enabled props via Omit<>. Revised test comment for disabled button. Changed toggle switches to use List.Item onPress handlers instead of inline switch handlers.
Maestro Test Flows
.maestro/tests/keyboardNavigation/keyboard-navigation-components.yaml, .maestro/tests/keyboardNavigation/keyboard-navigation-onboarding.yaml, .maestro/tests/keyboardNavigation/keyboard-navigation-room.yaml, .maestro/tests/room/jump-to-message.yaml
Added three new keyboard navigation test flows (keyboard-navigation-components.yaml, keyboard-navigation-onboarding.yaml, keyboard-navigation-room.yaml) validating keyboard traversal across UI components, onboarding, and room messaging. Changed jump-to-message.yaml selector from id-based to text-based for robustness.
CI/CD & Build Infrastructure
.github/workflows/build-pr.yml, .github/workflows/maestro-android.yml, scripts/create-avd-keyboard-navigation.sh
Increased e2e-run-android shard count from 13 to 14. Refactored maestro-android.yml to conditionally provision a keyboard-enabled AVD (Pixel_API_34_Keyboard) for shard 14, with custom hardware properties appended to AVD config. Added create-avd-keyboard-navigation.sh script to automate AVD provisioning with keyboard/display settings.
Localization & Constants
app/i18n/locales/en.json, app/lib/constants/accessibility.ts
Added "Add_reaction": "Add reaction" locale entry and exported MESSAGE_COMPOSER_EXIT_FOCUS_NATIVE_ID constant ('message-composer-exit-focus') to coordinate keyboard boundary focus.
React Native Gesture Handler Patch
patches/react-native-gesture-handler+2.24.0.patch
Patched RNGestureHandlerButton to support keyboard focus navigation by overriding canBecomeFocused, adding key-press detection (_isActivationKeyPress), and intercepting spacebar/return key presses to trigger button control events.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as React App
    participant RoomHeader
    participant RoomView
    participant InteractionManager
    
    User->>App: Navigation Focus (isMasterDetail=true)
    App->>RoomView: componentDidMount (focus listener registered)
    RoomView->>InteractionManager: runAfterInteractions()
    InteractionManager->>RoomView: Execute callback
    RoomView->>RoomHeader: roomHeaderRef.current?.focus()
    RoomHeader->>RoomHeader: useImperativeHandle (expose focus)
    RoomHeader->>RoomHeader: findNodeHandle(headerRef)
    RoomHeader->>App: AccessibilityInfo.setAccessibilityFocus(nodeHandle)
Loading
sequenceDiagram
    participant KeyEvent as KeyEvent (Tab/DPAD)
    participant InvertedScrollView
    participant ScrollChild as Current Focused Child
    participant AdjacentChild as Adjacent Cell
    participant Composer as Message Composer
    
    KeyEvent->>InvertedScrollView: dispatchKeyEvent(KEYCODE_TAB / DPAD_DOWN)
    InvertedScrollView->>InvertedScrollView: Resolve focused child cell
    InvertedScrollView->>AdjacentChild: requestFocus(direction)
    alt Focus Accepted
        AdjacentChild->>InvertedScrollView: return true
    else Boundary Reached (end of list)
        InvertedScrollView->>InvertedScrollView: Lookup exit focus via nativeID
        InvertedScrollView->>Composer: ReactFindViewUtil.findView(exitFocusNativeId)
        Composer->>Composer: requestFocus()
    end
Loading
sequenceDiagram
    participant ComposerInput as ComposerInput Component
    participant ExternalInput as ExternalInput Module
    participant Native as Android/iOS Native Layer
    participant Context as MessageComposer Context
    
    ComposerInput->>ComposerInput: onBlur triggered
    ComposerInput->>ExternalInput: isExternalKeyboardConnected()
    ExternalInput->>Native: Query Configuration/GCKeyboard
    Native->>ExternalInput: Return keyboard status
    alt External Keyboard Connected
        ComposerInput->>Context: Skip blur (keep focus state)
    else No External Keyboard
        ComposerInput->>Context: setFocused(false) with 150ms delay
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • chore: improvement to end to end test #7008: Modifies Maestro Android CI/workflow configuration and e2e test orchestration infrastructure, particularly .github/workflows/maestro-android.yml and test orchestration patterns.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR contains significant scope beyond MA-266: iOS keyboard module, physical keyboard detection, composer blur/focus timing, accessibility enhancements (Reactions, Thread, DisplayPrefs, SecurityPrivacy), RoomHeader imperative focus, and Maestro test flows. These extend beyond the stated Android inverted list fix. Clarify whether all added features (iOS module, ComposerInput blur logic, accessibility improvements, RoomHeader focus) are required for MA-266 or should be separated into dedicated issues/PRs.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(a11y): Physical Keyboard Navigation' directly describes the primary feature being added—comprehensive keyboard navigation support across the app.
Linked Issues check ✅ Passed The PR addresses MA-266 by implementing keyboard navigation for the inverted message list on Android, with focus traversal fixes via custom key event handling in InvertedScrollView and exit-focus coordination to the composer.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.70.0.108314

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNSHg7TdML4PFPLPaD3vbMSQN1kCdTXPzueVdh4N44q6GZfR1wk0ZNlhk_GdG11POn-I-qGjSYJWmIjmQQKs

@github-actions
Copy link
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.70.0.108317

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNRigBMM3Bes80dsLl0Wof7_pwq8w6zFWBUUGsRGp2rRCO4FnN6K1TeJ4P-FQhppg3r60B8pyKQNdZQTh6Zy

@github-actions
Copy link
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.70.0.108318

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNQfUhHNuTEb7FEFekyzv7gX_VAjowLUgqhtyPgrwQ7XZI58MKTI6S0_cS2vJpxejQfVdMxEh288m2qGEJxg

@github-actions
Copy link
Copy Markdown

iOS Build Available

Rocket.Chat Experimental 4.72.0.108566

@github-actions
Copy link
Copy Markdown

iOS Build Available

Rocket.Chat Experimental 4.72.0.108585

@github-actions
Copy link
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.72.0.108584

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNSB6GJHRG1Aaz0kNPmsRIVqmj1oEOGbdAajcQ5orWR04pihZm2Rcqj6QhureSsRyHuBYyTEW_FFOnSIBPJ9

Comment thread app/containers/RoomHeader/RoomHeader.tsx
Comment thread android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
@github-actions
Copy link
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.72.0.108646

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNQUTi2MCvyEpF0vL6eF6QWxyD-deHE4bTzn8xXLE1law4FQE9pQuNevSf6qUqddhYoOadSk1w_SiTxQNhgX

@github-actions
Copy link
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.72.0.108651

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNSjFcQLtGYLfn2ilMBlQRTHbxbSlP6cuXa9MhArSwXlOsyEvuW4XRUzS-smnsgJF_b3C_I2a4jueKnmrcZw

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants