Skip to content

Commit cbe3c71

Browse files
committed
resolve accessibility issue in textinputlayout
1 parent b4987fe commit cbe3c71

File tree

3 files changed

+160
-14
lines changed

3 files changed

+160
-14
lines changed

maui/src/TextInputLayout/SfTextInputLayout.Methods.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,18 @@ internal void StartAnimation()
242242

243243
#region Private Methods
244244

245+
/// <summary>
246+
/// This refresh the semantic nodes of the textinputlayout
247+
/// </summary>
248+
void ResetSemantics()
249+
{
250+
if (_textInputLayoutSemanticsNodes != null)
251+
{
252+
_textInputLayoutSemanticsNodes.Clear();
253+
this.InvalidateSemantics();
254+
}
255+
}
256+
245257
/// <summary>
246258
/// Gets the button size based on the vertical alignment and icon templates.
247259
/// </summary>

maui/src/TextInputLayout/SfTextInputLayout.Properties.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1779,7 +1779,18 @@ internal bool IsUpDownVerticalAlignment
17791779
/// <summary>
17801780
/// Gets the value of the input text of the <see cref="SfTextInputLayout"/>.
17811781
/// </summary>
1782-
public string Text { get; internal set; } = string.Empty;
1782+
public string Text
1783+
{
1784+
get => _text;
1785+
internal set
1786+
{
1787+
if (value != _text)
1788+
{
1789+
_text = value;
1790+
ResetSemantics();
1791+
}
1792+
}
1793+
}
17831794

17841795
/// <summary>
17851796
/// Gets a value indicating whether the background mode is outline.
@@ -2149,6 +2160,7 @@ static void OnIsLayoutFocusedChanged(BindableObject bindable, object oldValue, o
21492160
{
21502161
if (bindable is SfTextInputLayout inputLayout)
21512162
{
2163+
inputLayout.ResetSemantics();
21522164
inputLayout.ChangeVisualState();
21532165
inputLayout.StartAnimation();
21542166
}
@@ -2399,6 +2411,7 @@ static void OnPropertyChanged(BindableObject bindable, object oldValue, object n
23992411
if (bindable is SfTextInputLayout inputLayout && inputLayout._initialLoaded)
24002412
{
24012413
inputLayout.UpdateViewBounds();
2414+
inputLayout.ResetSemantics();
24022415
}
24032416
}
24042417

maui/src/TextInputLayout/SfTextInputLayout.cs

Lines changed: 134 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,20 @@ public partial class SfTextInputLayout : SfContentView, ITouchListener, IParentT
179179
/// </summary>
180180
readonly double _defaultLeadAndTrailViewWidth = 24;
181181

182-
/// <summary>
183-
/// Gets or sets a value of leading view width.
184-
/// </summary>
185-
double _leadViewWidth = 0;
182+
/// <summary>
183+
/// The list to add the bounds values of drawing elements as nodes.
184+
/// </summary>
185+
readonly List<SemanticsNode> _textInputLayoutSemanticsNodes = new();
186+
187+
/// <summary>
188+
/// List of semantic nodes for numeric entry.
189+
/// </summary>
190+
readonly List<SemanticsNode> _numericSemanticsNodes = new();
191+
192+
/// <summary>
193+
/// Gets or sets a value of leading view width.
194+
/// </summary>
195+
double _leadViewWidth = 0;
186196

187197
/// <summary>
188198
/// Gets or sets a value of trailing view width.
@@ -277,10 +287,20 @@ public partial class SfTextInputLayout : SfContentView, ITouchListener, IParentT
277287
/// </summary>
278288
float _fontsize = DefaultHintFontSize;
279289

280-
/// <summary>
281-
/// Gets the stroke color of the clear button
282-
/// </summary>
283-
static readonly Color ClearIconStrokeColor = Color.FromArgb("#49454F");
290+
/// <summary>
291+
/// The field sets and checks the new size of the drawn elements.
292+
/// </summary>
293+
Size _controlSize = Size.Zero;
294+
295+
/// <summary>
296+
/// The field sets and checks the text.
297+
/// </summary>
298+
string _text = string.Empty;
299+
300+
/// <summary>
301+
/// Gets the stroke color of the clear button
302+
/// </summary>
303+
static readonly Color ClearIconStrokeColor = Color.FromArgb("#49454F");
284304

285305
readonly EffectsRenderer _effectsRenderer;
286306

@@ -665,10 +685,109 @@ void OnPickerSelectedIndexChanged(object? sender, EventArgs e)
665685
}
666686
InvalidateDrawable();
667687
}
668-
#endregion
688+
689+
/// <summary>
690+
/// Populates the list of numeric semantics nodes.
691+
/// </summary>
692+
/// <param name="content">The content object.</param>
693+
void PopulateNumericSemanticsNodes(object? content)
694+
{
695+
_numericSemanticsNodes.Clear();
696+
697+
switch (content)
698+
{
699+
case SfNumericUpDown numericUpDown:
700+
AddNumericUpDownNodes(numericUpDown);
701+
break;
702+
case SfNumericEntry when IsClearIconVisible:
703+
AddSemanticsNode(_clearIconRectF, 2, "Clear button");
704+
break;
705+
}
706+
}
707+
708+
/// <summary>
709+
/// Adds semantic nodes for the numeric up-down control.
710+
/// </summary>
711+
/// <param name="numericUpDown">The numeric up/down control.</param>
712+
void AddNumericUpDownNodes(SfNumericUpDown numericUpDown)
713+
{
714+
bool isUpEnabled = numericUpDown.AutoReverse || numericUpDown._valueStates != ValueStates.Maximum;
715+
bool isDownEnabled = numericUpDown.AutoReverse || numericUpDown._valueStates != ValueStates.Minimum;
716+
AddUpDownNodes(numericUpDown, isUpEnabled, isDownEnabled);
717+
}
718+
719+
/// <summary>
720+
/// Adds semantic nodes for up-down buttons.
721+
/// </summary>
722+
void AddUpDownNodes(SfNumericUpDown numericUpDown, bool isUpEnabled, bool isDownEnabled)
723+
{
724+
bool isVerticalInline = numericUpDown.IsInlineVerticalPlacement();
725+
bool isLeftAlignment = numericUpDown.UpDownButtonAlignment == UpDownButtonAlignment.Left;
726+
bool addClearIconFirst = isVerticalInline ? !isLeftAlignment : !isVerticalInline && !isLeftAlignment;
727+
728+
if (addClearIconFirst && IsClearIconVisible)
729+
{
730+
AddSemanticsNode(_clearIconRectF, 2, "Clear button");
731+
}
732+
733+
AddSemanticsNode(isVerticalInline ? _downIconRectF : _upIconRectF, addClearIconFirst ? 3 : 2, "Up button", isUpEnabled);
734+
AddSemanticsNode(isVerticalInline ? _upIconRectF : _downIconRectF, addClearIconFirst ? 4 : 3, "Down button", isDownEnabled);
735+
736+
if (!addClearIconFirst && IsClearIconVisible)
737+
{
738+
AddSemanticsNode(_clearIconRectF, 4, "Clear button");
739+
}
740+
}
741+
742+
/// <summary>
743+
/// Adds a semantic node with specified properties.
744+
/// </summary>
745+
void AddSemanticsNode(RectF bounds, int id, string description, bool isEnabled = true)
746+
{
747+
string stateDescription = isEnabled ? $"{description}, double tap to activate" : $"{description}, disabled";
748+
if (bounds.Width > 0 && bounds.Height > 0)
749+
{
750+
_numericSemanticsNodes.Add(CreateSemanticsNode(id, new Rect(bounds.X, bounds.Y, bounds.Width, bounds.Height), stateDescription));
751+
}
752+
}
753+
754+
/// <summary>
755+
/// Creates a semantic node with specified ID, bounds, and description.
756+
/// </summary>
757+
/// <param name="id">The ID of the semantics node.</param>
758+
/// <param name="rect">The bounds of the node.</param>
759+
/// <param name="description">The description associated with the node.</param>
760+
/// <returns>A newly created SemanticsNode object.</returns>
761+
SemanticsNode CreateSemanticsNode(int id, Rect rect, string description) =>
762+
new SemanticsNode
763+
{
764+
Id = id,
765+
Bounds = rect,
766+
Text = description
767+
};
768+
#endregion
669769

670770
#region Override Methods
671771

772+
/// <summary>
773+
/// Returns the semantics node list.
774+
/// </summary>
775+
/// <param name="width">The width of the element.</param>
776+
/// <param name="height">The height of the element.</param>
777+
/// <returns>A list of semantics nodes.</returns>
778+
protected override List<SemanticsNode> GetSemanticsNodesCore(double width, double height)
779+
{
780+
Size newControlSize = new(Width, Height);
781+
if (_controlSize == newControlSize && _textInputLayoutSemanticsNodes.Count != 0)
782+
{
783+
return _textInputLayoutSemanticsNodes;
784+
}
785+
_controlSize = newControlSize;
786+
PopulateNumericSemanticsNodes(Content);
787+
_textInputLayoutSemanticsNodes.AddRange(_numericSemanticsNodes);
788+
return _textInputLayoutSemanticsNodes;
789+
}
790+
672791
/// <summary>
673792
/// Invoked when the size of the element is allocated.
674793
/// </summary>
@@ -718,8 +837,11 @@ protected override void OnContentChanged(object oldValue, object newValue)
718837
{
719838
if (numericEntryContent.Children[0] is Entry numericInputView)
720839
{
721-
numericInputView.Opacity = IsHintFloated ? 1 : (DeviceInfo.Platform == DevicePlatform.iOS ? 0.00001 : 0);
722-
AutomationProperties.SetIsInAccessibleTree(numericInputView, false); // Exclude numeric entry view from accessibility.
840+
#if ANDROID || IOS
841+
numericInputView.Opacity = IsHintFloated ? 1 : 0.00001;
842+
#else
843+
numericInputView.Opacity = IsHintFloated ? 1 : 0;
844+
#endif
723845
}
724846
}
725847
else if (newValue is Picker picker)
@@ -728,7 +850,6 @@ protected override void OnContentChanged(object oldValue, object newValue)
728850
{
729851
picker.Opacity = IsHintFloated ? 1 : (DeviceInfo.Platform == DevicePlatform.iOS ? 0.00001 : 0);
730852
}
731-
AutomationProperties.SetIsInAccessibleTree(picker, false); // Exclude picker from accessibility.
732853
}
733854

734855
base.OnContentChanged(oldValue, newValue);
@@ -737,8 +858,8 @@ protected override void OnContentChanged(object oldValue, object newValue)
737858
{
738859
OnEnabledPropertyChanged(IsEnabled);
739860
}
740-
741861
SetCustomDescription(newValue);
862+
ResetSemantics();
742863
}
743864

744865
/// <summary>

0 commit comments

Comments
 (0)