RFC: Conditional close-on-outside-pointer hook for brn-dialog/brn-popover (so nested CDK overlays don't dismiss the host)
#1,382 opened on Jun 2, 2026
Repository metrics
- Stars
- (2,788 stars)
- PR merge metrics
- (PR metrics pending)
Description
Scope
popover, dialog
Information
Problem
BrnDialog (and therefore BrnPopover) only exposes closeOnOutsidePointerEvents as a boolean. There is no way to conditionally keep the dialog/popover open for a specific outside pointer event.
This breaks a very common composition: an overlay-based component opened from inside a popover — e.g. a brn-select (or any CDK-overlay component: autocomplete, combobox, menu, nested popover) placed inside brn-popover's content.
brn-select renders its listbox in its own CDK overlay, which is a sibling of the popover's overlay in the global overlay container — not a DOM descendant of the popover's pane. So a pointer interaction with the open select panel (or its backdrop) counts as an outside pointer event for the popover. With closeOnOutsidePointerEvents enabled (the default for brn-popover), the popover tears itself down together with the select the moment the user clicks the select.
Result: you cannot reliably put a select/combobox/menu inside a popover, which is a standard UI pattern (filter popovers, settings popovers, etc.).
Root cause
In BrnDialog the close is wired unconditionally whenever the boolean is true (current main, @spartan-ng/brain@0.0.1-alpha.702):
const closeOnOutsidePointerEvents =
options?.closeOnOutsidePointerEvents ?? this._defaultOptions.closeOnOutsidePointerEvents;
if (closeOnOutsidePointerEvents) {
cdkDialogRef.outsidePointerEvents
.pipe(takeUntil(merge(destroyed$, optionsChanged$)))
.subscribe(() => {
/* close */
});
}
There is no per-event gate between outsidePointerEvents firing and the close.
Proposed solution
Add an optional close predicate input to BrnDialog (inherited by BrnPopover) that gates the outside-pointer close:
/** Return `false` to keep the dialog/popover open for this outside pointer event. */
readonly closePredicate = input<(event: Event) => boolean>();
Wire it into the existing subscription:
if (closeOnOutsidePointerEvents) {
cdkDialogRef.outsidePointerEvents
.pipe(
filter((event) => this.closePredicate()?.(event) ?? true),
takeUntil(merge(destroyed$, optionsChanged$)),
)
.subscribe(() => {
/* close */
});
}
Usage:
<brn-popover [closePredicate]="keepOpenWhileNestedOverlayOpen">
...
</brn-popover>
Notes on the API shape:
- This maps directly onto CDK's existing
OverlayConfig.eventPredicate: (event: Event) => boolean("determines if the overlay should receive a specific event or if the event should go to the next overlay in the stack"). Forwarding/surfacing that field would achieve the same result at the overlay layer. - A higher-level, batteries-included option would be an opt-in (e.g.
ignoreNestedOverlayPointerEvents) where brain itself detects that the pointer event originated in a CDK overlay logically spawned from this dialog's content and skips the close. A genericclosePredicateis the minimal, most flexible primitive; the nested-overlay behaviour can be built on top of it.
This is fully backward compatible — when closePredicate is not provided, behaviour is unchanged.
Reproduction
A brn-select inside a brn-popover:
- Open the popover.
- Open the select inside it.
- Click an option / click the select backdrop to dismiss the select.
Expected: the select closes; the popover stays open. Actual: the popover closes together with the select (the click is treated as an outside pointer event for the popover).
A minimal StackBlitz/GitHub reproduction can be provided on request.
Environment / verified against
@spartan-ng/brain: confirmed on0.0.1-alpha.702(latest at time of writing);closeOnOutsidePointerEventsis still boolean-only andBrnDialogOptionsexposes no predicate.- Angular 21,
@angular/cdk21.
Alternatives / workarounds currently in use
We keep brn-popover open by reaching into CDK internals from a directive applied to the brn-popover element, and setting the overlay's eventPredicate on each open:
// Walk brn-dialog -> CDK DialogRef -> OverlayRef to reach OverlayConfig.eventPredicate.
// All access is optional-chained so a future refactor degrades to default close behaviour.
const overlayRef = (popover as any)._dialogRef()?._cdkDialogRef?.overlayRef;
const config = overlayRef?._config;
const popoverPane = overlayRef?.overlayElement;
if (!config || !popoverPane) return;
config.eventPredicate = (event: Event) => {
if (event.type !== 'click' && event.type !== 'auxclick' && event.type !== 'contextmenu') return true;
// Keep open only while one of THIS popover's own descendant overlays (e.g. a select panel) is open.
return !hasOpenNestedOverlay(popoverPane);
};
This works but relies on private fields (_dialogRef, _cdkDialogRef, _config) that are version-specific and undocumented — exactly what a public closePredicate (or forwarded eventPredicate) input would replace.
I would be willing to submit a PR to fix this issue
- Yes
- No