12
12
using Syncfusion . Maui . Toolkit . NumericEntry ;
13
13
using Syncfusion . Maui . Toolkit . EntryRenderer ;
14
14
using Syncfusion . Maui . Toolkit . EntryView ;
15
+ using Syncfusion . Maui . Toolkit . NumericUpDown ;
15
16
16
17
namespace Syncfusion . Maui . Toolkit . TextInputLayout
17
18
{
@@ -224,7 +225,13 @@ public partial class SfTextInputLayout : SfContentView, ITouchListener, IParentT
224
225
/// </summary>
225
226
internal bool IsIconPressed { get ; private set ; } = false ;
226
227
#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 ; }
228
235
/// <summary>
229
236
/// Gets or sets a value indicating the hint was animating from down to up.
230
237
/// </summary>
@@ -385,13 +392,18 @@ void UpDownButtonPressed(Point touchpoint)
385
392
{
386
393
if ( ( _downIconRectF . Contains ( touchpoint ) && IsUpDownVerticalAlignment ) || ( _upIconRectF . Contains ( touchpoint ) && ! IsUpDownVerticalAlignment ) )
387
394
{
388
- numericEntry . UpButtonPressed ( ) ;
395
+ // Update the semantic description when the button is pressed
396
+ SemanticProperties . SetDescription ( this , "Up button pressed" ) ;
397
+ numericEntry . UpButtonPressed ( ) ;
389
398
}
390
399
else if ( ( _upIconRectF . Contains ( touchpoint ) && IsUpDownVerticalAlignment ) || ( _downIconRectF . Contains ( touchpoint ) && ! IsUpDownVerticalAlignment ) )
391
400
{
392
- numericEntry . DownButtonPressed ( ) ;
401
+ // Update the semantic description when the button is pressed
402
+ SemanticProperties . SetDescription ( this , "Down button pressed" ) ;
403
+ numericEntry . DownButtonPressed ( ) ;
393
404
}
394
- }
405
+ SemanticScreenReader . Announce ( SemanticProperties . GetDescription ( this ) ) ;
406
+ }
395
407
#if IOS || MACCATALYST
396
408
await Task . Delay ( 10 ) ;
397
409
IsIconPressed = false ;
@@ -406,7 +418,10 @@ void ClearText()
406
418
{
407
419
if ( this . Content is ITextInputLayout numericEntry )
408
420
{
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 ( ) ;
410
425
if ( ! IsClearIconVisible && _effectsRenderer != null && _effectsRenderer . HighlightBounds . Width > 0 && _effectsRenderer . HighlightBounds . Height > 0 )
411
426
{
412
427
_effectsRenderer . RemoveHighlight ( ) ;
@@ -649,6 +664,210 @@ void OnPickerSelectedIndexChanged(object? sender, EventArgs e)
649
664
650
665
#region Override Methods
651
666
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
+
652
871
/// <summary>
653
872
/// Invoked when the size of the element is allocated.
654
873
/// </summary>
@@ -689,21 +908,24 @@ protected override void OnContentChanged(object oldValue, object newValue)
689
908
if ( newValue is InputView entryEditorContent )
690
909
{
691
910
entryEditorContent . Opacity = IsHintFloated ? 1 : ( DeviceInfo . Platform == DevicePlatform . iOS ? 0.00001 : 0 ) ;
692
- }
911
+ AutomationProperties . SetIsInAccessibleTree ( entryEditorContent , false ) ; // Exclude entry content from accessibility.
912
+ }
693
913
else if ( newValue is SfView numericEntryContent && numericEntryContent . Children . Count > 0 )
694
914
{
695
915
if ( numericEntryContent . Children [ 0 ] is Entry numericInputView )
696
916
{
697
917
numericInputView . Opacity = IsHintFloated ? 1 : ( DeviceInfo . Platform == DevicePlatform . iOS ? 0.00001 : 0 ) ;
698
- }
918
+ AutomationProperties . SetIsInAccessibleTree ( numericInputView , false ) ; // Exclude numeric entry view from accessibility.
919
+ }
699
920
}
700
921
else if ( newValue is Picker picker )
701
922
{
702
923
if ( DeviceInfo . Platform != DevicePlatform . WinUI )
703
924
{
704
925
picker . Opacity = IsHintFloated ? 1 : ( DeviceInfo . Platform == DevicePlatform . iOS ? 0.00001 : 0 ) ;
705
- }
706
- }
926
+ }
927
+ AutomationProperties . SetIsInAccessibleTree ( picker , false ) ; // Exclude picker from accessibility.
928
+ }
707
929
708
930
base . OnContentChanged ( oldValue , newValue ) ;
709
931
0 commit comments