Skip to content

[Due for payment 2026-11-19] [$175] Web - Chat - Composer doesn't regain focus after closing a modal once a message has been edited #105111

Open

Description

@MelvinBot

Action Performed

  1. Open the app in a desktop browser.
  2. Go to any chat.
  3. Send a message.
  4. Click the chat header to open a modal, then close it. Verify that the composer automatically regains focus.
  5. Right-click the message > Edit comment > Cancel or Save.
  6. Click the chat header again to open the modal, then close it.
  7. Notice that the composer doesn't regain focus.

Expected Result

The composer should automatically regain focus after the modal is closed.

Actual Result

The composer doesn't regain focus after the modal is closed.

Platforms

  • Web (desktop browser)

Reproducible on staging. Not caused by [LegendList 4/9] fix: stabilize composer focus and scroll notifications.

Video

2026-10-27.11-18-49.mp4

Context

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @dmkt9

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 27, 2026
  2. dmkt9 commented on Oct 27, 2026

    @dmkt9
    Contributor

    @MelvinBot submit a proposal

  3. MelvinBot commented on Oct 27, 2026

    @MelvinBot
    ContributorAuthor

    Proposal

    What is the root cause of that problem?

    The edit composer leaves the INPUT_FOCUSED Onyx flag set to true after it unmounts.

    1. When the edit composer gets focus, composerFocusKeepFocusOn sets INPUT_FOCUSED to true (InputFocus/index.web.ts:17-20, called from ReportActionItemMessageEdit.tsx:172-174).
    2. Cancel or Save unmounts the edit composer. Its unmount cleanup never resets the flag (ReportActionItemMessageEdit.tsx:176-190).
    3. The main composer is then focused in code, not by a click (useEditComposerToggle.ts:97-100). The flag is only cleared on a real click, so it stays true.
    4. When the modal closes, the main composer's refocus effect sees editFocused === true. It assumes an edit composer still owns focus, so it clears the flag and returns without calling focus(true) (ComposerWithSuggestions.tsx:934-938). The effect runs again after the flag changes, but by then the "modal just closed" check at line 930 no longer passes.

    What changes do you think we should make in order to solve the problem?

    In the unmount cleanup of ReportActionItemMessageEdit, call inputFocusChange(false) before the existing early return. Once the edit composer is gone, it can't own focus, so the flag must not block the main composer.

    inputFocusChange does nothing on native, so this only changes web.

    Related cleanup (optional)

    The same cleanup compares editComposerRef.current with composerRef.current. The inline ref callback sets composerRef.current to null during unmount, before the cleanup runs. So the check always returns early, and ReportActionComposeFocusManager.clear(true) never runs. Capture composerRef.current in a local variable when the effect runs and compare against that.

    What alternative solutions did you explore? (Optional)

    Clear INPUT_FOCUSED in the main composer's handleFocus when editFocused is true. Then any focus on the main composer resets the flag, not just a click. This is broader and may affect the case where an edit composer is still mounted.

    Verification notes

    This analysis comes from reading the code at 1279b3e. I haven't confirmed it in a browser. To check, log editFocused, prevIsFocused and prevIsModalVisible in the refocus effect after Cancel → open the header → close.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/105111) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 27, 2026
  5. melvin-bot commented on Oct 27, 2026

    @melvin-bot

    Current assignee @dmkt9 is eligible for the External assigner, not assigning anyone new.

  6. changed the title [-]Web - Chat - Composer doesn't regain focus after closing a modal once a message has been edited[/-] [+][$175] Web - Chat - Composer doesn't regain focus after closing a modal once a message has been edited[/+] on Oct 27, 2026
  7. melvin-bot commented on Oct 27, 2026

    @melvin-bot
  8. camesenin commented on Oct 27, 2026

    @camesenin
  9. melvin-bot commented on Oct 27, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  10. huhuli083-creator commented on Oct 27, 2026

    @huhuli083-creator

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Web (wide layout), after a user edits an existing chat message (and saves with Enter or cancels with Escape), the main chat composer regains focus. However, if the user then opens any modal (such as the emoji picker or attachment menu) and closes it without selecting an item, the main chat composer fails to regain focus.

    What is the root cause of that problem?

    There are three interacting bugs in how web input focus state (ONYXKEYS.INPUT_FOCUSED / editFocused), cached DOM focus refs (refSave), and edit-composer unmount cleanup are managed across ReportActionItemMessageEdit.tsx, ComposerWithSuggestions.tsx, and InputFocus/index.web.ts:

    1. ONYXKEYS.INPUT_FOCUSED (editFocused) is set to true when focusing the edit composer, and is never reset when the edit composer closes or when the main composer regains focus programmatically:

      • In src/pages/inbox/report/ReportActionItemMessageEdit.tsx (lines 167–169), a useEffect runs on wide layout whenever isFocused is true and calls composerFocusKeepFocusOn(composerRef.current, isFocused, modal, onyxInputFocused).
      • In src/libs/actions/InputFocus/index.web.ts (lines 14–22), when isFocused is true, composerFocusKeepFocusOn calls inputFocusChange(true) (which merges ONYXKEYS.INPUT_FOCUSED = true) and stores the edit composer DOM node in the module-level variable refSave = ref.
      • When the user finishes editing (via Enter in publishDraft or Escape in triggerSaveOrCancel), deleteReportActionDraft removes the draft and ReportActionItemMessageEdit unmounts without resetting inputFocusChange(false).
      • Next, src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts (lines 89–98) detects that editing ended (wasEditingRef.current && !isEditing) and calls onFocus?.() (handleEditFocus in src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx, lines 326–329), which calls focus(true) on the main composer.
      • Focusing the main composer triggers handleFocus (ComposerWithSuggestions.tsx, lines 1006–1010), which sets isFocused = true (setIsCommentEmpty / ReportActionComposeFocusManager.onComposerFocus), but does not clear ONYXKEYS.INPUT_FOCUSED (inputFocusChange(false)). In ComposerWithSuggestions.tsx (lines 738–744), inputFocusChange(false) is only called inside setShouldBlockSuggestionCalcToFalse, which is bound to onClick on the main composer (line 1043). Because focus was restored programmatically via useEditComposerToggle (or via keyboard navigation) rather than a physical mouse click on the main composer, ONYXKEYS.INPUT_FOCUSED (editFocused) remains true even while the user is actively focused in the main composer.
    2. Closing a modal checks editFocused and aborts main-composer focus restoration:

      • When a modal (such as the emoji picker) opens and then closes (prevIsModalVisible && !isModalVisible), the useEffect in src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx (lines 928–943) runs to restore focus to the main composer.
      • At lines 934–937, it checks:
        if (editFocused) { inputFocusChange(false); return; }
      • Because editFocused (ONYXKEYS.INPUT_FOCUSED) is still true from the earlier message edit, this branch clears ONYXKEYS.INPUT_FOCUSED and returns early instead of calling focus(true) on the main composer. Meanwhile, ReportActionItemMessageEdit is already unmounted, so nothing focuses the edit composer either—leaving neither composer focused.
    3. ReportActionItemMessageEdit's unmount cleanup fails to clear editComposerRef because its inline callback ref sets composerRef.current = null before useEffect cleanup runs:

      • In src/pages/inbox/report/ReportActionItemMessageEdit.tsx (lines 312–320), <Composer> receives an inline callback ref: ref={(el: ComposerRef | null) => { composerRef.current = el; ... }} that only updates ReportActionComposeFocusManager.editComposerRef.current = el when el !== null.
      • During unmount, React invokes callback refs with null before running useEffect cleanup functions. Thus composerRef.current becomes null while ReportActionComposeFocusManager.editComposerRef.current still points to the old ComposerRef instance.
      • When the unmount cleanup in ReportActionItemMessageEdit.tsx (lines 171–184) subsequently runs, the guard if (ReportActionComposeFocusManager.editComposerRef.current !== composerRef.current) { return; } compares the old ComposerRef against null, evaluates to true, and returns early—so ReportActionComposeFocusManager.clear(true) is never executed and editComposerRef.current remains a stale reference to the unmounted edit composer.

    What changes do you think we should make in order to solve the problem?

    1. Clear ONYXKEYS.INPUT_FOCUSED whenever the main composer gains focus (src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx):

      • In handleFocus (ComposerWithSuggestions.tsx, lines 1006–1010), call inputFocusChange(false) whenever editFocused is true (or unconditionally on main composer focus). This guarantees that whenever the main composer becomes the active focused input—whether via mouse click, useEditComposerToggle after saving/canceling an edit, or keyboard Tab/hotkey while an edit composer is still mounted—ONYXKEYS.INPUT_FOCUSED accurately reflects that the edit composer is no longer the focused input.
    2. Fix the unmount cleanup and ref assignment in src/pages/inbox/report/ReportActionItemMessageEdit.tsx (lines 171–184 and 312–320):

      • Extract the <Composer> ref callback into a useCallback (or capture the mounted composer instance in a local variable / ref that isn't overwritten before cleanup) so that on unmount:
        • ReportActionComposeFocusManager.clear(true) is properly called when this edit composer owned editComposerRef.current.
        • If onyxInputFocused is true (or this edit composer is the active input), call inputFocusChange(false) so ONYXKEYS.INPUT_FOCUSED is not left true after ReportActionItemMessageEdit unmounts (for example, on narrow/mobile web layouts where useEditComposerToggle does not auto-focus the main composer).
    3. Clear refSave in src/libs/actions/InputFocus/index.web.ts when inputFocusChange(false) is called or when the saved element is disconnected:

      • In src/libs/actions/InputFocus/index.web.ts (lines 10–32), reset refSave = null inside inputFocusChange(false) (or guard refSave.isConnected before calling refSave.focus()) so composerFocusKeepFocusOn never attempts to focus a detached DOM node from an unmounted edit composer.

    What alternative solutions did you explore? (Optional)

    • Only calling inputFocusChange(false) in ReportActionItemMessageEdit unmount cleanup: While resetting inputFocusChange(false) on unmount fixes the case where the edit composer is closed before opening a modal, it does not fix the case where a user starts editing a message (leaving the edit box open), focuses the main composer via keyboard/shortcut, and then opens and closes a modal. Clearing inputFocusChange(false) inside the main composer's handleFocus in ComposerWithSuggestions.tsx in addition to cleaning up on ReportActionItemMessageEdit unmount handles both flows cleanly.
  11. melvin-bot commented on Oct 27, 2026

    @melvin-bot

    📣 @huhuli083-creator! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  12. khizzcr7 commented on Oct 27, 2026

    @khizzcr7

    Proposal

    What is the root cause of that problem?

    The edit composer sets the INPUT_FOCUSED Onyx flag to true on focus (src/libs/actions/InputFocus/index.web.ts:17-20, called from src/pages/inbox/report/ReportActionItemMessageEdit.tsx:172-174), and its unmount cleanup never clears it (ReportActionItemMessageEdit.tsx:176-190). The main composer is then focused in code, not by a click (src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts:97-100), and the flag is only cleared on a real click, so it stays true. When the modal closes, the main composer's refocus effect (src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx:934-938) sees editFocused === true, assumes an edit composer still owns focus, clears the flag and returns without calling focus(true). By the time the effect runs again, the "modal just closed" check at line 930 no longer passes, so focus is lost for good.

    What changes do you think we should make in order to solve the problem?

    • In useEditComposerToggle's programmatic focus path, call inputFocusChange(false) right after focusing the main composer, so the stale edit flag never reaches the modal-close effect. This fixes the trap at the consumer instead of only in one component's cleanup: any other programmatic-focus path after an edit would hit the same stale flag.
    • Keep the modal-close effect's early return for a genuinely mounted edit composer; with the stale flag cleared it will no longer trigger on a dead edit session.

    What will I verify?

    • Send a message, open and close the chat-header modal, and confirm the composer regains focus.
    • Edit a message, then cancel or save, open and close the modal again, and confirm the composer regains focus.
    • Confirm edit-composer focus still works while actually editing a message.
  13. 14 remaining items

  14. dmkt9 commented on Nov 5, 2026

    @dmkt9
    Contributor
  15. MelvinBot commented on Nov 5, 2026

    @MelvinBot
    ContributorAuthor

    🤖 Web test steps verification results: ⚠️ partial

    Verified desktop web chat, sending, and normal header-dismissal focus. Edit-specific patch behavior remains unverified because the supported web CLI cannot dispatch ArrowUp.

    Main composer received untargeted typing after header dismissal

    Step Status
    Baseline: send a messageConfirmed the own message appeared in chat.
    ✅
    Baseline: composer automatically focused after header dismissalUntargeted typing entered the composer after closing Details.
    Baseline composer receives typing after header dismissal
    ✅
    Baseline: Edit comment → Cancel and Save → reopen/close headerTooling blocked: secondary-click is unsupported; hovering exposed no edit actions. Not an observed App failure.
    ❌
    Patched: desktop web chat opensChat, messages, header, and composer were visible.
    Open desktop chat
    ✅
    Patched: sending own message worksSent the verification message and confirmed it appeared.
    Chat containing newly sent verification message
    ✅
    Patched: header open/close restores main composer focusUntargeted typing entered the main composer after dismissing Details.
    Main composer received untargeted typing after header dismissal
    ✅
    Patched: Cancel editing, then header dismissal restores focusTooling blocked: ArrowUp returned INVALID_ARGS. This flow remains unverified.
    ❌
    Patched: Save edited text, then header dismissal restores focusTooling blocked by inability to enter editing.
    ❌
    Patched: modal dismissal preserves active edit focusTooling blocked by inability to enter editing.
    ❌

    Implemented the approved minimal cleanup in Fix web composer focus after finishing message edits, created as a draft and assigned to dmkt9.

    The new regression test fails without the fix and passes with it. Full Jest: 37,121 tests passed, 146 skipped. Lint, typecheck, spelling, and Storybook smoke checks passed. Existing React Compiler errors and unavailable formatting checks are documented in the PR.

    Please complete browser verification of Cancel/Save and mark the PR ready for review, or ask me to mark it ready when verification is complete.


    view run · no recording available

  16. added
    ReviewingHas a PR in review
    and removed on Nov 5, 2026
  17. melvin-bot commented on Nov 5, 2026

    @melvin-bot

    📣 @dmkt9 We're missing your Upwork ID to automatically send you an offer for the Reviewer role.
    Once you apply to the Upwork job, your Upwork ID will be stored and you will be automatically hired for future jobs!

  18. jeffrey-graham commented on Nov 7, 2026

    @jeffrey-graham
  19. melvin-bot commented on Nov 7, 2026

    @melvin-bot
  20. changed the title [-][$175] Web - Chat - Composer doesn't regain focus after closing a modal once a message has been edited[/-] [+][Due for payment 2026-11-19] [$175] Web - Chat - Composer doesn't regain focus after closing a modal once a message has been edited[/+] on Nov 12, 2026
  21. melvin-bot commented on Nov 12, 2026

    @melvin-bot

    @dmkt9

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.36-0 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-11-19. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

  22. melvin-bot commented on Nov 12, 2026

    @melvin-bot

    @dmkt9 Huh... This is 4 days overdue. Who can take care of this?

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

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorOverdue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions