diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ValidatePickerTitleAndItemCharacterSpacing.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ValidatePickerTitleAndItemCharacterSpacing.png new file mode 100644 index 000000000000..b95f40dd6cd9 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ValidatePickerTitleAndItemCharacterSpacing.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue30464.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue30464.cs new file mode 100644 index 000000000000..66d84a07ab8b --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue30464.cs @@ -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 { "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 { "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, + } + }; + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30464.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30464.cs new file mode 100644 index 000000000000..a55ac34235f3 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30464.cs @@ -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 \ No newline at end of file diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ValidatePickerTitleAndItemCharacterSpacing.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ValidatePickerTitleAndItemCharacterSpacing.png new file mode 100644 index 000000000000..89315b5fefd4 Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ValidatePickerTitleAndItemCharacterSpacing.png differ diff --git a/src/Core/src/Handlers/Picker/PickerHandler.Windows.cs b/src/Core/src/Handlers/Picker/PickerHandler.Windows.cs index ede369bf75aa..71159d71c774 100644 --- a/src/Core/src/Handlers/Picker/PickerHandler.Windows.cs +++ b/src/Core/src/Handlers/Picker/PickerHandler.Windows.cs @@ -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; @@ -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) + { + PlatformView.ApplyCharacterSpacingToSelectedItem(VirtualView.CharacterSpacing.ToEm()); + } + PlatformView.MinWidth = 0; } @@ -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 diff --git a/src/Core/src/Platform/Windows/CharacterSpacingConverter.cs b/src/Core/src/Platform/Windows/CharacterSpacingConverter.cs new file mode 100644 index 000000000000..1b384a7d44fc --- /dev/null +++ b/src/Core/src/Platform/Windows/CharacterSpacingConverter.cs @@ -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(); + } +} \ No newline at end of file diff --git a/src/Core/src/Platform/Windows/PickerExtensions.cs b/src/Core/src/Platform/Windows/PickerExtensions.cs index 30f33ec854df..77d7ff402e11 100644 --- a/src/Core/src/Platform/Windows/PickerExtensions.cs +++ b/src/Core/src/Platform/Windows/PickerExtensions.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using Microsoft.Maui.Graphics; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -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"); + var textBlock = contentPresenter?.GetFirstDescendant(); + + if (textBlock is not null) + { + textBlock.CharacterSpacing = characterSpacing; + } } public static void UpdateFont(this ComboBox nativeComboBox, IPicker picker, IFontManager fontManager) => diff --git a/src/Core/src/Platform/Windows/Styles/MauiComboBoxStyle.xaml b/src/Core/src/Platform/Windows/Styles/MauiComboBoxStyle.xaml index f52b6279a916..4fbbe790cd54 100644 --- a/src/Core/src/Platform/Windows/Styles/MauiComboBoxStyle.xaml +++ b/src/Core/src/Platform/Windows/Styles/MauiComboBoxStyle.xaml @@ -6,7 +6,7 @@ diff --git a/src/Core/src/Platform/Windows/Styles/Resources.xaml b/src/Core/src/Platform/Windows/Styles/Resources.xaml index 037beebde72e..32d80894bd47 100644 --- a/src/Core/src/Platform/Windows/Styles/Resources.xaml +++ b/src/Core/src/Platform/Windows/Styles/Resources.xaml @@ -14,6 +14,7 @@ true + 350