microsoft / microsoft/microsoft-ui-xaml

Button cannot be invoked via keyboard if its container collapses while primary pointer button is held down

Open
#11,148 1 comment 0 reactions 1 assignee Claimed by @marcelwgn View on GitHub
area-Button bug
Dominant language
C++
Stars
8.4k
Forks
942
Avg merge
2d 7h
Merged PRs (30d)
105

Description

### Describe the bug

If the control containing a `Button` is collapsed while the `Button` is in a "pointer pressed" state, the `Button` can no longer be invoked using keyboard buttons until the `Button` is re-clicked.

### Why is this important?

WinUI applications may have "draggable" controls that the user can move around. If the user initiates a drag on a button, this bug can easily repro if a data template for the `Button` changes in response to the drag.

### Steps to reproduce the bug

1. Have a `Button` inside another control
2. Press and hold the primary pointer button while hovering over the `Button`
3. Without releasing pointer button, force the `Button`'s container to enter a `Collapsed` state
4. Release pointer button
5. Re-show the `Button`'s container
6. Focus the `Button` (tab to it, or programmatically move focus)
7. Try to invoke the button using space or enter

### Actual behavior

The button visually changes to a "pressed" state, but is never invoked.

### Expected behavior

The button gets invoked as normal.

### Screenshots

https://github.com/user-attachments/assets/8ea32d62-0936-4a56-82cb-2a540ea195eb

### NuGet package version

2.1.3

### Windows version

Windows 11 (24H2): Build 26100

### Additional context

Here is source code for video above.

`MainPage.xaml`:
```xaml





Clicking the button or invoking via keyboard should increment the click count above.

Click and drag the button more than 20 pixels to briefly collapse the border containing it. After it re-appears, focus it and try to invoke using keyboard.

Note that clicking the button again restores correct keyboard behavior.

Focus Above Button

```
`MainPage.xaml.cs`:
```cs
using System;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Windows.Foundation;

namespace ButtonBug;

///
/// Repro page for WinUI Button pointer-state bug:
/// after OnPointerCanceled the Button can no longer be invoked by keyboard
/// (Space/Enter) until it is re-entered with the pointer.
///
public sealed partial class MainPage : Page
{
private Point _pressOrigin;
private bool _isTracking;
private bool _hasCollapsed;
private int _clickCount;

public MainPage()
{
InitializeComponent();

// Button marks pointer events Handled in its ControlTemplate, so
// attaching via XAML attributes does nothing. Use AddHandler with
// handledEventsToo: true so we still receive them.
ReproButton.AddHandler(UIElement.PointerPressedEvent,
new PointerEventHandler(ReproButton_PointerPressed), true);
ReproButton.AddHandler(UIElement.PointerMovedEvent,
new PointerEventHandler(ReproButton_PointerMoved), true);
ReproButton.AddHandler(UIElement.PointerReleasedEvent,
new PointerEventHandler(ReproButton_PointerReleased), true);
ReproButton.AddHandler(UIElement.PointerCanceledEvent,
new PointerEventHandler(ReproButton_PointerCanceled), true);
ReproButton.AddHandler(UIElement.PointerCaptureLostEvent,
new PointerEventHandler(ReproButton_PointerCaptureLost), true);
}

private void ReproButton_Click(object sender, RoutedEventArgs e)
{
_clickCount++;
ClickCountLabel.Text = $"Click count: {_clickCount}";
}

private void ReproButton_PointerPressed(object sender, PointerRoutedEventArgs e)
{
_pressOrigin = e.GetCurrentPoint(this).Position;
_isTracking = true;
_hasCollapsed = false;
}

private void ReproButton_PointerMoved(object sender, PointerRoutedEventArgs e)
{
if (!_isTracking || _hasCollapsed)
{
return;
}

var p = e.GetCurrentPoint(this).Position;
var dx = p.X - _pressOrigin.X;
var dy = p.Y - _pressOrigin.Y;
if ((dx * dx) + (dy * dy) > 20.0 * 20.0)
{
_hasCollapsed = true;
ButtonBorder.Visibility = Visibility.Collapsed;
_ = RestoreAfterDelayAsync();
}
}

private void ReproButton_PointerReleased(object sender, PointerRoutedEventArgs e)
{
_isTracking = false;
}

private void ReproButton_PointerCanceled(object sender, PointerRoutedEventArgs e)
{
_isTracking = false;
}

private void ReproButton_PointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
_isTracking = false;
}

private async Task RestoreAfterDelayAsync()
{
await Task.Delay(2000);
DispatcherQueue.TryEnqueue(() =>
{
ButtonBorder.Visibility = Visibility.Visible;
});
}

private void FocusButton_Click(object sender, RoutedEventArgs e)
{
this.ReproButton.Focus(FocusState.Keyboard);
}
}
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.