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 @@ AppCompatEditText GetNativeEditor(EditorHandler editorHandler) =>
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 @@ await ValidatePropertyUpdatesValue(
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; }