From f5f536f7c92c9769a4a6721f15652470ada12bbf Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:29:23 +0100 Subject: [PATCH] fix: one kind of button in the Files selection bar, a closable details panel on tablets, and a share dialog that takes the focus --- .../file-browser-shared-folders.test.tsx | 8 +++ components/files/file-browser.tsx | 9 ++- components/files/files-header.tsx | 9 +-- .../share-collection-dialog.test.tsx | 9 +++ .../settings/share-collection-dialog.tsx | 56 +++++-------------- 5 files changed, 45 insertions(+), 46 deletions(-) diff --git a/components/files/__tests__/file-browser-shared-folders.test.tsx b/components/files/__tests__/file-browser-shared-folders.test.tsx index e9502f9f6..7a82a2fc8 100644 --- a/components/files/__tests__/file-browser-shared-folders.test.tsx +++ b/components/files/__tests__/file-browser-shared-folders.test.tsx @@ -462,4 +462,12 @@ describe('FileBrowser', () => { expect(shared).toEqual(inList.filter((l) => inTree.includes(l))); expect(shared).toEqual(expect.arrayContaining(['open', 'star', 'rename', 'move_to', 'download_zip', 'delete'])); }); + + it("closes the details panel over a narrow list with Escape", async () => { + renderBrowser({ props: { selectedResources: new Set(['notes.txt']) } }); + fireEvent.click(screen.getByRole('button', { name: 'details' })); + expect(screen.getByRole('button', { name: 'details' })).toHaveAttribute('aria-pressed', 'true'); + fireEvent.keyDown(window, { key: 'Escape' }); + expect(screen.getByRole('button', { name: 'details' })).toHaveAttribute('aria-pressed', 'false'); + }); }); diff --git a/components/files/file-browser.tsx b/components/files/file-browser.tsx index 6117bd7e5..e6088cb15 100644 --- a/components/files/file-browser.tsx +++ b/components/files/file-browser.tsx @@ -745,7 +745,9 @@ export function FileBrowser({ return; } if (e.key === "Escape") { - if (searchQuery) setSearchQuery(""); + // The details panel over the list closes first (innermost layer, R8). + if (showDetails && isNarrow) toggleDetails(false); + else if (searchQuery) setSearchQuery(""); else if (selected.length > 0) onClearSelection(); return; } @@ -796,6 +798,7 @@ export function FileBrowser({ return () => window.removeEventListener("keydown", handler); }, [ showNewFolder, showNewTextFile, renameTarget, renameFolder, shareTargetId, moveTarget, accountPickerMode, selectedList, singleSelected, inTrash, + showDetails, isNarrow, toggleDetails, selectAllVisible, onCopy, onCut, onPaste, clipboard, searchQuery, onClearSelection, deleteNames, openResource, focusedName, onToggleSelect, currentPath, navigateUp, viewMode, gridColumns, moveFocus, ]); @@ -1294,6 +1297,10 @@ export function FileBrowser({ )} + {/* Over the list on a narrow pane: a backdrop that closes it, as for the drawer. */} + {detailsPanel && isNarrow && ( +
toggleDetails(false)} /> + )} {detailsPanel} {isNarrow && canCreate && !selectionActions && ( diff --git a/components/files/files-header.tsx b/components/files/files-header.tsx index 23bfc4849..e9398f54b 100644 --- a/components/files/files-header.tsx +++ b/components/files/files-header.tsx @@ -135,20 +135,21 @@ export function FilesHeader({ {selection.trash ? ( <> {selection.onRestore && ( - + )} ) : ( <> + {/* All icons with a tooltip, as in Mail's selection bar: one kind of button. */} {selection.canOpen && ( - + )} {selection.fileCount > 0 && ( - + )} {selection.canShare && ( - + )} {selection.canRename && ( diff --git a/components/settings/__tests__/share-collection-dialog.test.tsx b/components/settings/__tests__/share-collection-dialog.test.tsx index 46198efc7..9bf721f2d 100644 --- a/components/settings/__tests__/share-collection-dialog.test.tsx +++ b/components/settings/__tests__/share-collection-dialog.test.tsx @@ -160,3 +160,12 @@ describe('ShareCollectionDialog directory', () => { expect(screen.queryByText('no_principals')).toBeNull(); }); }); + +describe('ShareCollectionDialog focus', () => { + it('takes the focus when it opens and has a named close button', async () => { + renderDialog(fullDirectory()); + const dialog = screen.getByRole('dialog'); + await waitFor(() => expect(dialog.contains(document.activeElement)).toBe(true)); + expect(screen.getAllByRole('button', { name: 'close' }).length).toBeGreaterThan(0); + }); +}); diff --git a/components/settings/share-collection-dialog.tsx b/components/settings/share-collection-dialog.tsx index e007e0c22..a9412dc62 100644 --- a/components/settings/share-collection-dialog.tsx +++ b/components/settings/share-collection-dialog.tsx @@ -1,10 +1,11 @@ "use client"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Avatar } from "@/components/ui/avatar"; -import { X, Loader2, UserPlus, Trash2, Users, ChevronDown } from "@/components/icons"; +import { Loader2, UserPlus, Trash2, ChevronDown, Share2 } from "@/components/icons"; +import { Dialog } from "@/components/ui/dialog"; import type { IJMAPClient } from "@/lib/jmap/client-interface"; import type { Principal, PrincipalDirectoryStatus, CalendarRights, AddressBookRights, FileNodeRights, MailboxRights } from "@/lib/jmap/types"; import { loadPrincipalDirectory } from "@/lib/jmap/principal-directory"; @@ -151,7 +152,6 @@ export function ShareCollectionDialog({ }: ShareCollectionDialogProps) { const t = useTranslations("sharing"); const tCommon = useTranslations("common"); - const modalRef = useRef(null); const [allPrincipals, setAllPrincipals] = useState([]); const [directoryStatus, setDirectoryStatus] = useState("ok"); const [directoryQueriesOff, setDirectoryQueriesOff] = useState(false); @@ -189,15 +189,6 @@ export function ShareCollectionDialog({ return allPrincipals.filter((p) => p.id !== ownAccountId && !existing.has(p.id)); }, [allPrincipals, ownAccountId, shareWith]); - // Close on Escape, focus trap, click outside - useEffect(() => { - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - document.addEventListener("keydown", onKey); - return () => document.removeEventListener("keydown", onKey); - }, [onClose]); - const handleSetRights = async (principalId: string, preset: RolePreset) => { if (preset === "custom") return; // custom is read-only here const rights = presetRights(kind, preset); @@ -260,30 +251,17 @@ export function ShareCollectionDialog({ : ["read", "readWrite", "manager"] as const; return ( -
-