Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
66 changes: 65 additions & 1 deletion NiceEntry/LabeledAutoCompleteEntry.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,50 @@ public LabeledAutoCompleteEntry()
defaultValue: TextAlignment.Start,
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.HorizontalTextAlignment = (TextAlignment)n);

public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(
nameof(FontSize), typeof(double), typeof(LabeledAutoCompleteEntry), defaultValue: LabelBase.DefaultFontSize,
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.FontSize = (double)n);

public static readonly BindableProperty PlaceholderColorProperty = BindableProperty.Create(
nameof(PlaceholderColor), typeof(Color), typeof(LabeledAutoCompleteEntry), defaultValue: Color.FromArgb("#808080"),
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.PlaceholderColor = (Color)n);

public static readonly BindableProperty IsPasswordProperty = BindableProperty.Create(
nameof(IsPassword), typeof(bool), typeof(LabeledAutoCompleteEntry), defaultValue: false,
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.IsPassword = (bool)n);

public static readonly BindableProperty IsReadOnlyProperty = BindableProperty.Create(
nameof(IsReadOnly), typeof(bool), typeof(LabeledAutoCompleteEntry), defaultValue: false,
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.IsReadOnly = (bool)n);

public static readonly BindableProperty SelectAllOnFocusProperty = BindableProperty.Create(
nameof(SelectAllOnFocus), typeof(bool), typeof(LabeledAutoCompleteEntry), defaultValue: true,
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.SelectAllOnFocus = (bool)n);

public static readonly BindableProperty ExampleProperty = BindableProperty.Create(
nameof(Example), typeof(string), typeof(LabeledAutoCompleteEntry),
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.Example = (string)n);

public static readonly BindableProperty ContentPaddingProperty = BindableProperty.Create(
nameof(ContentPadding), typeof(Thickness), typeof(LabeledAutoCompleteEntry),
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.ContentPadding = (Thickness)n);
Comment thread
nord- marked this conversation as resolved.

public static readonly BindableProperty UnitProperty = BindableProperty.Create(
nameof(Unit), typeof(string), typeof(LabeledAutoCompleteEntry),
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.Unit = (string)n);

public static readonly BindableProperty UnitFontFamilyProperty = BindableProperty.Create(
nameof(UnitFontFamily), typeof(string), typeof(LabeledAutoCompleteEntry),
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.UnitFontFamily = (string)n);

public static readonly BindableProperty UnitFontSizeProperty = BindableProperty.Create(
nameof(UnitFontSize), typeof(double), typeof(LabeledAutoCompleteEntry), defaultValue: LabelBase.DefaultFontSize,
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.UnitFontSize = (double)n);

public static readonly BindableProperty UnitColorProperty = BindableProperty.Create(
nameof(UnitColor), typeof(Color), typeof(LabeledAutoCompleteEntry),
propertyChanged: (b, _, n) => ((LabeledAutoCompleteEntry)b).Entry.UnitColor = (Color)n);

// Local to the autocomplete control
public static readonly BindableProperty SuggestionsProperty = BindableProperty.Create(
nameof(Suggestions), typeof(IEnumerable), typeof(LabeledAutoCompleteEntry),
Expand All @@ -101,6 +145,11 @@ public LabeledAutoCompleteEntry()
public static readonly BindableProperty CommitOnUpperCaseProperty = BindableProperty.Create(
nameof(CommitOnUpperCase), typeof(bool), typeof(LabeledAutoCompleteEntry), defaultValue: false);

// Extracts the committed text from a selected suggestion. When null, ToString() is used,
// which only makes sense for string suggestions or objects with a meaningful ToString().
public static readonly BindableProperty SuggestionTextSelectorProperty = BindableProperty.Create(
nameof(SuggestionTextSelector), typeof(Func<object, string>), typeof(LabeledAutoCompleteEntry));

public string Label { get => (string)GetValue(LabelProperty); set => SetValue(LabelProperty, value); }
public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
public string Placeholder { get => (string)GetValue(PlaceholderProperty); set => SetValue(PlaceholderProperty, value); }
Expand All @@ -115,6 +164,18 @@ public LabeledAutoCompleteEntry()
public int MaxSuggestions { get => (int)GetValue(MaxSuggestionsProperty); set => SetValue(MaxSuggestionsProperty, value); }
public DataTemplate SuggestionTemplate { get => (DataTemplate)GetValue(SuggestionTemplateProperty); set => SetValue(SuggestionTemplateProperty, value); }
public bool CommitOnUpperCase { get => (bool)GetValue(CommitOnUpperCaseProperty); set => SetValue(CommitOnUpperCaseProperty, value); }
public Func<object, string>? SuggestionTextSelector { get => (Func<object, string>?)GetValue(SuggestionTextSelectorProperty); set => SetValue(SuggestionTextSelectorProperty, value); }
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
public Color PlaceholderColor { get => (Color)GetValue(PlaceholderColorProperty); set => SetValue(PlaceholderColorProperty, value); }
public bool IsPassword { get => (bool)GetValue(IsPasswordProperty); set => SetValue(IsPasswordProperty, value); }
public bool IsReadOnly { get => (bool)GetValue(IsReadOnlyProperty); set => SetValue(IsReadOnlyProperty, value); }
public bool SelectAllOnFocus { get => (bool)GetValue(SelectAllOnFocusProperty); set => SetValue(SelectAllOnFocusProperty, value); }
public string Example { get => (string)GetValue(ExampleProperty); set => SetValue(ExampleProperty, value); }
public Thickness ContentPadding { get => (Thickness)GetValue(ContentPaddingProperty); set => SetValue(ContentPaddingProperty, value); }
public string Unit { get => (string)GetValue(UnitProperty); set => SetValue(UnitProperty, value); }
public string UnitFontFamily { get => (string)GetValue(UnitFontFamilyProperty); set => SetValue(UnitFontFamilyProperty, value); }
public double UnitFontSize { get => (double)GetValue(UnitFontSizeProperty); set => SetValue(UnitFontSizeProperty, value); }
public Color UnitColor { get => (Color)GetValue(UnitColorProperty); set => SetValue(UnitColorProperty, value); }

private static void TextChanged(BindableObject bindable, object oldValue, object newValue)
{
Expand Down Expand Up @@ -197,7 +258,10 @@ private void OnSuggestionTapped(object? sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection.Count == 0) return;

var picked = e.CurrentSelection[0]?.ToString();
var selected = e.CurrentSelection[0];
var picked = selected is null
? null
: SuggestionTextSelector?.Invoke(selected) ?? selected.ToString();
SuggestionsView.SelectedItem = null;

if (picked is null) return;
Expand Down
61 changes: 61 additions & 0 deletions NiceEntry/NiceButton.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,51 @@
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Layouts;
using System.Windows.Input;

namespace NiceEntry;

public class NiceButton : Layout
{
static NiceButton()
{
// Announce the control as a button to screen readers. The Layout root renders as a
// plain container by default, so without this TalkBack/VoiceOver wouldn't expose a
// button role. The accessible name comes from SemanticProperties.Description (set from
// Text or SemanticDescription). Filtered on the NiceButton type so other layouts are
// unaffected.
#if ANDROID
LayoutHandler.Mapper.AppendToMapping("NiceButtonAccessibility", (handler, view) =>
{
if (view is NiceButton)
{
handler.PlatformView.ImportantForAccessibility = Android.Views.ImportantForAccessibility.Yes;
handler.PlatformView.SetAccessibilityDelegate(new ButtonAccessibilityDelegate());
}
});
#elif IOS
LayoutHandler.Mapper.AppendToMapping("NiceButtonAccessibility", (handler, view) =>
{
if (view is NiceButton)
{
handler.PlatformView.IsAccessibilityElement = true;
handler.PlatformView.AccessibilityTraits = UIKit.UIAccessibilityTrait.Button;
}
});
#endif
}

#if ANDROID
private sealed class ButtonAccessibilityDelegate : Android.Views.View.AccessibilityDelegate
{
public override void OnInitializeAccessibilityNodeInfo(Android.Views.View host, Android.Views.Accessibility.AccessibilityNodeInfo info)
{
base.OnInitializeAccessibilityNodeInfo(host, info);
info.ClassName = "android.widget.Button";
}
}
#endif

/// <summary>Default font size for button text (not the field-tuned LabelBase value).</summary>
public const double DefaultFontSize = 14.0;

Expand Down Expand Up @@ -87,6 +127,7 @@ public NiceButton()
UpdateBorderStrokeView();
ApplyColors();
UpdateShadowView();
UpdateSemanticDescription();
Comment thread
nord- marked this conversation as resolved.
}

/// <summary>True when the button must be measured square (Circle shape).</summary>
Expand All @@ -102,6 +143,9 @@ public NiceButton()
public static readonly BindableProperty IconProperty = BindableProperty.Create(
nameof(Icon), typeof(MaterialIcon?), typeof(NiceButton), null, propertyChanged: IconChanged);

public static readonly BindableProperty SemanticDescriptionProperty = BindableProperty.Create(
nameof(SemanticDescription), typeof(string), typeof(NiceButton), null, propertyChanged: SemanticDescriptionChanged);

public static readonly BindableProperty OrientationProperty = BindableProperty.Create(
nameof(Orientation), typeof(ButtonContentOrientation), typeof(NiceButton),
ButtonContentOrientation.Horizontal, propertyChanged: LayoutAffectingChanged);
Expand Down Expand Up @@ -166,6 +210,12 @@ public NiceButton()

public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
public MaterialIcon? Icon { get => (MaterialIcon?)GetValue(IconProperty); set => SetValue(IconProperty, value); }

/// <summary>
/// Accessible name announced by screen readers. Defaults to <see cref="Text"/> when not set;
/// set this explicitly for icon-only buttons, which otherwise have no accessible name.
/// </summary>
public string SemanticDescription { get => (string)GetValue(SemanticDescriptionProperty); set => SetValue(SemanticDescriptionProperty, value); }
public ButtonContentOrientation Orientation { get => (ButtonContentOrientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); }
public IconPlacement IconPlacement { get => (IconPlacement)GetValue(IconPlacementProperty); set => SetValue(IconPlacementProperty, value); }
public double Spacing { get => (double)GetValue(SpacingProperty); set => SetValue(SpacingProperty, value); }
Expand Down Expand Up @@ -240,8 +290,19 @@ private static void TextChanged(BindableObject b, object o, object n)
}
else
btn.RebuildContent();

btn.UpdateSemanticDescription();
}

private static void SemanticDescriptionChanged(BindableObject b, object o, object n)
=> ((NiceButton)b).UpdateSemanticDescription();

// Prefer the explicit SemanticDescription; fall back to Text so text buttons are announced
// without extra configuration. Icon-only buttons have no Text, so SemanticDescription is the
// only way to give them an accessible name.
private void UpdateSemanticDescription()
=> SemanticProperties.SetDescription(this, string.IsNullOrEmpty(SemanticDescription) ? Text : SemanticDescription);

private static void IconChanged(BindableObject b, object o, object n)
{
var btn = (NiceButton)b;
Expand Down
1 change: 1 addition & 0 deletions NiceEntryDemoApp/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@

<ne:LabeledAutoCompleteEntry Label="ICAO-code"
Placeholder="e.g. ESGG"
Example="4-letter ICAO identifier"
CommitOnUpperCase="True"
MaxSuggestions="6"
Text="{Binding IcaoText}"
Expand Down
Loading