Я работаю над приложением Next.js, и в одном из компонентов у меня есть DropdownMenu
, где один из его элементов запускает DialogAlert
(оба компонента Shadcn). Я пытаюсь контролировать открытое состояние диалога с помощью setIsDialogOpen
, но это работает только тогда, когда перед ним есть синхронная функция (как в примере с кнопкой подтверждения). В противном случае это не сработает, если я просто позвоню setIsDialogOpen(false)
из действия кнопки отмены.
Вот код, воспроизводящий проблему:
"use client";
import { useState } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Loader2 } from "lucide-react";
export default function Home() {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleCancelDelete = () => {
setIsDialogOpen(false);
};
const handleDelete = async () => {
setIsLoading(true);
// Perform your delete operation here
await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulating an async operation
setIsLoading(false);
};
return (
<div>
<DropdownMenu>
<DropdownMenuTrigger>Dropdown</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Action</DropdownMenuItem>
<DropdownMenuItem
onClick = {() => setIsDialogOpen(true)}
onSelect = {(event) => event.preventDefault()}
>
<AlertDialog open = {isDialogOpen} onOpenChange = {setIsDialogOpen}>
<AlertDialogTrigger>Delete</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick = {() => {
alert("onClick works");
setIsDialogOpen(false);
}}
>
Cancel
</AlertDialogCancel>
{isLoading ? (
<Loader2 className = "animate-spin" />
) : (
<AlertDialogAction
onClick = {async (e) => {
e.preventDefault(); // Prevent default action that closes the dialog
await handleDelete();
setIsDialogOpen(false);
}}
>
Continue
</AlertDialogAction>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
Вот ссылка CodeSandbox, чтобы воспроизвести проблему: https://codesandbox.io/p/devbox/cranky-montalcini-4432z5?embed=1&file=%2Fapp%2Fpage.tsx%3A38%2C20
Код в песочнице, кажется, работает нормально. Можете ли вы отредактировать, чтобы прояснить проблему и каковы ваши шаги по воспроизведению для ее создания?
Извините, ребята, когда я нашел решение, я отредактировал код песочницы, чтобы проверить его, но забыл сделать то же самое здесь,
Проблема, с которой я столкнулся, связана с распространением событий в React. Когда вы нажимаете кнопку «Отмена», событие клика распространяется до родительского компонента (в данном случае DropdownMenuItem
).
Чтобы это исправить, мне пришлось остановить распространение события, вызвав event.stopPropagation()
в обработчике onClick
кнопки «Отмена». Вот обновленный код этой части:
<AlertDialogCancel
onClick = {(e) => {
e.stopPropagation(); // Add this line to stop event propagation
setIsDialogOpen(false);
}}
>
Cancel
</AlertDialogCancel>
Возможно, вы также хотите закрыть раскрывающийся список при нажатии кнопки отмены в диалоговом окне предупреждения. Для этого вам необходимо преобразовать раскрывающийся компонент из неконтролируемого в контролируемый, передав реквизиты open
и onChangeOpen
в раскрывающийся компонент.
export default function Home() {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleCancelDelete = () => {
setIsDialogOpen(false);
};
const handleDelete = async () => {
setIsLoading(true);
// Perform your delete operation here
await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulating an async operation
setIsLoading(false);
};
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
return (
<div>
<DropdownMenu open = {isDropDownOpen} onOpenChange = {() => {
setIsDropDownOpen(!isDropDownOpen);
}}>
<DropdownMenuTrigger>Dropdown</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Action</DropdownMenuItem>
<DropdownMenuItem
onClick = {() => setIsDialogOpen(true)}
onSelect = {(event) => event.preventDefault()}
>
<AlertDialog open = {isDialogOpen} onOpenChange = {setIsDialogOpen}>
<AlertDialogTrigger>Delete</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick = {(e) => {
e.stopPropagation(); // Add this line to stop event propagation
setIsDropDownOpen(false);
setIsDialogOpen(false);
}}
>
Cancel
</AlertDialogCancel>
{isLoading ? (
<Loader2 className = "animate-spin" />
) : (
<AlertDialogAction
onClick = {async (e) => {
e.preventDefault(); // Prevent default action that closes the dialog
await handleDelete();
setIsDialogOpen(false);
}}
>
Continue
</AlertDialogAction>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
Приятно, спасибо чувак! мне это действительно нужно
Реализация предлагаемого решения вызвала проблему, из-за которой все компоненты в моем приложении стали неактивными и зависли после запуска раскрывающегося меню, запуска действия удаления, его отмены и последующей прокрутки вниз. Обратите внимание, что у меня есть прокручиваемый компонент с несколькими компонентами, использующими это раскрывающееся меню. Есть ли у вас какие-нибудь мысли по этому поводу?
Такой пункт меню, как «Действие», не будет доступен для кликов, вам необходимо явно установить DropDown(false) onClick для всех остальных пунктов меню.
В вашей ссылке CodeSandbox, когда я нажимаю кнопку отмены, диалоговое окно закрывается. Разве это не ожидаемый результат?