microsoft / microsoft/microsoft-ui-xaml
Button cannot be invoked via keyboard if its container collapses while primary pointer button is held down
- 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
Assessment
This issue has not been assessed yet.