Skip to content
Merged
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue30464.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
namespace Controls.TestCases.HostApp.Issues;

[Issue(IssueTracker.Github, 30464, "The CharacterSpacing property on the Picker control is not applied to the title or the items", PlatformAffected.UWP)]
public class Issue30464 : ContentPage
{
public Issue30464()
{
Picker pickerTitleCharacterSpacing = new Picker
{
Title = "Select an item",
};
pickerTitleCharacterSpacing.Items.Add("Item 1");

Picker pickerItemCharacterSpacing = new Picker
{
ItemsSource = new List<string> { "Item 1", "Item 2" },
SelectedIndex = 1
};

Button applyCharacterSpacingBtn = new Button
{
Text = "Apply character spacing",
AutomationId = "Issue30464Btn"
};

applyCharacterSpacingBtn.Clicked += (sender, e) =>
{
pickerTitleCharacterSpacing.CharacterSpacing = 14;
pickerItemCharacterSpacing.CharacterSpacing = 14;
};

Label descriptionLabel = new Label
{
AutomationId = "Issue30464DescriptionLabel",
Text = "The test case passes only if character spacing is correctly applied to both the Picker title and items, and is maintained after selecting a different item; otherwise, it fails.",
};

Picker pickerSelectionChange = new Picker
{
ItemsSource = new List<string> { "Item 1", "Item 2" },
SelectedIndex = 0,
CharacterSpacing = 14,
AutomationId = "Issue30464SelectionChangePicker"
};

Button changeSelectionBtn = new Button
{
Text = "Change selection",
AutomationId = "Issue30464ChangeSelectionBtn"
};
changeSelectionBtn.Clicked += (sender, e) =>
{
pickerSelectionChange.SelectedIndex = 1;
};

Content = new VerticalStackLayout
{
Spacing = 20,
Padding = 20,
Children =
{
pickerTitleCharacterSpacing,
pickerItemCharacterSpacing,
applyCharacterSpacingBtn,
pickerSelectionChange,
changeSelectionBtn,
descriptionLabel,
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#if TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST // PR Link - https://github.com/dotnet/maui/pull/34974
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue30464 : _IssuesUITest
{
public Issue30464(TestDevice device) : base(device)
{
}

public override string Issue => "The CharacterSpacing property on the Picker control is not applied to the title or the items";

[Test]
[Category(UITestCategories.Picker)]
public void ValidatePickerTitleAndItemCharacterSpacing()
{
App.WaitForElement("Issue30464DescriptionLabel");
App.Tap("Issue30464Btn");
App.Tap("Issue30464ChangeSelectionBtn");
VerifyScreenshot();
}
}
#endif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions src/Core/src/Handlers/Picker/PickerHandler.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Microsoft.Maui.Platform;
using Microsoft.UI.Xaml.Controls;
using WSelectionChangedEventArgs = Microsoft.UI.Xaml.Controls.SelectionChangedEventArgs;

Expand Down Expand Up @@ -122,6 +123,13 @@ void OnControlSelectionChanged(object? sender, WSelectionChangedEventArgs e)
if (VirtualView != null && !UpdatingItemSource)
VirtualView.SelectedIndex = PlatformView.SelectedIndex;

// Reapply CharacterSpacing to the selected item's TextBlock so it persists
// across selection changes (e.g. programmatic SelectedIndex updates).
if (VirtualView != null && VirtualView.CharacterSpacing > 0)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[moderate] Logic and Correctness - This reapply path only runs when CharacterSpacing > 0, but CharacterSpacing has no non-negative validation and the other Windows text controls apply CharacterSpacing.ToEm() directly. A Picker using negative character spacing can update ComboBox.CharacterSpacing, then lose that value after selection/dropdown lifecycle because the selected TextBlock is not re-stamped here or in the matching dropdown-closed path at line 177. Apply the current converted value unconditionally so zero/negative values are preserved consistently.

{
PlatformView.ApplyCharacterSpacingToSelectedItem(VirtualView.CharacterSpacing.ToEm());
}

PlatformView.MinWidth = 0;
}

Expand All @@ -134,17 +142,43 @@ void OnMauiComboBoxDropDownOpened(object? sender, object e)

comboBox.MinWidth = comboBox.ActualWidth;

// Apply CharacterSpacing to each ComboBoxItem container so dropdown list items
// render with the configured spacing. Containers are only realized once the
// dropdown is opened, so this is the earliest reliable point to set them.
ApplyCharacterSpacingToItems(comboBox);

if (VirtualView is null)
return;

VirtualView.IsOpen = true;
}

static void ApplyCharacterSpacingToItems(ComboBox comboBox)
{
var characterSpacing = comboBox.CharacterSpacing;

for (int i = 0; i < comboBox.Items.Count; i++)
{
if (comboBox.ContainerFromIndex(i) is ComboBoxItem container)
{
container.CharacterSpacing = characterSpacing;
}
}
}

void OnMauiComboBoxDropDownClosed(object? sender, object e)
{
if (VirtualView is null)
return;

// After a manual dropdown selection, the ContentPresenter's TextBlock is reused
// and its rendered text doesn't pick up the CharacterSpacing already set on the
// ComboBox. Reapply CharacterSpacing directly to the selected item's TextBlock here.
if (sender is ComboBox cb && VirtualView.CharacterSpacing > 0)
{
cb.ApplyCharacterSpacingToSelectedItem(VirtualView.CharacterSpacing.ToEm());
}

if (sender is ComboBox comboBox && comboBox.MinWidth > 0)
{
//Reset the MinWidth to allow ComboBox to resize when the parent's size changes
Expand Down
21 changes: 21 additions & 0 deletions src/Core/src/Platform/Windows/CharacterSpacingConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace Microsoft.Maui.Platform;

internal sealed partial class CharacterSpacingConverter : UI.Xaml.Data.IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is double characterSpacing)
{
return characterSpacing.ToEm();
}

return 0;
}

public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
27 changes: 26 additions & 1 deletion src/Core/src/Platform/Windows/PickerExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable
using System;
using Microsoft.Maui.Graphics;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
Expand Down Expand Up @@ -74,7 +75,31 @@ public static void UpdateSelectedIndex(this ComboBox nativeComboBox, IPicker pic

public static void UpdateCharacterSpacing(this ComboBox nativeComboBox, IPicker picker)
{
nativeComboBox.CharacterSpacing = picker.CharacterSpacing.ToEm();
var characterSpacing = picker.CharacterSpacing.ToEm();
nativeComboBox.CharacterSpacing = characterSpacing;

// Apply directly to the selected item's TextBlock so the closed picker reflects spacing.
// If the control isn't loaded yet, defer until Loaded so the visual tree exists.
if (nativeComboBox.IsLoaded)
{
ApplyCharacterSpacingToSelectedItem(nativeComboBox, characterSpacing);
}
else
{
nativeComboBox.OnLoaded(() =>
ApplyCharacterSpacingToSelectedItem(nativeComboBox, nativeComboBox.CharacterSpacing));
}
}

internal static void ApplyCharacterSpacingToSelectedItem(this ComboBox nativeComboBox, int characterSpacing)
{
var contentPresenter = nativeComboBox.GetDescendantByName<ContentPresenter>("ContentPresenter");
var textBlock = contentPresenter?.GetFirstDescendant<TextBlock>();

if (textBlock is not null)
{
textBlock.CharacterSpacing = characterSpacing;
}
}

public static void UpdateFont(this ComboBox nativeComboBox, IPicker picker, IFontManager fontManager) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<DataTemplate x:Key="ComboBoxHeader">
<TextBlock
Text="{Binding Title}"
CharacterSpacing="{Binding Path=CharacterSpacing, ElementName=HeaderContentPresenter}"
CharacterSpacing="{Binding CharacterSpacing, Converter={StaticResource CharacterSpacingConverter}}"
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The binding for CharacterSpacing in the ComboBoxHeader template lacks ElementName=HeaderContentPresenter, so it will try to read CharacterSpacing from the data context rather than the control. Add ElementName=HeaderContentPresenter to the binding source.

Suggested change
CharacterSpacing="{Binding CharacterSpacing, Converter={StaticResource CharacterSpacingConverter}}"
CharacterSpacing="{Binding CharacterSpacing, ElementName=HeaderContentPresenter, Converter={StaticResource CharacterSpacingConverter}}"

Copilot uses AI. Check for mistakes.
Foreground="{Binding TitleColor, Converter={StaticResource ColorConverter}, ConverterParameter=DefaultTextForegroundThemeBrush}" />
</DataTemplate>

Expand Down
1 change: 1 addition & 0 deletions src/Core/src/Platform/Windows/Styles/Resources.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<x:Boolean x:Key="MicrosoftMauiCoreIncluded">true</x:Boolean>

<windows:ColorConverter x:Key="ColorConverter" />
<windows:CharacterSpacingConverter x:Key="CharacterSpacingConverter"/>

<x:Double x:Key="FlyoutMinWidth">350</x:Double>

Expand Down
Loading