Skip to content

Commit 84d190e

Browse files
Added Accessibility Support
1 parent 3d64db3 commit 84d190e

File tree

3 files changed

+242
-10
lines changed

3 files changed

+242
-10
lines changed

maui/src/NumericEntry/SfNumericUpDown.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1251,7 +1251,7 @@ bool IsInlinePlacement()
12511251
/// <returns>
12521252
/// <c>true</c> if the placement mode is InlineVertical; otherwise, <c>false</c>.
12531253
/// </returns>
1254-
bool IsInlineVerticalPlacement()
1254+
internal bool IsInlineVerticalPlacement()
12551255
{
12561256
return UpDownPlacementMode == NumericUpDownPlacementMode.InlineVertical;
12571257
}

maui/src/TextInputLayout/SfTextInputLayout.Properties.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,6 +1981,16 @@ bool IsPassowordToggleIconVisible
19811981
{
19821982
get { return (EnablePasswordVisibilityToggle && Content is Entry); }
19831983
}
1984+
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+
19841994
#endregion
19851995

19861996
#region Property Changed Methods

maui/src/TextInputLayout/SfTextInputLayout.cs

Lines changed: 231 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Syncfusion.Maui.Toolkit.NumericEntry;
1313
using Syncfusion.Maui.Toolkit.EntryRenderer;
1414
using Syncfusion.Maui.Toolkit.EntryView;
15+
using Syncfusion.Maui.Toolkit.NumericUpDown;
1516

1617
namespace Syncfusion.Maui.Toolkit.TextInputLayout
1718
{
@@ -224,7 +225,13 @@ public partial class SfTextInputLayout : SfContentView, ITouchListener, IParentT
224225
/// </summary>
225226
internal bool IsIconPressed { get; private set; } = false;
226227
#endif
227-
internal bool IsLayoutTapped { get; set; }
228+
229+
/// <summary>
230+
/// Indicates if the description has been set by the user.
231+
/// </summary>
232+
bool _isDescriptionNotSetByUser;
233+
234+
internal bool IsLayoutTapped { get; set; }
228235
/// <summary>
229236
/// Gets or sets a value indicating the hint was animating from down to up.
230237
/// </summary>
@@ -385,13 +392,18 @@ void UpDownButtonPressed(Point touchpoint)
385392
{
386393
if ((_downIconRectF.Contains(touchpoint) && IsUpDownVerticalAlignment) || (_upIconRectF.Contains(touchpoint) && !IsUpDownVerticalAlignment))
387394
{
388-
numericEntry.UpButtonPressed();
395+
// Update the semantic description when the button is pressed
396+
SemanticProperties.SetDescription(this, "Up button pressed");
397+
numericEntry.UpButtonPressed();
389398
}
390399
else if ((_upIconRectF.Contains(touchpoint) && IsUpDownVerticalAlignment) || (_downIconRectF.Contains(touchpoint) && !IsUpDownVerticalAlignment))
391400
{
392-
numericEntry.DownButtonPressed();
401+
// Update the semantic description when the button is pressed
402+
SemanticProperties.SetDescription(this, "Down button pressed");
403+
numericEntry.DownButtonPressed();
393404
}
394-
}
405+
SemanticScreenReader.Announce(SemanticProperties.GetDescription(this));
406+
}
395407
#if IOS || MACCATALYST
396408
await Task.Delay(10);
397409
IsIconPressed = false;
@@ -406,7 +418,10 @@ void ClearText()
406418
{
407419
if (this.Content is ITextInputLayout numericEntry)
408420
{
409-
numericEntry.ClearIconPressed();
421+
// Update the semantic description when the button is pressed
422+
SemanticProperties.SetDescription(this, "Clear button pressed");
423+
SemanticScreenReader.Announce(SemanticProperties.GetDescription(this));
424+
numericEntry.ClearIconPressed();
410425
if (!IsClearIconVisible && _effectsRenderer != null && _effectsRenderer.HighlightBounds.Width > 0 && _effectsRenderer.HighlightBounds.Height > 0)
411426
{
412427
_effectsRenderer.RemoveHighlight();
@@ -649,6 +664,210 @@ void OnPickerSelectedIndexChanged(object? sender, EventArgs e)
649664

650665
#region Override Methods
651666

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

708930
base.OnContentChanged(oldValue, newValue);
709931

0 commit comments

Comments
 (0)