Skip to content

Fix: Restore focus to trigger element on back navigation#79493

Draft
mavrickdeveloper wants to merge 6 commits intoExpensify:mainfrom
mavrickdeveloper:fix/76921-focus-restoration-back-navigation
Draft

Fix: Restore focus to trigger element on back navigation#79493
mavrickdeveloper wants to merge 6 commits intoExpensify:mainfrom
mavrickdeveloper:fix/76921-focus-restoration-back-navigation

Conversation

@mavrickdeveloper
Copy link
Contributor

@mavrickdeveloper mavrickdeveloper commented Jan 13, 2026

Explanation of Change

This PR fixes the Web-only issue where focus was lost in 48 scenarios/pages after navigating back in the app, causing screen readers to announce incorrect elements.

Root Cause:

Solution: Implement a custom NavigationFocusManager that:

Notes to code reviewers: #79493 (comment)

Fixed Issues

$ #76921
PROPOSAL: #76921 (comment)

Prerequisite:

The user is logged in
Using Windows + Chrome, open the page https://staging.new.expensify.com/settings/about
Navigate using TAB to the 'App download links' button, press enter to activate
Navigate to the 'Back' button using TAB, press enter to activate
Observe the focus behavior

Tests

Primary Test (Issue #76921):
Note : Tests should only be executed using keyboard navigation (TAB) , (See attached video below for demonstration)

  1. Navigate to Settings > Preferences
  2. Click on "Language" menu item
  3. Press Escape or navigate back
  4. Verify focus returns to the "Language" menu item
  5. With screen reader enabled, verify the correct label is announced

Additional scope : Other pages to test on:

  1. On Settings - About - Keyboard Shortcuts

    • How to test: Navigate to Settings > About > Use TAB to focus "Keyboard shortcuts" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Keyboard shortcuts" menu item with visible focus indicator
  2. On Settings - Save the world - I know a teacher

    • How to test: Navigate to Settings > Save the world > Use TAB to focus "I know a teacher" option > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "I know a teacher" menu item with visible focus indicator
  3. On Settings - Save the world - I am a teacher

    • How to test: Navigate to Settings > Save the world > Use TAB to focus "I am a teacher" option > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "I am a teacher" menu item with visible focus indicator
  4. On Settings - Save the world - Intro your school principal

    • How to test: Navigate to Settings > Save the world > Use TAB to focus "Intro your school principal" option > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Intro your school principal" menu item with visible focus indicator
  5. On Settings - Preferences - Language

    • How to test: Navigate to Settings > Preferences > Use TAB to focus "Language" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Language" menu item with visible focus indicator
  6. On Settings - Preferences - Priority mode

    • How to test: Navigate to Settings > Preferences > Use TAB to focus "Priority mode" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Priority mode" menu item with visible focus indicator
  7. On Settings - Security

    • How to test: Navigate to Settings > Use TAB to focus "Security" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Security" menu item with visible focus indicator
  8. On Settings - Security - Validate your account

    • How to test: N/A - Not a standalone menu item; part of Two-factor authentication flow
    • Expected behavior: N/A
  9. On Settings - Security - Close account

    • How to test: Navigate to Settings > Security > Use TAB to focus "Close account" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Close account" menu item with visible focus indicator
  10. On Settings - Security - Two-factor authentication

    • How to test: Navigate to Settings > Security > Use TAB to focus "Two-factor authentication" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Two-factor authentication" menu item with visible focus indicator
  11. On Settings - Profile - Display name

    • How to test: Navigate to Settings > Profile > Use TAB to focus display name row > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the display name menu item with visible focus indicator
  12. On Settings - Profile - Contact methods

    • How to test: Navigate to Settings > Profile > Use TAB to focus "Contact methods" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Contact methods" menu item with visible focus indicator
  13. On Settings - Profile - Pronouns

    • How to test: Navigate to Settings > Profile > Use TAB to focus "Pronouns" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Pronouns" menu item with visible focus indicator
  14. On Settings - Profile - Share Code

    • How to test: Navigate to Settings > Profile > Use TAB to focus "Share Code" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Share Code" menu item with visible focus indicator
  15. On Settings - Profile - Legal Name

    • How to test: Navigate to Settings > Profile > Use TAB to focus "Legal Name" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Legal Name" menu item with visible focus indicator
  16. On Settings - Profile - DOB

    • How to test: Navigate to Settings > Profile > Use TAB to focus "Date of birth" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Date of birth" menu item with visible focus indicator
  17. On Settings - Profile - Phone number

    • How to test: Navigate to Settings > Profile > Use TAB to focus "Phone number" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Phone number" menu item with visible focus indicator
  18. On Settings - Profile - Address

    • How to test: Navigate to Settings > Profile > Use TAB to focus "Address" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Address" menu item with visible focus indicator
  19. On Settings - Profile - Country

    • How to test: Navigate to Settings > Profile > Address > Use TAB to focus "Country" field > Press Enter to activate > Select a country > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Country" field or Address menu item with visible focus indicator
  20. On Settings - Profile - Timezone

    • How to test: Navigate to Settings > Profile > Use TAB to focus "Timezone" menu item > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Timezone" menu item with visible focus indicator
  21. On Workspaces - Duplicate Workspaces

    • How to test: Navigate to Workspaces > Use TAB to focus workspace row > Press Enter > Use TAB to focus More (...) button > Press Enter > Use TAB to focus "Duplicate workspace" > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "More" button with visible focus indicator
  22. On Workspaces - Delete Workspace

    • How to test: Navigate to Workspaces > Use TAB to focus workspace row > Press Enter > Use TAB to focus More (...) button > Press Enter > Use TAB to focus "Delete workspace" > Press Enter > Use TAB to focus Cancel button > Press Enter
    • Expected behavior: Focus returns to the "More" button with visible focus indicator
  23. On Workspaces - Overview - Workspace Name

    • How to test: Navigate to Workspaces > Use TAB to focus workspace > Press Enter > Use TAB to focus "Workspace name" menu item > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Workspace name" menu item with visible focus indicator
  24. On Workspaces - Overview - Expensify Policy

    • How to test: Navigate to Workspaces > Use TAB to focus workspace > Press Enter > Use TAB to focus workspace avatar/policy settings > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the policy settings menu item with visible focus indicator
  25. On Workspace - Categories - Add Category

    • How to test: Navigate to Workspace > Categories > Use TAB to focus "Add category" button > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Categories" menu item in workspace sidebar with visible focus indicator
  26. On Workspace - Categories - Settings

    • How to test: Navigate to Workspace > Categories > Use TAB to focus a category row > Press Enter to activate > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Categories" menu item in workspace sidebar with visible focus indicator
  27. On Workspace - Workflows - Edit Approval Workflow

    • How to test: Navigate to Workspace > Workflows > Use TAB to focus "Add approvals" or edit approval > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Workflows" menu item in workspace sidebar with visible focus indicator
  28. On Workspace - Workflows - Expenses From

    • How to test: Navigate to Workspace > Workflows > Use TAB to focus "Delay submissions" > Press Enter > Use TAB to focus frequency option > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Workflows" menu item in workspace sidebar with visible focus indicator
  29. On Workspace - Workflows - Approver

    • How to test: Navigate to Workspace > Workflows > Use TAB to focus approver settings > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Workflows" menu item in workspace sidebar with visible focus indicator
  30. On Workspace - Rules - Cash Expense Default

    • How to test: Navigate to Workspace > Rules > Use TAB to focus cash expense setting > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Rules" menu item in workspace sidebar with visible focus indicator
  31. On Workspaces - Distance Rates - Rate Details

    • How to test: Navigate to Workspace > Distance rates > Use TAB to focus a rate row > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Distance rates" menu item in workspace sidebar with visible focus indicator
  32. On Workspaces - Expensify Card - Add bank account

    • How to test: Navigate to Workspace > Expensify Card > Use TAB to focus "Add bank account" or setup flow > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Expensify Card" menu item in workspace sidebar with visible focus indicator
  33. On Workspaces - Expensify Card - Bank info

    • How to test: Navigate to Workspace > Expensify Card > Use TAB to focus bank info settings > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Expensify Card" menu item in workspace sidebar with visible focus indicator
  34. On Workspaces - Expensify Card - Confirm currency and country

    • How to test: Navigate to Workspace > Expensify Card > Card setup flow > Use TAB to focus Confirm currency/country step > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to previous step or "Expensify Card" menu item with visible focus indicator
  35. On Workspace - Company Card - Add Cards

    • How to test: Navigate to Workspace > Company cards > Use TAB to focus "Add cards" or card feed option > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Company cards" menu item in workspace sidebar with visible focus indicator
  36. On Workspace - Create Workspace - Confirm Workspace

    • How to test: Use TAB to focus "+" button > Press Enter to create new workspace > Reach confirmation step > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to previous step in workspace creation flow with visible focus indicator
  37. On Workspace - Create Workspace - Invite new members

    • How to test: During workspace creation > Invite members step > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to previous step in workspace creation flow with visible focus indicator
  38. On Workspace - Create Workspace - Default Currency

    • How to test: During workspace creation > Currency selection step > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to previous step in workspace creation flow with visible focus indicator
  39. On Create Report - Restricted

    • How to test: Navigate to create report flow with restricted permissions > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the element that initiated the report creation with visible focus indicator
  40. On Create Report - Add payment card

    • How to test: Create report flow > Add payment card step > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to previous step in report creation flow with visible focus indicator
  41. On Create Report - Change payment currency

    • How to test: Create report flow > Use TAB to focus Change currency option > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to previous step in report creation flow with visible focus indicator
  42. On Track Distance

    • How to test: Use TAB to focus "+" button > Press Enter > Use TAB to focus Track distance > Press Enter > Fill route details > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the route input field or previous navigation element with visible focus indicator
  43. On Track Distance - Choose Recipient

    • How to test: Track distance flow > Choose recipient step > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to previous step in track distance flow with visible focus indicator
  44. On Send Invoice

    • How to test: Use TAB to focus "+" button > Press Enter > Use TAB to focus Send invoice > Press Enter > Fill invoice details > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to previous step in invoice creation flow with visible focus indicator
  45. On Wallet - Add bank account

    • How to test: Navigate to Wallet > Use TAB to focus "Add bank account" > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the "Add bank account" option or Wallet menu with visible focus indicator
  46. On Create Expense flow

    • How to test: Use TAB to focus "+" button > Press Enter > Use TAB to focus Create expense > Press Enter > Fill expense details > Use TAB to focus Back button at any step > Press Enter
    • Expected behavior: Focus returns to previous step in expense creation flow with visible focus indicator
  47. On Paid Expense details flow

    • How to test: Navigate to a paid expense > Use TAB to focus expense details > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the expense row in the report with visible focus indicator
  48. On Reports flow

    • How to test: Navigate to Reports > Use TAB to focus a report > Press Enter > View report details > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the report row in the reports list with visible focus indicator
  49. On Chat flow

    • How to test: Navigate to a chat room > Use TAB to focus member/settings in RHP > Press Enter > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to the element that opened the RHP (member row or settings button) with visible focus indicator

Regression Test (Issue #46109):

  1. Open the app URL in a new browser tab
  2. Verify NO blue frame appears around any element
  3. Verify focus is NOT incorrectly restored
  • Verify that no errors appear in the JS console

Offline tests

N/A - Focus restoration is a client-side UI behavior that doesn't depend on network state.

QA Steps

Same as tests

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

2026-01-13.17-13-53.mp4

@github-actions
Copy link
Contributor

github-actions bot commented Jan 13, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@mavrickdeveloper
Copy link
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@mavrickdeveloper
Copy link
Contributor Author

recheck

CLABotify added a commit to Expensify/CLA that referenced this pull request Jan 13, 2026
@mavrickdeveloper mavrickdeveloper force-pushed the fix/76921-focus-restoration-back-navigation branch from 5fcfa13 to 5423a53 Compare January 13, 2026 16:31
@mavrickdeveloper mavrickdeveloper changed the title fix: restore focus to trigger element on back navigation Fix: Restore focus to trigger element on back navigation Jan 13, 2026
This fixes Issue Expensify#76921 where focus was lost after navigating back,
causing screen readers to announce incorrect elements.

The root cause was PR Expensify#49240 which set `setReturnFocus: false` in
FocusTrapForScreen to fix Issue Expensify#46109 (blue frame on new tab).
This was an over-correction that broke focus restoration during
in-app navigation.

The fix implements context-aware focus restoration:
- Initial page load: Don't restore focus (guards against Expensify#46109)
- Navigation back: Restore focus to the previously focused element

Changes:
- Capture `previouslyFocusedElement` in `onActivate` callback
- Track `wasActivatedViaNavigation` based on activeElement state
- Implement intelligent `setReturnFocus` that distinguishes scenarios
- Add `isElementFocusable` helper for robust element validation
- Add comprehensive unit tests (18 test cases)
…y#76921)

This implements a complete focus capture and restoration system for
back navigation, fixing Issue Expensify#76921.

Architecture:
- NavigationFocusManager: Singleton that captures focused elements on
  pointerdown/keydown (capture phase) and stores them per route key
- FocusTrapForScreen: Integrates with NavigationFocusManager to capture
  focus when screen loses focus, restore when it regains focus
- initialFocus callback handles navigation-based restoration
- setReturnFocus handles click-outside deactivation

Key changes:
- src/libs/NavigationFocusManager.ts: New focus capture/restore manager
- src/App.tsx: Initialize NavigationFocusManager on app startup
- FocusTrapForScreen: Integration with capture on blur, restore on focus
- MenuItem: interactive={false} removes role="menuitem" for proper
  WCAG semantics and correct NavigationFocusManager element capture
- ApprovalWorkflowSection: Uses interactive={false} pattern
- ButtonWithDropdownMenu: Focus anchor after modal close for capture
- MoneyRequestConfirmationList/NewTaskPage: Only blur inputs to preserve
  focus restoration for non-input elements
- useSyncFocusImplementation: Don't steal focus from already-focused
  elements to preserve NavigationFocusManager restoration

Tests:
- NavigationFocusManagerTest.tsx: 20+ test cases covering all scenarios
- FocusTrapForScreenTest.tsx: Updated with P0 test cases
The autoFocus prop has no cleanup mechanism, so it continues firing
during back navigation as the component unmounts. This steals focus
from NavigationFocusManager's restoration, causing focus to fall to
document.body instead of the original menu item.

useAutoFocusInput uses useFocusEffect which cancels pending focus
operations when the screen loses focus, allowing proper restoration.

This aligns with the established pattern used by 82 other pages.
…76921)

- Add identifier-based element matching for screens that unmount/remount
- Integrate beforeRemove listener to capture focus before screen removal
- Fix MenuItem interactive={false} to allow event bubbling to parent
- Fix ThreeDotsMenu nested keyboard activation (Enter/Space handling)
- Prevent useAutoFocusInput from stealing focus on back navigation
- Add comprehensive tests for NavigationFocusManager
@mavrickdeveloper mavrickdeveloper force-pushed the fix/76921-focus-restoration-back-navigation branch from 78a5558 to 1063217 Compare January 18, 2026 10:40
…ndex

After rebasing onto main, the new shouldBeAccessible and tabIndex props
(with defaults true/0) broke our fix for interactive={false} MenuItems.

Problem: MenuItems with interactive={false} were still accessible and
focusable because the new props ignored the interactive flag.

Solution: Make the JSX conditional on interactive:
- accessible={interactive && shouldBeAccessible}
- tabIndex={interactive ? tabIndex : -1}

This preserves main's flexibility (callers can override accessibility)
while ensuring interactive={false} always results in non-focusable,
non-accessible MenuItems - critical for ApprovalWorkflowSection and
90+ other places using display-only MenuItems.
@mavrickdeveloper mavrickdeveloper force-pushed the fix/76921-focus-restoration-back-navigation branch from acf0059 to a387464 Compare January 18, 2026 10:47
@mavrickdeveloper
Copy link
Contributor Author

Note for code-reviewers

This PR fixes Keyboard navigation focus loss during back navigation on Web. Three components work together:

  • NavigationFocusManager :Captures focus at T+0ms (before navigation)
  • FocusTrapForScreen : Restores focus via initialFocus callback
  • ButtonWithDropdownMenu : Focuses anchor after popover closes

useNavigationState approach didn't work for Focus Capture ,as there are a fundamental constraint,You cannot reliably capture focus state using reactive patterns (hooks) when the action of navigation itself causes focus to move.

The timing issue

When a user clicks a button that triggers navigation, the events unfold in a specific order: at T+0ms the user clicks and activeElement is the button; at T+1ms the click handler calls Navigation.navigate(); sometime later React processes the state change and useNavigationState fires; and by then, the screen transition has begun and activeElement has already moved to body.

By the time useNavigationState fires, focus has already moved. The hook tells you navigation happened, not what was focused before it happened.

Reactive vs Proactive

useNavigationState is reactive,it responds to navigation that already occurred, by which point focus is already lost. What we need is proactive capture,grabbing the element at the moment of user intent, before anything else happens.

My Solution

Capture-phase DOM events (pointerdown, keydown with {capture: true}) run at T+0ms, before any click handlers execute, before navigation triggers, before focus moves. This is the only mechanism that fires early enough to capture the correct element.

React hooks are designed to respond to state changes. But we need to capture state before the change occurs. This requires stepping outside React's reactive model entirely.


Changes

src/libs/NavigationFocusManager.ts (NEW)

Issue: By the time FocusTrap callbacks fire, focus has already moved to document.body.

Logic: Capture-phase listeners grab the element BEFORE navigation logic runs. Same pattern as ComposerFocusManager in this codebase.

Key features:

  • pointerdown/keydown capture-phase listeners
  • State-based menuitem skip (preserves anchor button for dropdowns)

src/components/FocusTrap/FocusTrapForScreen/index.web.tsx

Issue: setReturnFocus fires on trap DEactivation (leaving screen), not on returning.

Logic: Changed to use initialFocus callback which fires on trap activation (returning to screen). Added wasNavigatedTo flag to prevent focus on initial page load (#46109).


src/App.tsx

Issue: NavigationFocusManager must initialize before any navigation occurs.

Logic: App.tsx guarantees early initialization and proper cleanup for HMR/StrictMode. Same location as other app-wide modules (useDefaultDragAndDrop, OnyxUpdateManager).


src/components/ButtonWithDropdownMenu/index.tsx

Issue: After clicking dropdown menu item, the menu item is removed from DOM, breaking focus restoration.

Logic: Added onModalHide to focus anchor button. NavigationFocusManager then captures this valid element instead of the removed menu item.


src/hooks/useAutoFocusInput.ts

Issue: Auto-focus ran on EVERY screen focus event, stealing focus on back navigation.

Logic: Added hasInitialFocused ref to skip auto-focus after initial mount. Initial focus still works; back navigation respects NavigationFocusManager.


src/components/MoneyRequestConfirmationList.tsx & src/pages/tasks/NewTaskPage.tsx

Issue: Unconditional blurActiveElement() cleared focus from buttons/menuitems.

Logic: Blur only INPUT/TEXTAREA (for keyboard dismissal). Buttons don't trigger keyboard, shouldn't be blurred.


src/hooks/useSyncFocus/useSyncFocusImplementation.ts

Issue: Could override restored focus by focusing a different element.

Logic: Added guard to skip focus() if another element (not body) is already focused.


src/components/MenuItem.tsx

Issue: role="menuitem" applied even when interactive={false}, causing incorrect tab navigation.

Logic: Made role, accessible, tabIndex conditional on interactive prop. Also ensures onPress is not passed when interactive={false} to allow event bubbling to parent wrappers. Fixes accessibility (display items shouldn't announce as "menu item").


Design Decisions

  • Focus: Defers to next tick for DOM stability. Same pattern used by focus-trap library (they use double setTimeout). All conditions verified BEFORE the callback , this is not the prohibited "racing unknown async" pattern.

  • useIsFocused over useNavigationState: Direct API for focus detection with better timing alignment. This is the React Navigation recommended approach for focus-aware logic.

  • State-based menuitem skip: Time-based protection (1s expiry) would fail for slow users. State-based checks element semantics (!element.closest('[role="menuitem"]')) and never expires.

  • wasNavigatedTo flag: Prevents [$250] mWeb – Chat – Blue frame appears around + button when open conversation link URL in a new tab #46109 (blue frame on new tab) by only restoring focus on actual back navigation, not initial page load.

  • App.tsx initialization location: Guarantees listeners attach before any navigation occurs. Enables proper cleanup for HMR and React StrictMode. Follows existing patterns (useDefaultDragAndDrop, OnyxUpdateManager).

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.

1 participant