Skip to content

Commit c9691b1

Browse files
Merge pull request #173 from surya3655/TextInputLayout-Accessibility-Issue
Resolve Accessibility Issue in TextInputLayout
2 parents b4987fe + 2651f6d commit c9691b1

File tree

5 files changed

+201
-15
lines changed

5 files changed

+201
-15
lines changed

maui/src/NumericEntry/SfNumericEntry.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,8 +486,12 @@ protected override List<SemanticsNode> GetSemanticsNodesCore(double width, doubl
486486
{
487487
// Ensure any necessary size updates are performed
488488
UpdateSemanticsSizes();
489-
if (SemanticsDataIsCurrent() && IsTextInputLayout)
489+
if (SemanticsDataIsCurrent() || IsTextInputLayout)
490490
{
491+
if(IsTextInputLayout)
492+
{
493+
_numericEntrySemanticsNodes.Clear();
494+
}
491495
return _numericEntrySemanticsNodes;
492496
}
493497

maui/src/NumericEntry/SfNumericUpDown.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,10 @@ protected override List<SemanticsNode> GetSemanticsNodesCore(double width, doubl
11421142

11431143
if (SemanticsDataIsCurrent() || IsTextInputLayout)
11441144
{
1145+
if (IsTextInputLayout)
1146+
{
1147+
_numericUpDownSemanticsNodes.Clear();
1148+
}
11451149
return _numericUpDownSemanticsNodes;
11461150
}
11471151
var upbuttonstate = !(_valueStates == ValueStates.Maximum) || AutoReverse;

maui/src/TextInputLayout/SfTextInputLayout.Methods.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,35 @@ 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+
257+
/// <summary>
258+
/// Resets semantics on first character input or when text is cleared.
259+
/// </summary>
260+
void HandleSemanticsReset()
261+
{
262+
if (string.IsNullOrEmpty(_text))
263+
{
264+
_hasResetSemantics = false;
265+
ResetSemantics();
266+
}
267+
else if (_text.Length == 1 && !_hasResetSemantics)
268+
{
269+
ResetSemantics();
270+
_hasResetSemantics = true;
271+
}
272+
}
273+
245274
/// <summary>
246275
/// Gets the button size based on the vertical alignment and icon templates.
247276
/// </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+
HandleSemanticsReset();
1791+
}
1792+
}
1793+
}
17831794

17841795
/// <summary>
17851796
/// Gets a value indicating whether the background mode is outline.
@@ -2151,6 +2162,7 @@ static void OnIsLayoutFocusedChanged(BindableObject bindable, object oldValue, o
21512162
{
21522163
inputLayout.ChangeVisualState();
21532164
inputLayout.StartAnimation();
2165+
inputLayout.ResetSemantics();
21542166
}
21552167
}
21562168

@@ -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: 149 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,25 @@ 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+
/// Indicates whether semantics need to be reset.
297+
/// </summary>
298+
bool _hasResetSemantics = false;
299+
300+
/// <summary>
301+
/// The field sets and checks the text.
302+
/// </summary>
303+
string _text = string.Empty;
304+
305+
/// <summary>
306+
/// Gets the stroke color of the clear button
307+
/// </summary>
308+
static readonly Color ClearIconStrokeColor = Color.FromArgb("#49454F");
284309

285310
readonly EffectsRenderer _effectsRenderer;
286311

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

670785
#region Override Methods
671786

787+
/// <summary>
788+
/// Returns the semantics node list.
789+
/// </summary>
790+
/// <param name="width">The width of the element.</param>
791+
/// <param name="height">The height of the element.</param>
792+
/// <returns>A list of semantics nodes.</returns>
793+
protected override List<SemanticsNode> GetSemanticsNodesCore(double width, double height)
794+
{
795+
Size newControlSize = new(Width, Height);
796+
if (_controlSize == newControlSize && _textInputLayoutSemanticsNodes.Count != 0)
797+
{
798+
return _textInputLayoutSemanticsNodes;
799+
}
800+
_controlSize = newControlSize;
801+
PopulateNumericSemanticsNodes(Content);
802+
_textInputLayoutSemanticsNodes.AddRange(_numericSemanticsNodes);
803+
return _textInputLayoutSemanticsNodes;
804+
}
805+
672806
/// <summary>
673807
/// Invoked when the size of the element is allocated.
674808
/// </summary>
@@ -718,8 +852,11 @@ protected override void OnContentChanged(object oldValue, object newValue)
718852
{
719853
if (numericEntryContent.Children[0] is Entry numericInputView)
720854
{
721-
numericInputView.Opacity = IsHintFloated ? 1 : (DeviceInfo.Platform == DevicePlatform.iOS ? 0.00001 : 0);
722-
AutomationProperties.SetIsInAccessibleTree(numericInputView, false); // Exclude numeric entry view from accessibility.
855+
#if ANDROID || IOS
856+
numericInputView.Opacity = IsHintFloated ? 1 : 0.00001;
857+
#else
858+
numericInputView.Opacity = IsHintFloated ? 1 : 0;
859+
#endif
723860
}
724861
}
725862
else if (newValue is Picker picker)
@@ -728,7 +865,6 @@ protected override void OnContentChanged(object oldValue, object newValue)
728865
{
729866
picker.Opacity = IsHintFloated ? 1 : (DeviceInfo.Platform == DevicePlatform.iOS ? 0.00001 : 0);
730867
}
731-
AutomationProperties.SetIsInAccessibleTree(picker, false); // Exclude picker from accessibility.
732868
}
733869

734870
base.OnContentChanged(oldValue, newValue);
@@ -737,8 +873,8 @@ protected override void OnContentChanged(object oldValue, object newValue)
737873
{
738874
OnEnabledPropertyChanged(IsEnabled);
739875
}
740-
741876
SetCustomDescription(newValue);
877+
ResetSemantics();
742878
}
743879

744880
/// <summary>

0 commit comments

Comments
 (0)