mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 00:13:37 +00:00
fix: one kind of button in the Files selection bar, a closable details panel on tablets, and a share dialog that takes the focus
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Over the list on a narrow pane: a backdrop that closes it, as for the drawer. */}
|
||||
{detailsPanel && isNarrow && (
|
||||
<div className="absolute inset-0 z-20 bg-black/30" aria-hidden onClick={() => toggleDetails(false)} />
|
||||
)}
|
||||
{detailsPanel}
|
||||
|
||||
{isNarrow && canCreate && !selectionActions && (
|
||||
|
||||
@@ -135,20 +135,21 @@ export function FilesHeader({
|
||||
{selection.trash ? (
|
||||
<>
|
||||
{selection.onRestore && (
|
||||
<ToolButton icon={RotateCcw} label={t("restore")} onClick={selection.onRestore} compact={compact} />
|
||||
<ToolButton icon={RotateCcw} label={t("restore")} onClick={selection.onRestore} compact />
|
||||
)}
|
||||
<ToolButton icon={Trash2} label={t("delete_forever")} onClick={selection.onDelete} compact destructive />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* All icons with a tooltip, as in Mail's selection bar: one kind of button. */}
|
||||
{selection.canOpen && (
|
||||
<ToolButton icon={FolderOpen} label={t("open")} onClick={selection.onOpen} compact={compact} />
|
||||
<ToolButton icon={FolderOpen} label={t("open")} onClick={selection.onOpen} compact />
|
||||
)}
|
||||
{selection.fileCount > 0 && (
|
||||
<ToolButton icon={Download} label={t("download")} onClick={selection.onDownload} compact={compact} />
|
||||
<ToolButton icon={Download} label={t("download")} onClick={selection.onDownload} compact />
|
||||
)}
|
||||
{selection.canShare && (
|
||||
<ToolButton icon={Share2} label={t("share")} onClick={selection.onShare} compact={compact} />
|
||||
<ToolButton icon={Share2} label={t("share")} onClick={selection.onShare} compact />
|
||||
)}
|
||||
{selection.canRename && (
|
||||
<ToolButton icon={Pencil} label={t("rename")} onClick={selection.onRename} compact />
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const [allPrincipals, setAllPrincipals] = useState<Principal[]>([]);
|
||||
const [directoryStatus, setDirectoryStatus] = useState<PrincipalDirectoryStatus>("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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-[1px]" onClick={onClose} aria-hidden="true" />
|
||||
<div
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("title", { name: collectionName })}
|
||||
className="relative bg-background border border-border rounded-lg shadow-xl w-full max-w-lg mx-4 animate-in zoom-in-95 duration-200 max-h-[85vh] flex flex-col"
|
||||
>
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">{t("title", { name: collectionName })}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-md hover:bg-muted transition-colors duration-150 text-muted-foreground hover:text-foreground"
|
||||
aria-label={tCommon("close")}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 space-y-4 overflow-y-auto">
|
||||
// The shared dialog shell (R4): focus moves in and comes back, Escape
|
||||
// and the close X work from anywhere in it.
|
||||
<Dialog
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
title={t("title", { name: collectionName })}
|
||||
icon={<Share2 className="w-5 h-5 text-primary shrink-0 mt-1" />}
|
||||
size="lg"
|
||||
footer={<Button onClick={onClose}>{tCommon("close")}</Button>}
|
||||
>
|
||||
<div className="pt-4 space-y-4">
|
||||
<p className="text-sm text-muted-foreground">{t("description")}</p>
|
||||
|
||||
{sharedEntries.length === 0 && !showAdd && (
|
||||
@@ -440,10 +418,6 @@ export function ShareCollectionDialog({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-border">
|
||||
<Button onClick={onClose}>{tCommon("close")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user