Skip to content

Commit 477f36d

Browse files
Implemented Accessibility Support to TextInputLayout
1 parent e6c84de commit 477f36d

File tree

3 files changed

+82
-225
lines changed

3 files changed

+82
-225
lines changed

maui/src/TextInputLayout/SfTextInputLayout.Methods.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ internal void OnTextInputViewTextChanged(object? sender, TextChangedEventArgs e)
170170
InvalidateDrawable();
171171
}
172172

173+
SetCustomDescription(this.Content);
174+
173175
// Clear icon can't draw when isClearIconVisible property updated based on text.
174176
// So here call the InvalidateDrawable to draw the clear icon.
175177
if (Text?.Length <= 1)

maui/src/TextInputLayout/SfTextInputLayout.Properties.cs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,11 @@ static LabelStyle GetCounterLabelStyleDefaultValue()
753753
public bool ShowHint
754754
{
755755
get { return (bool)GetValue(ShowHintProperty); }
756-
set { SetValue(ShowHintProperty, value); }
756+
set
757+
{
758+
SetValue(ShowHintProperty, value);
759+
SetCustomDescription(this.Content);
760+
}
757761
}
758762

759763
/// <summary>
@@ -1274,8 +1278,12 @@ public bool IsHintAlwaysFloated
12741278
/// </example>
12751279
public string Hint
12761280
{
1277-
get => (string)GetValue(HintProperty);
1278-
set => SetValue(HintProperty, value);
1281+
get { return (string)GetValue(HintProperty); }
1282+
set
1283+
{
1284+
SetValue(HintProperty, value);
1285+
SetCustomDescription(this.Content);
1286+
}
12791287
}
12801288

12811289
/// <summary>
@@ -1755,7 +1763,11 @@ internal bool IsFilled
17551763
internal bool IsHintFloated
17561764
{
17571765
get { return (bool)GetValue(IsHintFloatedProperty); }
1758-
set { SetValue(IsHintFloatedProperty, value); }
1766+
set
1767+
{
1768+
SetValue(IsHintFloatedProperty, value);
1769+
SetCustomDescription(this.Content);
1770+
}
17591771
}
17601772

17611773
/// <summary>
@@ -1982,15 +1994,6 @@ bool IsPassowordToggleIconVisible
19821994
get { return (EnablePasswordVisibilityToggle && Content is Entry); }
19831995
}
19841996

1985-
/// <summary>
1986-
/// Gets a value indicating whether the SemanticProperties.Description is not set by user.
1987-
/// </summary>
1988-
internal bool IsDescriptionNotSetByUser
1989-
{
1990-
get { return _isDescriptionNotSetByUser; }
1991-
set { _isDescriptionNotSetByUser = value; }
1992-
}
1993-
19941997
#endregion
19951998

19961999
#region Property Changed Methods

maui/src/TextInputLayout/SfTextInputLayout.cs

Lines changed: 64 additions & 212 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,6 @@ public partial class SfTextInputLayout : SfContentView, ITouchListener, IParentT
226226
internal bool IsIconPressed { get; private set; } = false;
227227
#endif
228228

229-
/// <summary>
230-
/// Indicates if the description has been set by the user.
231-
/// </summary>
232-
bool _isDescriptionNotSetByUser;
233-
234229
/// <summary>
235230
/// Gets or sets a value indicating whether the layout has been tapped.
236231
/// </summary>
@@ -348,7 +343,9 @@ public partial class SfTextInputLayout : SfContentView, ITouchListener, IParentT
348343
UIKit.UITextField? uiEntry;
349344
#endif
350345

351-
#endregion
346+
private string _initialContentDescription = string.Empty;
347+
348+
#endregion
352349

353350
#region Constructor
354351

@@ -668,210 +665,6 @@ void OnPickerSelectedIndexChanged(object? sender, EventArgs e)
668665

669666
#region Override Methods
670667

671-
/// <summary>
672-
/// The list to add the bounds values of drawing elements as nodes.
673-
/// </summary>
674-
private readonly List<SemanticsNode> textInputLayoutSemanticsNodes = new();
675-
676-
/// <summary>
677-
/// List of semantic nodes for numeric entry.
678-
/// </summary>
679-
private readonly List<SemanticsNode> numericSemanticsNodes = new();
680-
681-
/// <summary>
682-
/// Returns the semantics node list.
683-
/// </summary>
684-
/// <param name="width">The width of the element.</param>
685-
/// <param name="height">The height of the element.</param>
686-
/// <returns>A list of semantics nodes.</returns>
687-
protected override List<SemanticsNode> GetSemanticsNodesCore(double width, double height)
688-
{
689-
textInputLayoutSemanticsNodes.Clear();
690-
691-
if (width > 0 && height > 0)
692-
{
693-
AddLayoutNode();
694-
}
695-
696-
PopulateNumericSemanticsNodes(Content);
697-
textInputLayoutSemanticsNodes.AddRange(numericSemanticsNodes);
698-
699-
if (ShowHelperText && !string.IsNullOrEmpty(HelperText) && _helperTextRect.Width > 0 && _helperTextRect.Height > 0)
700-
{
701-
AddHelperTextNode();
702-
}
703-
704-
return textInputLayoutSemanticsNodes;
705-
}
706-
707-
/// <summary>
708-
/// Retrieves the user-defined semantic description.
709-
/// </summary>
710-
/// <returns>The semantic description if available; otherwise, an empty string.</returns>
711-
private string GetUserDescription()
712-
{
713-
var description = SemanticProperties.GetDescription(this);
714-
IsDescriptionNotSetByUser = string.IsNullOrEmpty(description);
715-
return description ?? string.Empty;
716-
}
717-
718-
/// <summary>
719-
/// Adds a semantic node for the text input layout.
720-
/// </summary>
721-
private void AddLayoutNode()
722-
{
723-
string contentDescription = GetContentDescription(Content);
724-
string userDescription = GetUserDescription();
725-
string hintText = IsDescriptionNotSetByUser && !string.IsNullOrEmpty(Hint) ? Hint : userDescription;
726-
727-
var layoutNode = CreateSemanticsNode(
728-
1,
729-
new Rect(0, 0, _backgroundRectF.Width, _backgroundRectF.Height),
730-
$"{hintText}, {contentDescription}"
731-
);
732-
textInputLayoutSemanticsNodes.Add(layoutNode);
733-
}
734-
735-
/// <summary>
736-
/// Adds a semantic node for the helper text.
737-
/// </summary>
738-
private void AddHelperTextNode()
739-
{
740-
var helperTextNode = CreateSemanticsNode(5, _helperTextRect, HelperText);
741-
textInputLayoutSemanticsNodes.Add(helperTextNode);
742-
}
743-
744-
/// <summary>
745-
/// Retrieves the content description based on the provided content type.
746-
/// </summary>
747-
/// <param name="content">The content object.</param>
748-
/// <returns>A string containing the content description.</returns>
749-
private static string GetContentDescription(object? content) =>
750-
content switch
751-
{
752-
InputView entryEditorContent => GetDescription(entryEditorContent.IsFocused, entryEditorContent.Text, entryEditorContent.Placeholder),
753-
SfView numericEntryContent when numericEntryContent.Children.FirstOrDefault() is Entry numericInputView => GetDescription(numericInputView.IsFocused, numericInputView.Text, numericInputView.Placeholder),
754-
Picker picker => GetDescription(picker.IsFocused, GetSelectedItemText(picker), ""),
755-
_ => string.Empty
756-
};
757-
758-
/// <summary>
759-
/// Generates a formatted description based on the text, placeholder, and focus state.
760-
/// </summary>
761-
/// <param name="isFocused">Indicates if the element is focused.</param>
762-
/// <param name="text">The text content.</param>
763-
/// <param name="placeholder">The placeholder text.</param>
764-
/// <returns>A formatted string for semantic description.</returns>
765-
private static string GetDescription(bool isFocused, string? text, string? placeholder)
766-
{
767-
string formattedText = string.IsNullOrEmpty(text) ? placeholder ?? string.Empty :
768-
text.EndsWith(".") ? text : text + ".";
769-
770-
if (isFocused)
771-
{
772-
return formattedText;
773-
}
774-
775-
return $"{formattedText}\n\n\n\n\n\n\n\n\n Double-tap to edit text.";
776-
}
777-
778-
/// <summary>
779-
/// Retrieves the selected item text from a picker.
780-
/// </summary>
781-
/// <param name="picker">The picker control.</param>
782-
/// <returns>The selected item text.</returns>
783-
private static string? GetSelectedItemText(Picker picker)
784-
{
785-
if (picker.SelectedItem == null)
786-
return null;
787-
788-
if (picker.ItemDisplayBinding is Binding binding && binding.Path != null)
789-
{
790-
var property = picker.SelectedItem.GetType().GetProperty(binding.Path);
791-
return property?.GetValue(picker.SelectedItem)?.ToString();
792-
}
793-
794-
return picker.SelectedItem.ToString();
795-
}
796-
797-
/// <summary>
798-
/// Populates the list of numeric semantics nodes.
799-
/// </summary>
800-
/// <param name="content">The content object.</param>
801-
private void PopulateNumericSemanticsNodes(object? content)
802-
{
803-
numericSemanticsNodes.Clear();
804-
805-
switch (content)
806-
{
807-
case SfNumericUpDown numericUpDown:
808-
AddNumericUpDownNodes(numericUpDown);
809-
break;
810-
case SfNumericEntry when IsClearIconVisible:
811-
AddSemanticsNode(_clearIconRectF, 2, "Clear button");
812-
break;
813-
}
814-
}
815-
816-
/// <summary>
817-
/// Adds semantic nodes for the numeric up-down control.
818-
/// </summary>
819-
/// <param name="numericUpDown">The numeric up/down control.</param>
820-
private void AddNumericUpDownNodes(SfNumericUpDown numericUpDown)
821-
{
822-
bool isUpEnabled = numericUpDown.AutoReverse || numericUpDown._valueStates != ValueStates.Maximum;
823-
bool isDownEnabled = numericUpDown.AutoReverse || numericUpDown._valueStates != ValueStates.Minimum;
824-
AddUpDownNodes(numericUpDown, isUpEnabled, isDownEnabled);
825-
}
826-
827-
/// <summary>
828-
/// Adds semantic nodes for up-down buttons.
829-
/// </summary>
830-
private void AddUpDownNodes(SfNumericUpDown numericUpDown, bool isUpEnabled, bool isDownEnabled)
831-
{
832-
bool isVerticalInline = numericUpDown.IsInlineVerticalPlacement();
833-
bool isLeftAlignment = numericUpDown.UpDownButtonAlignment == UpDownButtonAlignment.Left;
834-
bool addClearIconFirst = isVerticalInline ? !isLeftAlignment : !isVerticalInline && !isLeftAlignment;
835-
836-
if (addClearIconFirst && IsClearIconVisible)
837-
{
838-
AddSemanticsNode(_clearIconRectF, 2, "Clear button");
839-
}
840-
AddSemanticsNode(_upIconRectF, addClearIconFirst ? 3 : 2, "Up button", isUpEnabled);
841-
AddSemanticsNode(_downIconRectF, addClearIconFirst ? 4 : 3, "Down button", isDownEnabled);
842-
if (!addClearIconFirst && IsClearIconVisible)
843-
{
844-
AddSemanticsNode(_clearIconRectF, 4, "Clear button");
845-
}
846-
}
847-
848-
/// <summary>
849-
/// Creates a semantic node with specified ID, bounds, and description.
850-
/// </summary>
851-
/// <param name="id">The ID of the semantics node.</param>
852-
/// <param name="rect">The bounds of the node.</param>
853-
/// <param name="description">The description associated with the node.</param>
854-
/// <returns>A newly created SemanticsNode object.</returns>
855-
private SemanticsNode CreateSemanticsNode(int id, Rect rect, string description) =>
856-
new SemanticsNode
857-
{
858-
Id = id,
859-
Bounds = rect,
860-
Text = description
861-
};
862-
863-
/// <summary>
864-
/// Adds a semantic node with specified properties.
865-
/// </summary>
866-
private void AddSemanticsNode(RectF bounds, int id, string description, bool isEnabled = true)
867-
{
868-
string stateDescription = isEnabled ? $"{description}, double tap to activate" : $"{description}, disabled";
869-
if (bounds.Width > 0 && bounds.Height > 0)
870-
{
871-
numericSemanticsNodes.Add(CreateSemanticsNode(id, new Rect(bounds.X, bounds.Y, bounds.Width, bounds.Height), stateDescription));
872-
}
873-
}
874-
875668
/// <summary>
876669
/// Invoked when the size of the element is allocated.
877670
/// </summary>
@@ -911,8 +704,12 @@ protected override void OnContentChanged(object oldValue, object newValue)
911704
//Adjusted Opacity from 0 to 0.00001 to ensure the content remains functionally active while enabling the ReturnType property.
912705
if (newValue is InputView entryEditorContent)
913706
{
914-
entryEditorContent.Opacity = IsHintFloated ? 1 : (DeviceInfo.Platform == DevicePlatform.iOS ? 0.00001 : 0);
915-
AutomationProperties.SetIsInAccessibleTree(entryEditorContent, false); // Exclude entry content from accessibility.
707+
#if ANDROID || IOS
708+
entryEditorContent.Opacity = IsHintFloated ? 1 : 0.00001;
709+
#else
710+
entryEditorContent.Opacity = IsHintFloated ? 1 : 0;
711+
#endif
712+
_initialContentDescription = SemanticProperties.GetDescription(entryEditorContent);
916713
}
917714
else if (newValue is SfView numericEntryContent && numericEntryContent.Children.Count > 0)
918715
{
@@ -937,8 +734,63 @@ protected override void OnContentChanged(object oldValue, object newValue)
937734
{
938735
OnEnabledPropertyChanged(IsEnabled);
939736
}
737+
738+
SetCustomDescription(newValue);
940739
}
941740

741+
/// <summary>
742+
/// Sets a custom semantic description for the content.
743+
/// </summary>
744+
private void SetCustomDescription(object content)
745+
{
746+
747+
if (this.Content == null || content == null)
748+
return;
749+
750+
var customDescription = string.Empty;
751+
#if ANDROID || MACCATALYST || IOS
752+
753+
if (content is InputView entryEditorContent)
754+
{
755+
customDescription = (string.IsNullOrEmpty(entryEditorContent.Text) && !string.IsNullOrEmpty(entryEditorContent.Placeholder) && DeviceInfo.Platform == DevicePlatform.Android) ? entryEditorContent.Placeholder : string.Empty;
756+
}
757+
758+
var layoutDescription = GetLayoutDescription();
759+
760+
var contentDescription = layoutDescription + (IsHintFloated ? customDescription + _initialContentDescription : string.Empty);
761+
762+
SemanticProperties.SetDescription(this.Content, contentDescription);
763+
#elif WINDOWS
764+
765+
customDescription = SemanticProperties.GetDescription(this);
766+
767+
if (string.IsNullOrEmpty(customDescription))
768+
{
769+
customDescription = ShowHint && !string.IsNullOrEmpty(Hint) ? Hint : string.Empty;
770+
SemanticProperties.SetDescription(this, customDescription);
771+
}
772+
#endif
773+
}
774+
775+
#if ANDROID || MACCATALYST || IOS
776+
/// <summary>
777+
/// Retrieves the layout semantic description.
778+
/// </summary>
779+
private string GetLayoutDescription()
780+
{
781+
var description = SemanticProperties.GetDescription(this);
782+
783+
if (string.IsNullOrEmpty(description))
784+
{
785+
description = ShowHint && !string.IsNullOrEmpty(Hint) ? Hint : string.Empty;
786+
}
787+
if(!string.IsNullOrEmpty(description))
788+
{
789+
description = description.EndsWith(".") ? description : description + ". ";
790+
}
791+
return description ?? string.Empty;
792+
}
793+
#endif
942794
/// <summary>
943795
/// Measures the size requirements for the content of the element.
944796
/// </summary>

0 commit comments

Comments
 (0)