diff --git a/src/Compatibility/Core/src/Android/Renderers/EditorRenderer.cs b/src/Compatibility/Core/src/Android/Renderers/EditorRenderer.cs index d599b63b1466..a1afe77a6f3d 100644 --- a/src/Compatibility/Core/src/Android/Renderers/EditorRenderer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/EditorRenderer.cs @@ -32,6 +32,7 @@ protected override FormsEditText CreateNativeControl() protected override EditText EditText => Control; + [PortHandler] protected override void UpdatePlaceholderColor() { _hintColorSwitcher = _hintColorSwitcher ?? new TextColorSwitcher(EditText.HintTextColors, Element.UseLegacyColorManagement()); @@ -269,6 +270,7 @@ void UpdateText() abstract protected void UpdateTextColor(); + [PortHandler] protected virtual void UpdatePlaceholderText() { if (EditText.Hint == Element.Placeholder) @@ -277,6 +279,7 @@ protected virtual void UpdatePlaceholderText() EditText.Hint = Element.Placeholder; } + [PortHandler] abstract protected void UpdatePlaceholderColor(); void OnKeyboardBackPressed(object sender, EventArgs eventArgs) diff --git a/src/Compatibility/Core/src/iOS/Renderers/EditorRenderer.cs b/src/Compatibility/Core/src/iOS/Renderers/EditorRenderer.cs index 4c2e80ff2935..ff569791bd08 100644 --- a/src/Compatibility/Core/src/iOS/Renderers/EditorRenderer.cs +++ b/src/Compatibility/Core/src/iOS/Renderers/EditorRenderer.cs @@ -63,6 +63,7 @@ protected internal override void UpdateFont() _placeholderLabel.Font = Element.ToUIFont(); } + [PortHandler] protected internal override void UpdatePlaceholderText() { _placeholderLabel.Text = Element.Placeholder; @@ -84,6 +85,7 @@ protected internal override void UpdateCharacterSpacing() _placeholderLabel.AttributedText = placeHolder; } + [PortHandler] protected internal override void UpdatePlaceholderColor() { Color placeholderColor = Element.PlaceholderColor; @@ -93,6 +95,7 @@ protected internal override void UpdatePlaceholderColor() _placeholderLabel.TextColor = placeholderColor.ToUIColor(); } + [PortHandler] void CreatePlaceholderLabel() { if (Control == null) @@ -342,7 +345,10 @@ protected internal virtual void UpdateText() } } + [PortHandler] protected internal abstract void UpdatePlaceholderText(); + + [PortHandler] protected internal abstract void UpdatePlaceholderColor(); protected internal abstract void UpdateCharacterSpacing(); diff --git a/src/Controls/samples/Controls.Sample/Pages/MainPage.cs b/src/Controls/samples/Controls.Sample/Pages/MainPage.cs index e0211b66a0dc..383742cf8107 100644 --- a/src/Controls/samples/Controls.Sample/Pages/MainPage.cs +++ b/src/Controls/samples/Controls.Sample/Pages/MainPage.cs @@ -50,6 +50,7 @@ void SetupMauiLayout() verticalStack.Add(new Label { Text = loremIpsum, MaxLines = 2, LineBreakMode = LineBreakMode.TailTruncation }); verticalStack.Add(new Label { Text = "This should have five times the line height!", LineHeight = 5 }); + verticalStack.Add(new Editor { Placeholder = "This is an editor placeholder." } ); var paddingButton = new Button { Padding = new Thickness(40), diff --git a/src/Core/src/Core/IEditor.cs b/src/Core/src/Core/IEditor.cs index 9e8ed359e896..e787ea09fce4 100644 --- a/src/Core/src/Core/IEditor.cs +++ b/src/Core/src/Core/IEditor.cs @@ -3,13 +3,13 @@ /// /// Represents a View used to accept multi-line input. /// - public interface IEditor : IView, IText + public interface IEditor : IView, ITextInput { /// - /// Gets the maximum allowed length of input. + /// Gets or sets the placeholder text color. /// - int MaxLength { get; } - + Color PlaceholderColor { get; set; } + /// /// Gets a value that controls whether text prediction and automatic text correction is on or off. /// diff --git a/src/Core/src/Handlers/Editor/EditorHandler.Android.cs b/src/Core/src/Handlers/Editor/EditorHandler.Android.cs index 8f0e58914989..1c678bb9110d 100644 --- a/src/Core/src/Handlers/Editor/EditorHandler.Android.cs +++ b/src/Core/src/Handlers/Editor/EditorHandler.Android.cs @@ -1,13 +1,16 @@ -using System; +using Android.Content.Res; using Android.Views; using Android.Views.InputMethods; using AndroidX.AppCompat.Widget; using Microsoft.Extensions.DependencyInjection; +using System; namespace Microsoft.Maui.Handlers { public partial class EditorHandler : AbstractViewHandler { + static ColorStateList? DefaultPlaceholderTextColors { get; set; } + protected override AppCompatEditText CreateNativeView() { var editText = new AppCompatEditText(Context) @@ -23,11 +26,27 @@ protected override AppCompatEditText CreateNativeView() return editText; } + protected override void SetupDefaults(AppCompatEditText nativeView) + { + base.SetupDefaults(nativeView); + DefaultPlaceholderTextColors = nativeView.HintTextColors; + } + public static void MapText(EditorHandler handler, IEditor editor) { handler.TypedNativeView?.UpdateText(editor); } + public static void MapPlaceholder(EditorHandler handler, IEditor editor) + { + handler.TypedNativeView?.UpdatePlaceholder(editor); + } + + public static void MapPlaceholderColor(EditorHandler handler, IEditor editor) + { + handler.TypedNativeView?.UpdatePlaceholderColor(editor, DefaultPlaceholderTextColors); + } + public static void MapCharacterSpacing(EditorHandler handler, IEditor editor) { handler.TypedNativeView?.UpdateCharacterSpacing(editor); diff --git a/src/Core/src/Handlers/Editor/EditorHandler.Standard.cs b/src/Core/src/Handlers/Editor/EditorHandler.Standard.cs index 722548b603c2..0abefaf9b3b2 100644 --- a/src/Core/src/Handlers/Editor/EditorHandler.Standard.cs +++ b/src/Core/src/Handlers/Editor/EditorHandler.Standard.cs @@ -7,6 +7,8 @@ public partial class EditorHandler : AbstractViewHandler protected override object CreateNativeView() => throw new NotImplementedException(); public static void MapText(IViewHandler handler, IEditor editor) { } + public static void MapPlaceholder(IViewHandler handler, IEditor editor) { } + public static void MapPlaceholderColor(IViewHandler handler, IEditor editor) { } public static void MapCharacterSpacing(IViewHandler handler, IEditor editor) { } public static void MapMaxLength(IViewHandler handler, IEditor editor) { } public static void MapIsTextPredictionEnabled(EditorHandler handler, IEditor editor) { } diff --git a/src/Core/src/Handlers/Editor/EditorHandler.cs b/src/Core/src/Handlers/Editor/EditorHandler.cs index b0fb4d9be7b2..efcd2e1c78cc 100644 --- a/src/Core/src/Handlers/Editor/EditorHandler.cs +++ b/src/Core/src/Handlers/Editor/EditorHandler.cs @@ -5,6 +5,8 @@ public partial class EditorHandler public static PropertyMapper EditorMapper = new PropertyMapper(ViewHandler.ViewMapper) { [nameof(IEditor.Text)] = MapText, + [nameof(IEditor.Placeholder)] = MapPlaceholder, + [nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor, [nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing, [nameof(IEditor.MaxLength)] = MapMaxLength, [nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled, @@ -13,7 +15,6 @@ public partial class EditorHandler public EditorHandler() : base(EditorMapper) { - } public EditorHandler(PropertyMapper? mapper = null) : base(mapper ?? EditorMapper) diff --git a/src/Core/src/Handlers/Editor/EditorHandler.iOS.cs b/src/Core/src/Handlers/Editor/EditorHandler.iOS.cs index bd9bc039d2a8..98c0bc649366 100644 --- a/src/Core/src/Handlers/Editor/EditorHandler.iOS.cs +++ b/src/Core/src/Handlers/Editor/EditorHandler.iOS.cs @@ -1,27 +1,32 @@ -using CoreGraphics; +using CoreGraphics; using Foundation; +using Microsoft.Maui.Platform.iOS; using System; using Microsoft.Extensions.DependencyInjection; using UIKit; namespace Microsoft.Maui.Handlers { - public partial class EditorHandler : AbstractViewHandler + public partial class EditorHandler : AbstractViewHandler { static readonly int BaseHeight = 30; - protected override UITextView CreateNativeView() + static readonly UIColor DefaultPlaceholderColor = ColorExtensions.PlaceholderColor; + + protected override MauiTextView CreateNativeView() { - return new UITextView(CGRect.Empty); + return new MauiTextView(CGRect.Empty); } - protected override void ConnectHandler(UITextView nativeView) + protected override void ConnectHandler(MauiTextView nativeView) { + nativeView.Changed += OnChanged; nativeView.ShouldChangeText += OnShouldChangeText; } - protected override void DisconnectHandler(UITextView nativeView) + protected override void DisconnectHandler(MauiTextView nativeView) { + nativeView.Changed -= OnChanged; nativeView.ShouldChangeText -= OnShouldChangeText; } @@ -36,6 +41,16 @@ public static void MapText(EditorHandler handler, IEditor editor) MapFormatting(handler, editor); } + public static void MapPlaceholder(EditorHandler handler, IEditor editor) + { + handler.TypedNativeView?.UpdatePlaceholder(editor); + } + + public static void MapPlaceholderColor(EditorHandler handler, IEditor editor) + { + handler.TypedNativeView?.UpdatePlaceholderColor(editor, DefaultPlaceholderColor); + } + public static void MapCharacterSpacing(EditorHandler handler, IEditor editor) { handler.TypedNativeView?.UpdateCharacterSpacing(editor); @@ -59,6 +74,16 @@ public static void MapFormatting(EditorHandler handler, IEditor editor) handler.TypedNativeView?.UpdateCharacterSpacing(editor); } + void OnChanged(object? sender, System.EventArgs e) => OnTextChanged(); + + void OnTextChanged() + { + if (TypedNativeView == null) + return; + + TypedNativeView.HidePlaceholder(!string.IsNullOrEmpty(TypedNativeView.Text)); + } + bool OnShouldChangeText(UITextView textView, NSRange range, string replacementString) { var currLength = textView?.Text?.Length ?? 0; diff --git a/src/Core/src/Platform/Android/EditTextExtensions.cs b/src/Core/src/Platform/Android/EditTextExtensions.cs index aa05e90df8b7..b95a5970d490 100644 --- a/src/Core/src/Platform/Android/EditTextExtensions.cs +++ b/src/Core/src/Platform/Android/EditTextExtensions.cs @@ -95,12 +95,31 @@ public static void UpdateMaxLength(this AppCompatEditText editText, int maxLengt editText.Text = TrimToMaxLength(editText.Text, maxLength); } - public static void UpdatePlaceholder(this AppCompatEditText editText, IEntry entry) + public static void UpdatePlaceholder(this AppCompatEditText editText, IPlaceholder textInput) { - if (editText.Hint == entry.Placeholder) + if (editText.Hint == textInput.Placeholder) return; - editText.Hint = entry.Placeholder; + editText.Hint = textInput.Placeholder; + } + + public static void UpdatePlaceholderColor(this AppCompatEditText editText, IEditor editor, ColorStateList? defaultColor) + { + var placeholderTextColor = editor.PlaceholderColor; + if (placeholderTextColor.IsDefault) + { + editText.SetHintTextColor(defaultColor); + } + else + { + var androidColor = placeholderTextColor.ToNative(); + + if (!editText.HintTextColors.IsOneColor(ColorExtensions.States, androidColor)) + { + var acolor = androidColor.ToArgb(); + editText.SetHintTextColor(new ColorStateList(ColorExtensions.States, new[] { acolor, acolor })); + } + } } public static void UpdateIsReadOnly(this AppCompatEditText editText, IEntry entry) diff --git a/src/Core/src/Platform/iOS/EditorExtensions.cs b/src/Core/src/Platform/iOS/EditorExtensions.cs new file mode 100644 index 000000000000..6c2287d34fec --- /dev/null +++ b/src/Core/src/Platform/iOS/EditorExtensions.cs @@ -0,0 +1,22 @@ +using UIKit; +using Microsoft.Maui.Platform.iOS; + +namespace Microsoft.Maui +{ + public static class EditorExtensions + { + public static void UpdatePlaceholder(this MauiTextView textView, IEditor editor) + { + textView.PlaceholderText = editor.Placeholder; + } + + public static void UpdatePlaceholderColor(this MauiTextView textView, IEditor editor, UIColor? defaultPlaceholderColor) + { + Color placeholderColor = editor.PlaceholderColor; + if (placeholderColor.IsDefault) + textView.PlaceholderTextColor = defaultPlaceholderColor; + else + textView.PlaceholderTextColor = placeholderColor.ToNative(); + } + } +} diff --git a/src/Core/src/Platform/iOS/MauiTextView.cs b/src/Core/src/Platform/iOS/MauiTextView.cs new file mode 100644 index 000000000000..3a07c9208afa --- /dev/null +++ b/src/Core/src/Platform/iOS/MauiTextView.cs @@ -0,0 +1,67 @@ +using UIKit; +using CoreGraphics; +using Foundation; + +namespace Microsoft.Maui.Platform.iOS +{ + public class MauiTextView : UITextView + { + UILabel PlaceholderLabel { get; } = new UILabel + { + BackgroundColor = UIColor.Clear, + Lines = 0 + }; + + public MauiTextView(CGRect frame) : base(frame) + { + InitPlaceholderLabel(); + } + + public string? PlaceholderText + { + get => PlaceholderLabel.Text; + set + { + PlaceholderLabel.Text = value; + PlaceholderLabel.SizeToFit(); + } + } + + public UIColor? PlaceholderTextColor + { + get => PlaceholderLabel.TextColor; + set => PlaceholderLabel.TextColor = value; + } + + public void HidePlaceholder(bool hide) + { + PlaceholderLabel.Hidden = hide; + } + + void InitPlaceholderLabel() + { + AddSubview(PlaceholderLabel); + + var edgeInsets = TextContainerInset; + var lineFragmentPadding = TextContainer.LineFragmentPadding; + + var vConstraints = NSLayoutConstraint.FromVisualFormat( + "V:|-" + edgeInsets.Top + "-[PlaceholderLabel]-" + edgeInsets.Bottom + "-|", 0, new NSDictionary(), + NSDictionary.FromObjectsAndKeys( + new NSObject[] { PlaceholderLabel }, new NSObject[] { new NSString("PlaceholderLabel") }) + ); + + var hConstraints = NSLayoutConstraint.FromVisualFormat( + "H:|-" + lineFragmentPadding + "-[PlaceholderLabel]-" + lineFragmentPadding + "-|", + 0, new NSDictionary(), + NSDictionary.FromObjectsAndKeys( + new NSObject[] { PlaceholderLabel }, new NSObject[] { new NSString("PlaceholderLabel") }) + ); + + PlaceholderLabel.TranslatesAutoresizingMaskIntoConstraints = false; + + AddConstraints(hConstraints); + AddConstraints(vConstraints); + } + } +} diff --git a/src/Core/src/Primitives/Color.cs b/src/Core/src/Primitives/Color.cs index 2e2f05bd1563..27432166af2b 100644 --- a/src/Core/src/Primitives/Color.cs +++ b/src/Core/src/Primitives/Color.cs @@ -2,7 +2,6 @@ using System.ComponentModel; using System.Diagnostics; using System.Globalization; -using Microsoft.Maui; namespace Microsoft.Maui { diff --git a/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.Android.cs b/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.Android.cs index a57ffc3b6ea9..33e202716c4d 100644 --- a/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.Android.cs +++ b/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.Android.cs @@ -69,6 +69,12 @@ public async Task FontFamilyInitializesCorrectly(string family) string GetNativeText(EditorHandler editorHandler) => GetNativeEditor(editorHandler).Text; + string GetNativePlaceholderText(EditorHandler editorHandler) => + GetNativeEditor(editorHandler).Hint; + + Color GetNativePlaceholderColor(EditorHandler editorHandler) => + ((uint)GetNativeEditor(editorHandler).CurrentHintTextColor).ToColor(); + bool GetNativeIsTextPredictionEnabled(EditorHandler editorHandler) => !GetNativeEditor(editorHandler).InputType.HasFlag(InputTypes.TextFlagNoSuggestions); diff --git a/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.cs b/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.cs index 2b622af3f9ce..88175d37921a 100644 --- a/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.cs +++ b/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.cs @@ -42,6 +42,73 @@ public async Task TextUpdatesCorrectly(string setValue, string unsetValue) unsetValue); } + [Fact(DisplayName = "PlaceholderColor Initializes Correctly")] + public async Task PlaceholderColorInitializesCorrectly() + { + var editor = new EditorStub() + { + Placeholder = "Test", + PlaceholderColor = Color.Yellow + }; + + await ValidatePropertyInitValue(editor, () => editor.PlaceholderColor, GetNativePlaceholderColor, editor.PlaceholderColor); + } + + [Theory(DisplayName = "PlaceholderColor Updates Correctly")] + [InlineData(0xFF0000, 0x0000FF)] + [InlineData(0x0000FF, 0xFF0000)] + public async Task PlaceholderColorUpdatesCorrectly(uint setValue, uint unsetValue) + { + var editor = new EditorStub + { + Placeholder = "Placeholder" + }; + + var setColor = Color.FromUint(setValue); + var unsetColor = Color.FromUint(unsetValue); + + await ValidatePropertyUpdatesValue( + editor, + nameof(IEditor.PlaceholderColor), + GetNativePlaceholderColor, + setColor, + unsetColor); + } + + [Fact(DisplayName = "PlaceholderText Initializes Correctly")] + public async Task PlaceholderTextInitializesCorrectly() + { + var editor = new EditorStub() + { + Text = "Test" + }; + + await ValidatePropertyInitValue(editor, () => editor.Placeholder, GetNativePlaceholderText, editor.Placeholder); + } + + [Theory(DisplayName = "PlaceholderText Updates Correctly")] + [InlineData(null, null)] + [InlineData(null, "Hello")] + [InlineData("Hello", null)] + [InlineData("Hello", "Goodbye")] + public async Task PlaceholderTextUpdatesCorrectly(string setValue, string unsetValue) + { + var editor = new EditorStub(); + + await ValidatePropertyUpdatesValue( + editor, + nameof(IEditor.Placeholder), + h => + { + var n = GetNativePlaceholderText(h); + if (string.IsNullOrEmpty(n)) + n = null; // native platforms may not support null text + return n; + }, + setValue, + unsetValue); + } + [Theory(DisplayName = "MaxLength Initializes Correctly")] [InlineData(2)] [InlineData(5)] diff --git a/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.iOS.cs b/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.iOS.cs index c481264375c3..38c3ac910722 100644 --- a/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.iOS.cs +++ b/src/Core/tests/DeviceTests/Handlers/Editor/EditorHandlerTests.iOS.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using Microsoft.Maui.Platform.iOS; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.DeviceTests.Stubs; using Microsoft.Maui.Handlers; @@ -60,12 +61,18 @@ public async Task FontFamilyInitializesCorrectly(string family) Assert.NotEqual(fontManager.DefaultFont.FamilyName, nativeFont.FamilyName); } - UITextView GetNativeEditor(EditorHandler editorHandler) => - (UITextView)editorHandler.View; + MauiTextView GetNativeEditor(EditorHandler editorHandler) => + (MauiTextView)editorHandler.View; string GetNativeText(EditorHandler editorHandler) => GetNativeEditor(editorHandler).Text; + string GetNativePlaceholderText(EditorHandler editorHandler) => + GetNativeEditor(editorHandler).PlaceholderText; + + Color GetNativePlaceholderColor(EditorHandler editorHandler) => + GetNativeEditor(editorHandler).PlaceholderTextColor.ToColor(); + double GetNativeCharacterSpacing(EditorHandler editorHandler) { var editor = GetNativeEditor(editorHandler); diff --git a/src/Core/tests/DeviceTests/Stubs/EditorStub.cs b/src/Core/tests/DeviceTests/Stubs/EditorStub.cs index 0c5185e49e61..d45be41b28e0 100644 --- a/src/Core/tests/DeviceTests/Stubs/EditorStub.cs +++ b/src/Core/tests/DeviceTests/Stubs/EditorStub.cs @@ -18,6 +18,12 @@ public string Text public double CharacterSpacing { get; set; } + public bool IsReadOnly { get;set; } + + public string Placeholder { get;set; } + + public Color PlaceholderColor { get; set; } + public int MaxLength { get; set; } = int.MaxValue; public bool IsTextPredictionEnabled { get; set; }