Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/responsive-dropdown-file-dialog-close.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@datum-cloud/datum-ui': patch
---

Fix `ResponsiveDropdown` closing when the window loses focus. `@radix-ui/react-menu` ≥2.1.17 closes menus on window blur, which unmounted dropdown content whenever the native file picker opened (or the user switched apps to drag a file in) — silently breaking flows like the DNS record import dropzone. The desktop variant now renders a Radix Popover, which keeps the same anchored dropdown UX without the blur-close behavior and is the correct semantics for the arbitrary interactive content this component hosts.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react'
import { fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { MobileSheet } from '../../mobile-sheet'
import { ResponsiveDropdown } from '../responsive-dropdown'
Expand Down Expand Up @@ -53,6 +53,23 @@ describe('responsiveDropdown', () => {
expect(screen.getByText('Dropdown body')).toBeInTheDocument()
})

it('stays open when the window loses focus (e.g. native file picker opens)', () => {
setViewport(1440)
const onOpenChange = vi.fn()
render(
<ResponsiveDropdown
open
onOpenChange={onOpenChange}
trigger={<button type="button">Trigger</button>}
sheetTitle="Menu"
>
<div>Dropdown body</div>
</ResponsiveDropdown>,
)
fireEvent.blur(window)
expect(onOpenChange).not.toHaveBeenCalledWith(false)
})

it('renders mobile sheet on mobile viewport', () => {
setViewport(500)
render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Slot } from '@radix-ui/react-slot'
import { isValidElement } from 'react'
import { useBreakpoint } from '../../../hooks/use-breakpoint'
import { cn } from '../../../utils/cn'
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../../features/dropdown'
import { MobileSheet, useInSheet } from '../mobile-sheet'
import { Popover, PopoverContent, PopoverTrigger } from '../popover'

interface ResponsiveDropdownProps {
open: boolean
Expand All @@ -17,10 +17,10 @@ interface ResponsiveDropdownProps {
sheetDescription?: string
/** Optional footer shown at the bottom of the mobile sheet (mobile only). */
sheetFooter?: ReactNode
/** DropdownMenuContent alignment (desktop only). @default 'end' (matches existing DropdownMenu convention for row-action menus) */
/** PopoverContent alignment (desktop only). @default 'end' (matches existing convention for row-action menus) */
align?: 'start' | 'center' | 'end'
contentClassName?: string
/** Called when DropdownMenuContent auto-focuses on close (desktop only) */
/** Called when PopoverContent auto-focuses on close (desktop only) */
onCloseAutoFocus?: (e: Event) => void
/**
* Force desktop dropdown even on mobile. Default: true (responsive).
Expand Down Expand Up @@ -84,16 +84,24 @@ export function ResponsiveDropdown({
)
}

// Desktop uses a Popover rather than a DropdownMenu: Radix menus (≥2.1.17)
// close on window blur, which unmounts content that opens the native file
// picker (e.g. a Dropzone) before the selection ever lands. Children here
// are arbitrary interactive content, not menu items, so popover (dialog)
// semantics are also the correct a11y fit.
return (
<DropdownMenu open={open} onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent
<Popover open={open} onOpenChange={onOpenChange} modal>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent
align={align}
className={cn('rounded-lg p-0', contentClassName)}
className={cn(
'max-h-(--radix-popover-content-available-height) w-auto min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-lg p-0',
contentClassName,
)}
onCloseAutoFocus={onCloseAutoFocus}
>
{children}
</DropdownMenuContent>
</DropdownMenu>
</PopoverContent>
</Popover>
)
}
Loading