diff --git a/Sources/Sidebar/AppKitList/Cells/SidebarRowSwiftUIPopoverPresenter.swift b/Sources/Sidebar/AppKitList/Cells/SidebarRowSwiftUIPopoverPresenter.swift index 43008e112bb6..4994c8d6ddac 100644 --- a/Sources/Sidebar/AppKitList/Cells/SidebarRowSwiftUIPopoverPresenter.swift +++ b/Sources/Sidebar/AppKitList/Cells/SidebarRowSwiftUIPopoverPresenter.swift @@ -41,6 +41,11 @@ final class SidebarRowSwiftUIPopoverPresenter: NSObject, NSPopoverDelegate { var isShown: Bool { popover?.isShown == true } + /// True from `popoverWillClose` until `popoverDidClose`. An animated close + /// keeps `isShown` true until the animation ends, so containers that + /// must know whether a close already began check this as well. + private(set) var isClosing = false + func present( _ root: AnyView, relativeTo rect: NSRect, @@ -112,7 +117,12 @@ final class SidebarRowSwiftUIPopoverPresenter: NSObject, NSPopoverDelegate { PopoverKeyWindowElevator.promoteToKeyIfPossible(hostingController.view.window) } + func popoverWillClose(_ notification: Notification) { + isClosing = true + } + func popoverDidClose(_ notification: Notification) { + isClosing = false visibleUpdateScheduler.cancel() pendingRoot = nil popover = nil diff --git a/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowChecklistSection.swift b/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowChecklistSection.swift index 9e13a233b1fe..6fdeee968bff 100644 --- a/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowChecklistSection.swift +++ b/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowChecklistSection.swift @@ -493,10 +493,21 @@ final class SidebarRowChecklistSection: NSView { override func viewDidMoveToWindow() { super.viewDidMoveToWindow() - if window != nil, popoverPresenter.isShown { - // The popover survived the reparent, so a later close is a real - // click-away rather than a detach side effect. - popoverAnchorDetachedWhilePresented = false + if window != nil, popoverAnchorDetachedWhilePresented, popoverPresenter.isShown { + // The popover may have survived the reparent, in which case a + // later close is a real click-away rather than a detach side + // effect. Decide once the reattach settles: an animated popover + // stays `isShown` while its detach-induced close animates, and + // that close can finish after the anchor is back in the window. + let generation = popoverPresentationGeneration + DispatchQueue.main.async { [weak self] in + guard let self, + self.popoverPresentationGeneration == generation, + self.window != nil, + self.popoverPresenter.isShown, + !self.popoverPresenter.isClosing else { return } + self.popoverAnchorDetachedWhilePresented = false + } } if window != nil, pendingPopoverPresentation { needsLayout = true