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 (
-
-
-
-
-
-
-
{t("title", { name: collectionName })}
-
-
-
-
-
+ // The shared dialog shell (R4): focus moves in and comes back, Escape
+ // and the close X work from anywhere in it.
+ }
+ size="lg"
+ footer={}
+ >
+