# fix\(web\): return focus to the composer after closing a media preview · PR \#9513 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/9513)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: fix/image-preview-return-focus

Target branch: fix/composer-hold-for-selection

Head commit: f842ddfbf4b77c1eeb9e3e7523ebefd472aa2dd3

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/9513)

[Commits](https://git.cafe/pingdotgg/t3code/pull/9513/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/9513/diffs?format=markdown)

## Description

---

Reported: with the composer expanded and an attachment on it, opening the attachment keeps the composer up (correct), but closing the preview collapses it instead of leaving focus on the composer.

Opening the preview leaves focus on the thumbnail button, which is inside the composer, so the composer stays expanded. But interacting with the dialog — the close button, the image, a media control — moves focus into the portaled dialog, which is outside the composer's focus scope. When the dialog unmounts on close, focus falls to `<body>`; the composer's blur check sees nothing of its own focused and rests it. Reliable repro: attach an image with an empty prompt, open it, close it with the ✕.

Two changes to `ExpandedImageDialog`:
- It marks itself as a composer-owned floating layer (`composerFloatingLayerProps`), so focus landing on the close button or a media control while the preview is open counts as still inside the composer scope — the same mechanism already used for composer menus and popovers.
- On unmount it returns focus to whatever element opened it, so closing lands the caret back where the user was instead of on `<body>`.

Applies to image and video previews and every close path (✕, backdrop, Escape, or after interacting with the media). Stacked on [#9499](https://github.com/pingdotgg/t3code/pull/9499); the diff is independent.

## Before

Empty prompt + attachment, open the preview, close with ✕ → composer collapses to its resting line.

![before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/715a7b3b6dcaad34/before-preview.png)

https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2ac3f3888c0ee100/before-preview.mp4

## After

Same steps; the composer stays expanded with focus back on it.

![after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9e7af84889ac7b77/after-preview.png)

https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/dfb194be5223fd9f/after-preview.mp4

## Surfaces

- Web and desktop. The `composerEventScope` test already covers that a composer-owned floating layer counts as inside the composer scope.
- Manual pass against a copy of real data: on `main` the composer collapsed 254px → 84px on every close; with the fix it stays 254px across ✕, backdrop, Escape, and after clicking the media, for both image and video.

Claude Fable 5 via Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- Macroscope's pull request summary starts here -->
<!-- Macroscope will only edit the content between these invisible markers, and the markers themselves will not be visible in the GitHub rendered markdown. -->
<!-- If you delete either of the start / end markers from your PR's description, Macroscope will append its summary at the bottom of the description. -->
> [!NOTE]
> ### Restore focus to previously active element when `ExpandedImageDialog` closes
> Records the document's active element on mount and refocuses it on unmount using `preventScroll`, so closing a media preview returns focus to the composer. The portal root also now receives `composerFloatingLayerProps`.
>
> <!-- Macroscope's review summary starts here -->
>
> <sup><a href="https://app.macroscope.com">Macroscope</a> summarized f842ddf.</sup>
> <!-- Macroscope's review summary ends here -->
>
<!-- Macroscope's pull request summary ends here -->
