Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement MaxLength property in EntryHandlers #541

Merged
merged 4 commits into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ void OnKeyboardBackPressed(object sender, EventArgs eventArgs)
Control?.ClearFocus();
}

[PortHandler]
void UpdateMaxLength()
{
var currentFilters = new List<IInputFilter>(EditText?.GetFilters() ?? new IInputFilter[0]);
Expand Down
2 changes: 2 additions & 0 deletions src/Compatibility/Core/src/iOS/Renderers/EntryRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ void UpdateCharacterSpacing()
UpdateAttributedPlaceholder(placeHolder);
}

[PortHandler]
void UpdateMaxLength()
{
var currentControlText = Control.Text;
Expand All @@ -396,6 +397,7 @@ void UpdateMaxLength()
Control.Text = currentControlText.Substring(0, Element.MaxLength);
}

[PortHandler]
bool ShouldChangeCharacters(UITextField textField, NSRange range, string replacementString)
{
var newLength = textField?.Text?.Length + replacementString?.Length - range.Length;
Expand Down
1 change: 1 addition & 0 deletions src/Controls/samples/Controls.Sample/Pages/MainPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ void SetupMauiLayout()
verticalStack.Add(new Entry { IsTextPredictionEnabled = false });
verticalStack.Add(new Entry { Placeholder = "This should be placeholder text" });
verticalStack.Add(new Entry { Text = "This should be read only property", IsReadOnly = true });
verticalStack.Add(new Entry { MaxLength = 5, Placeholder = "MaxLength text" });

verticalStack.Add(new ProgressBar { Progress = 0.5 });
verticalStack.Add(new ProgressBar { Progress = 0.5, BackgroundColor = Color.LightCoral });
Expand Down
5 changes: 5 additions & 0 deletions src/Core/src/Core/ITextInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ public interface ITextInput : IText, IPlaceholder
/// Gets a value indicating whether or not the view is read-only.
/// </summary>
bool IsReadOnly { get; }

/// <summary>
/// Gets the maximum allowed length of input.
/// </summary>
int MaxLength { get; }
}
}
5 changes: 5 additions & 0 deletions src/Core/src/Handlers/Entry/EntryHandler.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public static void MapIsTextPredictionEnabled(EntryHandler handler, IEntry entry
handler.TypedNativeView?.UpdateIsTextPredictionEnabled(entry);
}

public static void MapMaxLength(EntryHandler handler, IEntry entry)
{
handler.TypedNativeView?.UpdateMaxLength(entry);
}

public static void MapPlaceholder(EntryHandler handler, IEntry entry)
{
handler.TypedNativeView?.UpdatePlaceholder(entry);
Expand Down
1 change: 1 addition & 0 deletions src/Core/src/Handlers/Entry/EntryHandler.Standard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static void MapTextColor(IViewHandler handler, IEntry entry) { }
public static void MapIsPassword(IViewHandler handler, IEntry entry) { }
public static void MapHorizontalTextAlignment(IViewHandler handler, IEntry entry) { }
public static void MapIsTextPredictionEnabled(IViewHandler handler, IEntry entry) { }
public static void MapMaxLength(IViewHandler handler, IEntry entry) { }
public static void MapPlaceholder(IViewHandler handler, IEntry entry) { }
public static void MapIsReadOnly(IViewHandler handler, IEntry entry) { }
public static void MapFont(IViewHandler handler, IEntry entry) { }
Expand Down
1 change: 1 addition & 0 deletions src/Core/src/Handlers/Entry/EntryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public partial class EntryHandler
[nameof(IEntry.IsPassword)] = MapIsPassword,
[nameof(IEntry.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
[nameof(IEntry.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled,
[nameof(IEntry.MaxLength)] = MapMaxLength,
[nameof(IEntry.Placeholder)] = MapPlaceholder,
[nameof(IEntry.IsReadOnly)] = MapIsReadOnly,
[nameof(IEntry.Font)] = MapFont,
Expand Down
50 changes: 46 additions & 4 deletions src/Core/src/Handlers/Entry/EntryHandler.iOS.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Foundation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Platform.iOS;
using UIKit;
Expand All @@ -25,13 +26,15 @@ protected override void ConnectHandler(MauiTextField nativeView)
nativeView.EditingChanged += OnEditingChanged;
nativeView.EditingDidEnd += OnEditingEnded;
nativeView.TextPropertySet += OnTextPropertySet;
nativeView.ShouldChangeCharacters += OnShouldChangeCharacters;
}

protected override void DisconnectHandler(MauiTextField nativeView)
{
nativeView.EditingChanged -= OnEditingChanged;
nativeView.EditingDidEnd -= OnEditingEnded;
nativeView.TextPropertySet -= OnTextPropertySet;
nativeView.ShouldChangeCharacters -= OnShouldChangeCharacters;
}

protected override void SetupDefaults(MauiTextField nativeView)
Expand All @@ -45,6 +48,9 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra
public static void MapText(EntryHandler handler, IEntry entry)
{
handler.TypedNativeView?.UpdateText(entry);

// Any text update requires that we update any attributed string formatting
MapFormatting(handler, entry);
}

public static void MapTextColor(EntryHandler handler, IEntry entry)
Expand All @@ -67,6 +73,11 @@ public static void MapIsTextPredictionEnabled(EntryHandler handler, IEntry entry
handler.TypedNativeView?.UpdateIsTextPredictionEnabled(entry);
}

public static void MapMaxLength(EntryHandler handler, IEntry entry)
{
handler.TypedNativeView?.UpdateMaxLength(entry);
}

public static void MapPlaceholder(EntryHandler handler, IEntry entry)
{
handler.TypedNativeView?.UpdatePlaceholder(entry);
Expand All @@ -82,6 +93,27 @@ public static void MapReturnType(EntryHandler handler, IEntry entry)
handler.TypedNativeView?.UpdateReturnType(entry);
}

public static void MapFont(EntryHandler handler, IEntry entry)
{
_ = handler.Services ?? throw new InvalidOperationException($"{nameof(Services)} should have been set by base class.");

var fontManager = handler.Services.GetRequiredService<IFontManager>();

handler.TypedNativeView?.UpdateFont(entry, fontManager);
}

public static void MapFormatting(EntryHandler handler, IEntry entry)
{
handler.TypedNativeView?.UpdateMaxLength(entry);

// Update all of the attributed text formatting properties
handler.TypedNativeView?.UpdateCharacterSpacing(entry);

// Setting any of those may have removed text alignment settings,
// so we need to make sure those are applied, too
handler.TypedNativeView?.UpdateHorizontalTextAlignment(entry);
}

void OnEditingChanged(object? sender, EventArgs e) => OnTextChanged();

void OnEditingEnded(object? sender, EventArgs e) => OnTextChanged();
Expand All @@ -101,13 +133,23 @@ void OnTextChanged()
VirtualView.Text = nativeText;
}

public static void MapFont(EntryHandler handler, IEntry entry)
bool OnShouldChangeCharacters(UITextField textField, NSRange range, string replacementString)
{
_ = handler.Services ?? throw new InvalidOperationException($"{nameof(Services)} should have been set by base class.");
var currLength = textField?.Text?.Length ?? 0;

var fontManager = handler.Services.GetRequiredService<IFontManager>();
// fix a crash on undo
if (range.Length + range.Location > currLength)
return false;

handler.TypedNativeView?.UpdateFont(entry, fontManager);
if (VirtualView == null || TypedNativeView == null)
return false;

var addLength = replacementString?.Length ?? 0;
var remLength = range.Length;

var newLength = currLength + addLength - remLength;

return newLength <= VirtualView.MaxLength;
}
}
}
25 changes: 22 additions & 3 deletions src/Core/src/Platform/Android/EditTextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Android.Content.Res;
using System.Collections.Generic;
using Android.Content.Res;
using Android.Text;
using Android.Util;
using AndroidX.AppCompat.Widget;
Expand Down Expand Up @@ -76,9 +77,27 @@ public static void UpdateIsTextPredictionEnabled(this AppCompatEditText editText
public static void UpdateIsTextPredictionEnabled(this AppCompatEditText editText, IEditor editor)
{
if (editor.IsTextPredictionEnabled)
return;
editText.InputType &= ~InputTypes.TextFlagNoSuggestions;
else
editText.InputType |= InputTypes.TextFlagNoSuggestions;
}

public static void UpdateMaxLength(this AppCompatEditText editText, IEntry entry)
{
var currentFilters = new List<IInputFilter>(editText?.GetFilters() ?? new IInputFilter[0]);

for (var i = 0; i < currentFilters.Count; i++)
{
if (currentFilters[i] is InputFilterLengthFilter)
{
currentFilters.RemoveAt(i);
break;
}
}

currentFilters.Add(new InputFilterLengthFilter(entry.MaxLength));

editText.InputType |= InputTypes.TextFlagNoSuggestions;
editText?.SetFilters(currentFilters.ToArray());
}

public static void UpdatePlaceholder(this AppCompatEditText editText, IEntry entry)
Expand Down
8 changes: 8 additions & 0 deletions src/Core/src/Platform/iOS/TextFieldExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ public static void UpdateIsTextPredictionEnabled(this UITextField textField, IEn
textField.AutocorrectionType = UITextAutocorrectionType.No;
}

public static void UpdateMaxLength(this UITextField textField, IEntry entry)
{
var currentControlText = textField.Text;

if (currentControlText?.Length > entry.MaxLength)
textField.Text = currentControlText.Substring(0, entry.MaxLength);
}

public static void UpdatePlaceholder(this UITextField textField, IEntry entry)
{
textField.Placeholder = entry.Placeholder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,22 @@ public async Task IsTextPredictionEnabledCorrectly(bool isEnabled)

await ValidatePropertyInitValue(editor, () => editor.IsTextPredictionEnabled, GetNativeIsTextPredictionEnabled, isEnabled);
}

[Theory(DisplayName = "IsTextPredictionEnabled Updates Correctly")]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public async Task IsTextPredictionEnabledUpdatesCorrectly(bool setValue, bool unsetValue)
{
var editor = new EditorStub();

await ValidatePropertyUpdatesValue(
editor,
nameof(IEditor.IsTextPredictionEnabled),
GetNativeIsTextPredictionEnabled,
setValue,
unsetValue);
}
}
}
53 changes: 50 additions & 3 deletions src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.Maui.DeviceTests.Stubs;
using Microsoft.Maui.Handlers;
using Xunit;
Expand Down Expand Up @@ -237,5 +236,53 @@ public async Task TextChangedEventsFireCorrectly(string initialText, string newT
else
Assert.Equal(0, eventFiredCount);
}

[Theory(DisplayName = "MaxLength Initializes Correctly")]
[InlineData(2)]
[InlineData(5)]
[InlineData(8)]
[InlineData(10)]
public async Task MaxLengthInitializesCorrectly(int maxLength)
{
const string text = "Lorem ipsum dolor sit amet";
var expectedText = text.Substring(0, maxLength);

var entry = new EntryStub()
{
MaxLength = maxLength,
Text = text
};

var nativeText = await GetValueAsync(entry, GetNativeText);

Assert.Equal(expectedText, nativeText);
Assert.Equal(expectedText, entry.Text);
}

[Theory(DisplayName = "MaxLength Clips Native Text Correctly")]
[InlineData(2)]
[InlineData(5)]
[InlineData(8)]
[InlineData(10)]
public async Task MaxLengthClipsNativeTextCorrectly(int maxLength)
{
const string text = "Lorem ipsum dolor sit amet";
var expectedText = text.Substring(0, maxLength);

var entry = new EntryStub()
{
MaxLength = maxLength,
};

var nativeText = await GetValueAsync(entry, handler =>
{
entry.Text = text;

return GetNativeText(handler);
});

Assert.Equal(expectedText, nativeText);
Assert.Equal(expectedText, entry.Text);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ public async Task ReturnTypeInitializesCorrectly()
Assert.Equal(expectedValue, values.NativeViewValue);
}


UITextField GetNativeEntry(EntryHandler entryHandler) =>
(UITextField)entryHandler.View;

Expand Down
2 changes: 2 additions & 0 deletions src/Core/tests/DeviceTests/Stubs/EntryStub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public string Text

public Font Font { get; set; }

public int MaxLength { get; set; } = int.MaxValue;

public TextAlignment HorizontalTextAlignment { get; set; }

public ReturnType ReturnType { get; set; }
Expand Down