Compare commits

...

1 Commits

Author SHA1 Message Date
chatasweetie
af71d4969c Add ItemsRepeater focus restoration on Extensions settings page 2026-02-24 12:34:17 -08:00
2 changed files with 134 additions and 0 deletions

View File

@@ -216,6 +216,7 @@
<ItemsRepeater
x:Name="ProvidersRepeater"
x:Load="{x:Bind viewModel.Extensions.HasResults, Mode=OneWay}"
GettingFocus="ProvidersRepeater_GettingFocus"
ItemsSource="{x:Bind viewModel.Extensions.FilteredProviders, Mode=OneWay}"
Layout="{StaticResource VerticalStackLayout}">
<ItemsRepeater.ItemTemplate>
@@ -224,6 +225,7 @@
Click="SettingsCard_Click"
DataContext="{x:Bind}"
Description="{x:Bind ExtensionSubtext, Mode=OneWay}"
GotFocus="SettingsCard_GotFocus"
Header="{x:Bind DisplayName, Mode=OneWay}"
IsClickEnabled="True">
<controls:SettingsCard.HeaderIcon>

View File

@@ -19,6 +19,8 @@ public sealed partial class ExtensionsPage : Page
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
private readonly SettingsViewModel? viewModel;
private WeakReference<SettingsCard>? _lastFocusedCard;
private bool _isRestoringFocus;
public ExtensionsPage()
{
@@ -58,4 +60,134 @@ public sealed partial class ExtensionsPage : Page
Logger.LogError("Error when showing FallbackRankerDialog", ex);
}
}
private void SettingsCard_GotFocus(object sender, RoutedEventArgs e)
{
// Track when a SettingsCard gets focus (not its children like ToggleSwitch)
if (sender is SettingsCard card && ReferenceEquals(e.OriginalSource, card))
{
_lastFocusedCard = new WeakReference<SettingsCard>(card);
}
}
private void ProvidersRepeater_GettingFocus(UIElement sender, GettingFocusEventArgs args)
{
// Prevent recursive focus handling
if (_isRestoringFocus)
{
return;
}
// Only intervene when focus is coming into the ItemsRepeater from outside
if (args.OldFocusedElement != null && IsElementInsideRepeater(args.OldFocusedElement))
{
return;
}
SettingsCard? targetCard = null;
// Try to restore focus to the last focused card
if (_lastFocusedCard?.TryGetTarget(out var lastCard) == true && IsCardValid(lastCard))
{
targetCard = lastCard;
}
// If no valid last focused card, focus the first one
if (targetCard == null)
{
targetCard = GetFirstSettingsCard();
}
// Use async focus restoration to avoid crashes during focus transition
if (targetCard != null)
{
args.TryCancel();
_ = RestoreFocusAsync(targetCard);
}
}
private async Task RestoreFocusAsync(SettingsCard card)
{
if (_isRestoringFocus)
{
return;
}
try
{
_isRestoringFocus = true;
// Verify the card is still valid before focusing
if (IsCardValid(card))
{
// Bring the element into view before focusing
card.StartBringIntoView(new BringIntoViewOptions
{
AnimationDesired = true,
VerticalAlignmentRatio = 0.5, // Center vertically
});
_ = card.Focus(FocusState.Keyboard);
}
}
catch (Exception ex)
{
Logger.LogError("Error restoring focus to SettingsCard", ex);
}
finally
{
_isRestoringFocus = false;
}
}
private bool IsElementInsideRepeater(object element)
{
if (element is not DependencyObject depObj)
{
return false;
}
var parent = depObj;
while (parent != null)
{
if (ReferenceEquals(parent, ProvidersRepeater))
{
return true;
}
parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(parent);
}
return false;
}
private bool IsCardValid(SettingsCard card)
{
try
{
// Check if the card is in the visual tree and can receive focus
return IsElementInsideRepeater(card) && card.IsLoaded;
}
catch
{
return false;
}
}
private SettingsCard? GetFirstSettingsCard()
{
try
{
if (ProvidersRepeater.TryGetElement(0) is FrameworkElement firstElement)
{
return firstElement as SettingsCard;
}
}
catch (Exception ex)
{
Logger.LogError("Error getting first SettingsCard", ex);
}
return null;
}
}