HeroUI Version
@heroui/react@3.0.1
Describe the bug
When a controlled Dropdown is closed inside onAction and an AlertDialog is opened immediately after, the dropdown remains visible on large/desktop screens. The issue does not reproduce consistently on mobile or small screens.
All straightforward approaches to close the dropdown before opening the dialog fail on large screens. The suspected root cause is React Aria's async focus restoration — when the dropdown closes, React Aria restores focus to the trigger asynchronously, which appears to interfere with or re-open the controlled dropdown before the AlertDialog mounts and traps focus.
Your Example Website or App
No response
Steps to Reproduce the Bug or Issue
- Create a controlled
Dropdown using isOpen / onOpenChange
- Inside
onAction, call dropdown.close() then open an AlertDialog
- View on a large/desktop screen
- Click the dropdown trigger, then click the action item
"use client";
import {
Description,
Dropdown,
Header,
Label,
type UseOverlayStateReturn,
useOverlayState,
} from "@heroui/react";
import { CiTrash } from "react-icons/ci";
import { IoMdMore } from "react-icons/io";
import type { Client } from "@/features/clients/types/client";
import DeleteClientAlertDialog from "@/features/clients/ui/delete-client-alert-dialog";
type Props = {
client: Client;
};
function ClientAction({ client }: Props) {
const dropdown: UseOverlayStateReturn = useOverlayState();
const state: UseOverlayStateReturn = useOverlayState();
return (
<>
<Dropdown isOpen={dropdown.isOpen} onOpenChange={dropdown.setOpen}>
<Dropdown.Trigger
className="button button-md button--ghost button--icon-only data-[focus-visible=true]:status-focused"
aria-label={`More actions for client ${client.email}`}
aria-haspopup="menu"
>
<IoMdMore aria-hidden="true" />
</Dropdown.Trigger>
<Dropdown.Popover placement="bottom end">
<Dropdown.Menu
aria-label={`Actions for ${client.email}`}
onAction={(key) => {
if (key === "delete") {
dropdown.close();
state.open();
}
}}
>
<Dropdown.Section>
<Header>Actions</Header>
<Dropdown.Item
id="delete"
textValue="Delete Client"
variant="danger"
className="text-danger"
aria-label={`Delete client ${client.email}`}
>
<CiTrash aria-hidden="true" />
<div className="flex flex-col">
<Label>Delete</Label>
<Description className="text-xs">
Permanently removes this client.
</Description>
</div>
</Dropdown.Item>
</Dropdown.Section>
</Dropdown.Menu>
</Dropdown.Popover>
</Dropdown>
<DeleteClientAlertDialog client={client} state={state} />
</>
);
}
export { ClientAction as default };
"use client";
import { AlertDialog, Button, type UseOverlayStateReturn } from "@heroui/react";
import { useDeleteClient } from "@/features/clients/queries/client.q";
import type { Client } from "@/features/clients/types/client";
type Props = {
client: Client;
state: UseOverlayStateReturn;
};
function DeleteClientAlertDialog({ client, state }: Props) {
const { mutate: deleteClient, isPending } = useDeleteClient(() =>
state.close(),
);
return (
<AlertDialog.Backdrop isOpen={state.isOpen} onOpenChange={state.setOpen}>
<AlertDialog.Container size="md">
<AlertDialog.Dialog
role="alertdialog"
aria-labelledby="delete-dialog-heading"
aria-describedby="delete-dialog-description"
>
<AlertDialog.CloseTrigger aria-label="Cancel deletion" />
<AlertDialog.Header>
<AlertDialog.Icon status="danger" aria-hidden="true" />
<AlertDialog.Heading id="delete-dialog-heading">
Delete Client
</AlertDialog.Heading>
</AlertDialog.Header>
<AlertDialog.Body id="delete-dialog-description">
<p>
Are you sure you want to delete <strong>{client.email}</strong>?
This will permanently remove their account and all associated
data. This action cannot be undone.
</p>
</AlertDialog.Body>
<AlertDialog.Footer>
<Button
slot="close"
variant="tertiary"
isDisabled={isPending}
aria-label="Cancel and close dialog"
>
Cancel
</Button>
<Button
variant="danger"
isDisabled={isPending}
isPending={isPending}
aria-label={
isPending
? `Deleting ${client.email}, please wait`
: `Confirm deletion of ${client.email}`
}
onPress={() => deleteClient(client.id)}
>
{isPending ? "Deleting..." : "Delete"}
</Button>
</AlertDialog.Footer>
</AlertDialog.Dialog>
</AlertDialog.Container>
</AlertDialog.Backdrop>
);
}
export { DeleteClientAlertDialog as default };
Expected behavior
When dropdown.close() is called inside onAction, the dropdown should fully close before the AlertDialog opens. Closing one overlay and immediately opening another should work without requiring any workarounds.
Screenshots or Videos
Operating System Version
Linux
Browser
Chrome
HeroUI Version
@heroui/react@3.0.1
Describe the bug
When a controlled
Dropdownis closed insideonActionand anAlertDialogis opened immediately after, the dropdown remains visible on large/desktop screens. The issue does not reproduce consistently on mobile or small screens.All straightforward approaches to close the dropdown before opening the dialog fail on large screens. The suspected root cause is React Aria's async focus restoration — when the dropdown closes, React Aria restores focus to the trigger asynchronously, which appears to interfere with or re-open the controlled dropdown before the
AlertDialogmounts and traps focus.Your Example Website or App
No response
Steps to Reproduce the Bug or Issue
DropdownusingisOpen/onOpenChangeonAction, calldropdown.close()then open anAlertDialogExpected behavior
When
dropdown.close()is called insideonAction, the dropdown should fully close before theAlertDialogopens. Closing one overlay and immediately opening another should work without requiring any workarounds.Screenshots or Videos
Operating System Version
Linux
Browser
Chrome