feat(ui): use native mobile confirmation sheets (#3621)

This commit is contained in:
José Almeida
2026-09-12 14:58:49 +02:00
committed by GitHub
parent e598742110
commit 3f33ce0451
171 changed files with 7570 additions and 1428 deletions
+73 -13
View File
@@ -14,6 +14,7 @@ import {
import { cn } from "@/lib/utils";
import { useToast } from "@/components/toast-provider";
import { ActionConfirmPopover } from "@/components/confirmation/action-confirm-popover";
import { MobileActionConfirmation } from "@/components/confirmation/mobile-action-confirmation";
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
import { useTranslation } from "react-i18next";
import type { FileTreeNode } from "@/lib/types/backend";
@@ -229,7 +230,10 @@ function FileContextMenuSurface({
onConfirmDelete,
onDelete,
}: FileContextMenuSurfaceProps) {
const { isMobile } = useResponsiveBreakpoint();
const renamePendingRef = useRef(false);
const deletePendingRef = useRef(false);
const touchTriggerRef = useRef<HTMLButtonElement>(null);
const handleStartRename = useCallback(() => {
renamePendingRef.current = true;
@@ -237,16 +241,24 @@ function FileContextMenuSurface({
const handleCloseAutoFocus = useCallback(
(event: Event) => {
if (deletePendingRef.current) {
event.preventDefault();
deletePendingRef.current = false;
setDeleteConfirmationOpen(true);
return;
}
if (!renamePendingRef.current) return;
event.preventDefault();
renamePendingRef.current = false;
onStartRename();
},
[onStartRename],
[onStartRename, setDeleteConfirmationOpen],
);
return (
<FileDeleteActionContext.Provider value={deleteAction}>
<FileDeleteActionContext.Provider
value={deleteAction ? { ...deleteAction, triggerRef: touchTriggerRef } : null}
>
<ContextMenu>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
<ContextMenuContent onCloseAutoFocus={handleCloseAutoFocus}>
@@ -265,7 +277,10 @@ function FileContextMenuSurface({
onDownloadFile={onDownloadFile}
onUploadFilesHere={onUploadFilesHere}
onStartRename={handleStartRename}
onDelete={onDelete}
onDelete={(event) => {
if (isMobile && !isBulk) deletePendingRef.current = true;
else onDelete(event);
}}
/>
</ContextMenuContent>
</ContextMenu>
@@ -274,20 +289,16 @@ function FileContextMenuSurface({
<DeleteConfirmDialog selectedCount={selectedCount} onConfirm={onConfirmDelete} />
</AlertDialog>
)}
{!isBulk && isFinePointer && deleteAction && (
<ActionConfirmPopover
{!isBulk && deleteAction && (
<FileDeleteConfirmation
action={deleteAction}
open={deleteConfirmationOpen}
path={node.path}
isFinePointer={isFinePointer}
anchorRef={anchorRef}
focusBoundaryRef={focusBoundaryRef}
title={deleteAction.title}
description={deleteAction.description}
cancelLabel={deleteAction.cancelLabel}
confirmLabel={deleteAction.label}
confirmAriaLabel={deleteAction.title}
confirmTestId="file-delete-confirm"
testId="file-delete-confirm-popover"
focusReturnRef={touchTriggerRef.current ? touchTriggerRef : anchorRef}
onOpenChange={setDeleteConfirmationOpen}
onCancel={() => setDeleteConfirmationOpen(false)}
onConfirm={onConfirmDelete}
/>
)}
@@ -295,6 +306,55 @@ function FileContextMenuSurface({
);
}
function FileDeleteConfirmation({
action,
path,
isFinePointer,
anchorRef,
focusReturnRef,
focusBoundaryRef,
...props
}: {
action: FileDeleteAction;
path: string;
isFinePointer: boolean;
anchorRef: React.RefObject<HTMLElement | null>;
focusReturnRef: React.RefObject<HTMLElement | null>;
focusBoundaryRef: React.RefObject<HTMLElement | null>;
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
}) {
const actions = {
...props,
title: action.title,
description: action.description,
cancelLabel: action.cancelLabel,
confirmLabel: action.label,
confirmAriaLabel: action.title,
confirmTestId: "file-delete-confirm",
};
return (
<MobileActionConfirmation
{...actions}
targetKey={path}
subject={path.includes("/") ? path : undefined}
focusReturnRef={focusReturnRef}
fallback={
isFinePointer ? (
<ActionConfirmPopover
{...actions}
anchorRef={anchorRef}
focusBoundaryRef={focusBoundaryRef}
testId="file-delete-confirm-popover"
onCancel={() => props.onOpenChange(false)}
/>
) : null
}
/>
);
}
/** Context menu for file nodes with Download, Rename, and Delete options */
export function FileContextMenu({
children,