authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-03 17:04:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 17:04:39-07:00
logc0ebc882b88dbcffd5a712e403b3c3c517ba4ea6
tree6f98078d2cca37e9ec571df330ac6b7aa1acff86
parent522ebe65a542ace6f22d9ad95f95ef9ade7e87f1
signature Signed by PGP key B5690EEEBB952194

fix(web): return focus to the composer after closing a media preview (#9513)

Co-authored-by: Claude Code <noreply@anthropic.com>

1 files changed, 17 insertions(+), 1 deletions(-)

apps/web/src/components/chat/ExpandedImageDialog.tsx+17-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { memo, useCallback, useEffect, useState, type ReactNode } from "react";1import { memo, useCallback, useEffect, useRef, useState, type ReactNode } from "react";
2import { createPortal } from "react-dom";2import { createPortal } from "react-dom";
3import { ChevronLeftIcon, ChevronRightIcon, XIcon } from "lucide-react";3import { ChevronLeftIcon, ChevronRightIcon, XIcon } from "lucide-react";
4import { Button } from "../ui/button";4import { Button } from "../ui/button";
...@@ -9,6 +9,7 @@ import { OpenMediaLink } from "../media/OpenMediaLink";...@@ -9,6 +9,7 @@ import { OpenMediaLink } from "../media/OpenMediaLink";
9import { MediaActions, type MediaActionSource } from "../media/MediaActions";9import { MediaActions, type MediaActionSource } from "../media/MediaActions";
10import { MediaVideoPlayer } from "../media/MediaVideoPlayer";10import { MediaVideoPlayer } from "../media/MediaVideoPlayer";
11import { isContextMenuOpen } from "../../contextMenuFallback";11import { isContextMenuOpen } from "../../contextMenuFallback";
12import { composerFloatingLayerProps } from "./composerEventScope";
1213
13interface ExpandedImageDialogProps {14interface ExpandedImageDialogProps {
14 preview: ExpandedImagePreview;15 preview: ExpandedImagePreview;
...@@ -79,6 +80,20 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({...@@ -79,6 +80,20 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({
79 setImageOffset((current) => current + direction);80 setImageOffset((current) => current + direction);
80 }, []);81 }, []);
8182
83 // The element that opened the preview gets focus back on close. Without
84 // this a close button click leaves focus on the unmounted dialog, and the
85 // composer that owned the opener reads that as a blur and rests.
86 const openerRef = useRef<Element | null>(null);
87 useEffect(() => {
88 openerRef.current = document.activeElement;
89 return () => {
90 const opener = openerRef.current;
91 if (opener instanceof HTMLElement && opener.isConnected) {
92 opener.focus({ preventScroll: true });
93 }
94 };
95 }, []);
96
82 useEffect(() => {97 useEffect(() => {
83 const onKeyDown = (event: globalThis.KeyboardEvent) => {98 const onKeyDown = (event: globalThis.KeyboardEvent) => {
84 if (event.defaultPrevented || isContextMenuOpen()) {99 if (event.defaultPrevented || isContextMenuOpen()) {
...@@ -115,6 +130,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({...@@ -115,6 +130,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({
115130
116 return createPortal(131 return createPortal(
117 <div132 <div
133 {...composerFloatingLayerProps}
118 className="fixed inset-0 z-[60] flex items-center justify-center bg-black/75 px-4 py-6 [-webkit-app-region:no-drag]"134 className="fixed inset-0 z-[60] flex items-center justify-center bg-black/75 px-4 py-6 [-webkit-app-region:no-drag]"
119 role="dialog"135 role="dialog"
120 aria-modal="true"136 aria-modal="true"