Skip to content

Conversation

@mavrickdeveloper
Copy link

@mavrickdeveloper mavrickdeveloper commented Jan 18, 2026

Explanation of Change

  • This PR fixes the Web-only issue where keyboard navigation focus was lost across 48 scenarios/pages after navigating back in the app, causing screen readers to announce incorrect elements.
    adherence to WCAG 2.4.3 - Focus Order (Level A) , WCAG 3.2.3 Consistent Navigation (AA) , WCAG 2.1.2 No Keyboard Trap (A) & , WCAG 2.1.1 Keyboard (A)

  • Keyboard focus order , should not conflict with pre-existing custom focus baehavior

  • Notes to code reviewers: fix: [Web] Focus restoration mechanism on back navigation (#76921) #79834 (comment)

Root Cause:

Solution: Implement a custom NavigationFocusManager that:

Fixed Issues

$ #76921
PROPOSAL: #76921 (comment)

Prerequisite:

The user is logged in
Using Windows + Chrome, open expensify dev
Navigate using TAB only in the scenarios below
Navigate to the 'Back' button using TAB, press enter to activate
Observe the focus behavior , the correct behavior is the back button (in RHP for eg) should restore the keyboard focus to the triggering element

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 > Tap ESC keyboard button > 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 > TAB to three dots 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 > Use TAB to focus Back button > Press Enter
    • Expected behavior: Focus returns to previous step , "+" button
  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 room title (for eg: #admins) 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

Implements NavigationFocusManager to capture and restore focus when navigating
back to previous screens. Key changes:

- Add NavigationFocusManager singleton for focus capture/restoration
- Integrate with FocusTrapForScreen for seamless restoration
- Add guard in useSyncFocus to prevent focus stealing
- Fix MenuItem interactive prop to support display-only mode
- Fix useAutoFocusInput to only focus on initial mount
- Add selective blur for INPUT/TEXTAREA elements only

Includes 97 focus-related tests covering all scenarios.

Fixes Expensify#76921
@mavrickdeveloper
Copy link
Author

mavrickdeveloper commented Jan 18, 2026

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), tags with route key
  • FocusTrapForScreen: Restores focus via initialFocus callback
  • NavigationRoot: Cleans up focus data when routes are removed from navigation state

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

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. Each capture is tagged with the current route key for state-based validation.

Key features:

  • pointerdown/keydown capture-phase listeners
  • State-based menuitem skip (preserves anchor button for dropdowns)
  • Route-tagged captures (forRoute) for deterministic validation
  • cleanupRemovedRoutes() removes stale focus data when routes leave navigation state
  • Keyboard interaction tracking for modal auto-focus decisions

src/libs/Navigation/NavigationRoot.tsx

Issue: Focus data for removed routes was never cleaned up, causing potential memory leaks and stale restoration.

Logic: Calls NavigationFocusManager.cleanupRemovedRoutes(state) in handleStateChange. Route existence in navigation state is the source of truth,no timestamps needed.


src/components/ButtonWithDropdownMenu/index.tsx

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

Logic: Added keyboard interaction tracking via NavigationFocusManager.wasRecentKeyboardInteraction(). Focuses anchor button after popover closes. NavigationFocusManager then captures this valid element instead of the removed menu item.


src/components/ThreeDotsMenu/index.tsx

Issue: Menu opened via keyboard (Enter/Space) needed different focus handling than mouse clicks.

Logic: Captures keyboard interaction state BEFORE menu opens using NavigationFocusManager.wasRecentKeyboardInteraction(). Clears flag after capture to prevent stale state.


src/components/PopoverMenu.tsx

Issue: Focus restoration after popover close needed to respect keyboard vs mouse interaction.

Logic: Integrates with NavigationFocusManager for keyboard interaction tracking. Ensures proper focus restoration based on how the popover was opened.


src/components/ConfirmModal.tsx

Issue: Modal focus handling needed to distinguish keyboard vs pointer interactions.

Logic: Added keyboard interaction detection for proper auto-focus behavior. When opened via keyboard, modal content receives focus appropriately.


src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx

Issue: Composer focus behavior needed integration with NavigationFocusManager.

Logic: Added focus restoration integration to work with the navigation focus system.


tests/unit/libs/NavigationFocusManagerTest.tsx

Coverage: Comprehensive test suite with 82 tests covering:

  • State-based validation (route must be registered before capture is valid)
  • cleanupRemovedRoutes() removes focus data for removed routes
  • Keyboard interaction tracking
  • Element identifier matching for screen remounts
  • Menuitem protection for dropdown menus

Design Decisions

  • State-based validation over time-based: Route key tagging (forRoute) validates captures deterministically. No arbitrary timeouts that fail on slow devices. Route existence in navigation state is the source of truth.

  • cleanupRemovedRoutes() integration: Called from NavigationRoot on every state change. Automatically removes focus data when routes are removed (back navigation, tab switches). Prevents memory leaks and stale restoration.

  • Keyboard interaction tracking: wasRecentKeyboardInteraction() allows components to detect Enter/Space activation. Enables proper auto-focus behavior in modals opened via keyboard.

  • State-based menuitem skip: Checks element semantics (!element.closest('[role="menuitem"]')) rather than time expiry. Never expires, works for slow users.

  • Capture-phase listeners: {capture: true} ensures we run at T+0ms, before any React handlers or navigation logic. This is the only reliable way to capture focus before it moves.

@mavrickdeveloper mavrickdeveloper changed the title fix: ensure focus restoration on back navigation (#76921) fix: Focus restoration mechanism on back navigation (#76921) Jan 18, 2026
@mavrickdeveloper mavrickdeveloper changed the title fix: Focus restoration mechanism on back navigation (#76921) fix: [Web] Focus restoration mechanism on back navigation (#76921) Jan 18, 2026
…#76921)

Replace time-based validation with route-based validation in NavigationFocusManager.

Changes:
- Remove timestamp from CapturedFocus and ElementIdentifier types
- Tag captures with route key (forRoute) for deterministic validation
- Add cleanupRemovedRoutes() called from NavigationRoot on state change
- Add keyboard interaction tracking for modal focus handling
- Integrate focus restoration in PopoverMenu, ConfirmModal, ThreeDotsMenu,
  ButtonWithDropdownMenu, and ComposerWithSuggestions

This eliminates timing-dependent behavior on slow devices and ensures
focus data is cleaned up when routes are removed from navigation state.
@mavrickdeveloper mavrickdeveloper marked this pull request as ready for review January 21, 2026 18:53
@mavrickdeveloper mavrickdeveloper requested review from a team as code owners January 21, 2026 18:53
@melvin-bot
Copy link

melvin-bot bot commented Jan 21, 2026

@ikevin127 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot bot removed request for a team January 21, 2026 18:54
// Use startsWith for robustness against minor content changes
const candidateText = (candidate.textContent ?? '').slice(0, 100).trim();
if (identifier.textContentPreview && candidateText.startsWith(identifier.textContentPreview.slice(0, 20))) {
score += 30;
Copy link
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-2 (docs)

The magic number 20 in slice(0, 20) is used in the element matching logic without clear documentation or being defined as a named constant. This reduces code maintainability.

Suggested fix:
Define a constant at the top of the file:

const TEXT_CONTENT_PREFIX_LENGTH = 20; // Chars to match for fuzzy text comparison

Then use it in the code:

if (identifier.textContentPreview && candidateText.startsWith(identifier.textContentPreview.slice(0, TEXT_CONTENT_PREFIX_LENGTH))) {
    score += 30;
}
```\n\n---\n\nPlease rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Author

Choose a reason for hiding this comment

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

Will address

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50e2a4d6b7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +220 to +226
const computeInitialFocus = (() => {
const platform = getPlatform();

// Skip for mouse/touch opens or non-web platforms
if (!wasOpenedViaKeyboardRef.current || platform !== CONST.PLATFORM.WEB) {
return false;
}

Choose a reason for hiding this comment

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

P2 Badge Evaluate keyboard-opened modals after flag is captured

On the first render when isVisible flips to true, wasOpenedViaKeyboardRef.current is still undefined because it’s only set in the useLayoutEffect below. That means computeInitialFocus is computed as false and never re-evaluated (no state update), so a ConfirmModal opened via keyboard won’t auto-focus its first button on web. This is a regression for keyboard users because the focus trap never gets the intended initial target. Consider deferring the keyboard check to the initialFocus function itself or forcing a re-render after capturing the flag.

Useful? React with 👍 / 👎.

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