Stop filter-popup clicks from reaching the column header; use a Flyout
Clicking inside the open flags filter popup could act on the
DataGridColumnHeader underneath: unhandled press/release pairs bubbled
out of the popup into the header (which treats them as a sort click
and flashes its pressed visual), and on X11 overlay popups the
light-dismiss machinery could even re-target a click's press directly
at the header while the popup stayed open.
The filter UI is now hosted in a Flyout attached to the funnel icon
instead of a hand-rolled Popup parked in the header's panel. Avalonia
positions Popup as the low-level primitive and recommends Flyout for
attached pickers: light dismiss, Escape-to-close, focus handling, and
theme-correct presenter chrome come built in (the old hard-coded white
background was also wrong in dark mode). The flyout content swallows
unhandled wheel and press/release events, since its internal popup is
still logically parented to the funnel inside the header.
A headless end-to-end regression test drives a real DataGrid with
overlay popup hosts (the X11 OverlayPopups=true configuration the app
runs with) and raw input through the funnel and every visible flyout
control, asserting the header never sorts and never receives an
unhandled press or release.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
$"clicking the {target.GetType().Name} inside the flyout must not close it");
// ProcessSort is dispatched via Dispatcher.UIThread.Post — drain per click so
// a leaked sort is attributed to the control that caused it.
Dispatcher.UIThread.RunJobs();
sortingRaised.Should().Be(0,
$"clicking the {target.GetType().Name} inside the popup must not sort the column");
headerUnhandledPresses.Should().Be(0,
$"clicking the {target.GetType().Name} inside the popup must not deliver an unhandled press to a column header (the header would flash its pressed visual)");
headerUnhandledReleases.Should().Be(0,
$"clicking the {target.GetType().Name} inside the popup must not deliver an unhandled release to a column header");
}
clicked.Should().BeGreaterThan(2,"setup precondition — the sweep must actually click several visible controls");
// ProcessSort is dispatched via Dispatcher.UIThread.Post — drain the queue so a
// leaked sort would actually fire before the assertion.
Dispatcher.UIThread.RunJobs();
sortingRaised.Should().Be(0,
"neither opening the filter popup nor clicking controls inside it may sort the column");