Skip to content

Clicking a UI element inside an entity should not invoke the default open behavior #26

Closed

Description

@JoshuaRowePhantom

Bug Report

Clicking on a UI element within an entity (e.g. a button, link, or control) incorrectly triggers the entity's default open behavior in addition to the element's own action.

Steps to reproduce

  1. Open an entity that contains interactive UI elements
  2. Click on one of those UI elements

Expected behavior
Only the UI element's own action fires; the entity's default open behavior does not trigger.

Actual behavior
The entity's default open behavior is invoked alongside (or instead of) the UI element's action.

Activity

  1. JoshuaRowePhantom commented on Jun 24, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Investigation Findings

    Root Cause

    File: Phantom.Workspaces/Controls/EntityCardControl.axaml.cs — OnEntityCardTapped, line 19

    private void OnEntityCardTapped(object? sender, TappedEventArgs e)
    {
        if (e.Source is Button)   // ← BUG
        {
            return;
        }
        // ... fires open command
    }

    In Avalonia's routed-event system, e.Source is the innermost hit-tested element — when a user clicks a Button, they land on its rendered content (a TextBlock, ContentPresenter, etc.), not the Button object itself. So e.Source is Button is almost never true in practice. The guard fires only in the rare case that the pointer lands precisely on a Button with no content filling its bounds.

    What user clicks e.Source Guard fires? Result
    Button label/glyph TextBlock (button content) ✗ ❌ Also opens entity
    TextBox field editor TextBox or inner TextPresenter ✗ ❌ Also opens entity
    Direct Button hit (rare) Button ✓ ✅ Correctly suppressed
    Plain title TextBlock TextBlock ✗ ✅ Correct (open intended)

    The design doc (docs/design/entity-click-shortcut-handler.md, line 74) explicitly listed a "No double-trigger" test as required but it was never implemented.


    Missing Tests

    New file: Phantom.Workspaces.Tests/EntityCardControlClickTests.cs

    1. OnEntityCardTapped_WhenSourceIsButton_DoesNotInvokeOpenCommand — regression guard for the existing (partial) fix
    2. OnEntityCardTapped_WhenSourceIsChildOfButton_DoesNotInvokeOpenCommand ⚠️ the failing case — e.Source is a TextBlock inside a Button; open command must not fire
    3. OnEntityCardTapped_WhenSourceIsTextBox_DoesNotInvokeOpenCommand — clicking a TextBox field editor must not open the entity
    4. OnEntityCardTapped_WhenSourceIsChildOfTextBox_DoesNotInvokeOpenCommand — clicking text inside a TextBox (TextPresenter) must not open the entity
    5. OnEntityCardTapped_WhenSourceIsPlainTextBlock_InvokesOpenCommand — clicking a non-interactive TextBlock (entity title) should fire the open command (happy path)
    6. OnEntityCardTapped_WhenSourceIsCardBorder_InvokesOpenCommand — clicking the card background itself should fire the open command

    Recommended Fix

    File: Phantom.Workspaces/Controls/EntityCardControl.axaml.cs

    Replace the e.Source is Button guard with a visual-tree walk:

    private void OnEntityCardTapped(object? sender, TappedEventArgs e)
    {
        // Walk the visual tree upward from the hit-tested source element.
        // If any ancestor is an interactive control (Button, TextBox), that
        // element owns the interaction — don't also fire the card open action.
        if (e.Source is Visual source && IsInsideInteractiveElement(source))
        {
            return;
        }
    
        // ... existing open-command logic unchanged ...
        e.Handled = true;
    }
    
    private static bool IsInsideInteractiveElement(Visual source)
    {
        Visual? current = source;
        while (current is not null)
        {
            if (current is Button or TextBox)
            {
                return true;
            }
            current = current.GetVisualParent();
        }
        return false;
    }

    No XAML changes needed. GetVisualParent() is already available via the Avalonia.VisualTree import on line 4.


    Side Effects

    • TextBox fields: Clicking into a field editor to edit text no longer silently fires the open command as a side effect.
    • All button content: Any Button with composed visual content (glyph, image, SVG) is now correctly suppressed.
    • Extensible: IsInsideInteractiveElement is a single place to add further interactive types (ComboBox, CheckBox, etc.) as needed.
    • Visual tree walk cost: Bounded at a few levels; no performance concern.
  2. JoshuaRowePhantom commented on Jun 30, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification passed

    Checked: Commit 514bda5 (tag: ix-entity-card-interactive-guard) on the eatures branch — files Phantom.Workspaces/Controls/EntityCardControl.axaml.cs and Phantom.Workspaces.Tests/EntityCardControlInteractionTests.cs.

    Implementation: OnEntityCardTapped now calls IsInteractiveSource(e.Source, this) instead of the narrow �.Source is Button check. IsInteractiveSource walks the visual tree from the tap source up to the card boundary and returns rue if any ancestor is Button, TextBox, ComboBox, Slider, ListBox, or MenuItem — correctly suppressing the card's open action for all interactive controls and their nested content.

    Tests: EntityCardControlInteractionTests contains two AvaloniaFact tests:

    Result: Implementation found, all verification criteria satisfied, tests present and green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingverifiedIssue has been verified

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions