diff --git a/CHANGELOG.md b/CHANGELOG.md
index eabbb54ff..b2ccee0db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## [53.8.0]
+- [Accessibility] Added `Mode` attached property.
+
## [53.7.4]
- Fix error causing index out of range in `TabBadgeService` when tabs have changed
diff --git a/src/app/Components/AccessibilitySamples/VoiceOverSamples/GroupChildrenSamples.xaml b/src/app/Components/AccessibilitySamples/VoiceOverSamples/GroupChildrenSamples.xaml
new file mode 100644
index 000000000..a8ad29be5
--- /dev/null
+++ b/src/app/Components/AccessibilitySamples/VoiceOverSamples/GroupChildrenSamples.xaml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/Components/AccessibilitySamples/VoiceOverSamples/GroupChildrenSamples.xaml.cs b/src/app/Components/AccessibilitySamples/VoiceOverSamples/GroupChildrenSamples.xaml.cs
new file mode 100644
index 000000000..a135c7755
--- /dev/null
+++ b/src/app/Components/AccessibilitySamples/VoiceOverSamples/GroupChildrenSamples.xaml.cs
@@ -0,0 +1,9 @@
+namespace Components.AccessibilitySamples.VoiceOverSamples;
+
+public partial class GroupChildrenSamples : DIPS.Mobile.UI.Components.Pages.ContentPage
+{
+ public GroupChildrenSamples()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/app/Components/AccessibilitySamples/VoiceOverSamples/VoiceOverSamples.xaml b/src/app/Components/AccessibilitySamples/VoiceOverSamples/VoiceOverSamples.xaml
new file mode 100644
index 000000000..eeca3a872
--- /dev/null
+++ b/src/app/Components/AccessibilitySamples/VoiceOverSamples/VoiceOverSamples.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/Components/AccessibilitySamples/VoiceOverSamples/VoiceOverSamples.xaml.cs b/src/app/Components/AccessibilitySamples/VoiceOverSamples/VoiceOverSamples.xaml.cs
new file mode 100644
index 000000000..e0615f68e
--- /dev/null
+++ b/src/app/Components/AccessibilitySamples/VoiceOverSamples/VoiceOverSamples.xaml.cs
@@ -0,0 +1,9 @@
+namespace Components.AccessibilitySamples.VoiceOverSamples;
+
+public partial class VoiceOverSamples
+{
+ public VoiceOverSamples()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/app/Components/App.xaml.cs b/src/app/Components/App.xaml.cs
index b8f5f3a99..78dcee867 100644
--- a/src/app/Components/App.xaml.cs
+++ b/src/app/Components/App.xaml.cs
@@ -1,90 +1,50 @@
using Components.Resources.LocalizedStrings;
-using Components.Services;
-using DIPS.Mobile.UI.API.Library;
+using Enum = System.Enum;
namespace Components;
public partial class App
{
- private readonly AppCenterService m_appCenterService;
-
public App()
{
InitializeComponent();
-
- m_appCenterService = new AppCenterService();
}
protected override Window CreateWindow(IActivationState? activationState)
{
- var shell = new DIPS.Mobile.UI.Components.Shell.Shell
- {
- ShouldGarbageCollectPreviousPage = true,
-
- };
+ var shell = new DIPS.Mobile.UI.Components.Shell.Shell();
+ var allSamples = REGISTER_YOUR_SAMPLES_HERE.RegisterSamples();
var tabBar = new TabBar();
- var tab = new Tab();
- tab.Items.Add(new ShellContent
+ foreach (var sampleType in Enum.GetValues())
{
- ContentTemplate =
- new DataTemplate(() => new MainPage(new List {SampleType.Resources, SampleType.Components}.OrderBy(s => s.ToString()),
- REGISTER_YOUR_SAMPLES_HERE.RegisterSamples()))
- });
- tabBar.Items.Add(tab);
- shell.Items.Add(tabBar);
-
- return new Window(shell);
- }
-
- protected override void OnStart()
- {
- _ = TryGetLatestVersion();
-
- base.OnStart();
- }
-
-
- private async Task TryGetLatestVersion()
- {
-#if DEBUG
- return true;
-#endif
-
- var release = await m_appCenterService.GetLatestVersion();
- if (release != null)
- {
- var latestVersion = new Version(release.Version);
- var currentVersion = AppInfo.Version;
- if (currentVersion >= latestVersion)
+ var samples = allSamples.Where(s => s.Type == sampleType).OrderBy(s => s.Name);
+ var title = sampleType switch
{
- return false;
- }
-
- if (Current?.MainPage == null) return true;
- var wantToDownload = await Current.MainPage.DisplayAlert(LocalizedStrings.New_version,
- LocalizedStrings.New_version_message, LocalizedStrings.Download, LocalizedStrings.Cancel);
- if (!wantToDownload)
+ SampleType.Resources => LocalizedStrings.Resources,
+ SampleType.Components => LocalizedStrings.Components,
+ SampleType.Accessibility => LocalizedStrings.Accessibility,
+ _ => sampleType.ToString()
+ };
+
+ var tab = new Tab
{
- return false;
- }
-
- await Launcher.OpenAsync(release.InstallUri);
- return true;
+ Title = title,
+ Items =
+ {
+ new ShellContent
+ {
+ ContentTemplate = new DataTemplate(() => new SamplesPage(sampleType, samples))
+ }
+ }
+ };
+
+ tabBar.Items.Add(tab);
}
+
+ shell.Items.Add(tabBar);
- return false;
- }
-
- protected override void OnResume()
- {
- _ = TryGetLatestVersion();
- base.OnResume();
- }
-
- protected override void OnSleep()
- {
- base.OnSleep();
+ return new Window(shell);
}
}
\ No newline at end of file
diff --git a/src/app/Components/Components.csproj b/src/app/Components/Components.csproj
index 3efe9954e..f244a8b6e 100644
--- a/src/app/Components/Components.csproj
+++ b/src/app/Components/Components.csproj
@@ -27,8 +27,8 @@
false
+ Apple Development
Automatic
- iPhone Developer
false
@@ -133,6 +133,10 @@
BarcodeScanningSample.xaml
Code
+
+ VoiceOverSamples.xaml
+ Code
+
@@ -163,6 +167,13 @@
Designer
+
+ Designer
+
+
+
+
+
diff --git a/src/app/Components/MainPage.cs b/src/app/Components/MainPage.cs
deleted file mode 100644
index 63cae5fbd..000000000
--- a/src/app/Components/MainPage.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using Components.Resources.LocalizedStrings;
-using DIPS.Mobile.UI.Components.ListItems;
-using DIPS.Mobile.UI.Components.ListItems.Extensions;
-using DIPS.Mobile.UI.Resources.Sizes;
-
-namespace Components;
-
-public class MainPage : DIPS.Mobile.UI.Components.Pages.ContentPage
-{
-
- public MainPage(IEnumerable sampleTypes, List samples)
- {
- Title = $"{AppInfo.Current.Name} ({AppInfo.Current.VersionString})";
- var collectionView = new DIPS.Mobile.UI.Components.Lists.CollectionView()
- {
- ItemsSource = sampleTypes,
- ItemTemplate = new DataTemplate(() => new NavigateToSamplesItem(samples)),
- Header = new Grid // Padding at the top
- {
- Padding = new Thickness(0, Sizes.GetSize(SizeName.page_margin_small))
- }
- };
-
- Content = collectionView;
-
- DIPS.Mobile.UI.Effects.Layout.Layout.SetAutoHideLastDivider(collectionView, true);
-
- }
-}
-
-public class NavigateToSamplesItem : NavigationListItem
-{
- private readonly List m_samples;
- private SampleType m_sampleType;
-
- public NavigateToSamplesItem(List samples)
- {
- m_samples = samples;
- Command = new Command(TryNavigateToSamplesPage);
- HasBottomDivider = true;
- }
-
- private void TryNavigateToSamplesPage()
- {
- var samples = m_samples.Where(sample => sample.Type == m_sampleType).ToList().OrderBy(sample => sample.Name);
- if (!samples.Any())
- {
- Shell.Current.DisplayAlert("No samples",
- $"Theres no samples for {m_sampleType} yet.", "Ok");
- }
-
- Shell.Current.Navigation.PushAsync((new SamplesPage(m_sampleType, samples)));
- }
-
- protected override void OnBindingContextChanged()
- {
- base.OnBindingContextChanged();
- if (BindingContext is SampleType sampleType)
- {
- m_sampleType = sampleType;
- Title = sampleType switch
- {
- SampleType.Components => LocalizedStrings.Components,
- SampleType.Resources => LocalizedStrings.Resources,
- _ => "Unknown"
- };
- }
- }
-}
\ No newline at end of file
diff --git a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs
index 23aceeacd..d252fa844 100644
--- a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs
+++ b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs
@@ -1,3 +1,5 @@
+using Components.AccessibilitySamples;
+using Components.AccessibilitySamples.VoiceOverSamples;
using Components.ComponentsSamples.Alerting;
using Components.ComponentsSamples.AmplitudeView;
using Components.ComponentsSamples.BarcodeScanning;
@@ -65,6 +67,7 @@ public static List RegisterSamples()
new(SampleType.Components, "Tag", () => new TagsSamples()),
new(SampleType.Components, "Counters", () => new CountersSamples()),
new(SampleType.Components, "TabView", () => new TabViewSamples()),
+ new(SampleType.Accessibility, "VoiceOver/TalkBack", () => new VoiceOverSamples()),
diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs
index 90a43458c..176d7597d 100644
--- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs
+++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs
@@ -710,5 +710,167 @@ internal static string Elements {
return ResourceManager.GetString("Elements", resourceCulture);
}
}
+
+ internal static string Accessibility {
+ get {
+ return ResourceManager.GetString("Accessibility", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver {
+ get {
+ return ResourceManager.GetString("VoiceOver", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_Description {
+ get {
+ return ResourceManager.GetString("VoiceOver_Description", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_GroupChildren_Title {
+ get {
+ return ResourceManager.GetString("VoiceOver_GroupChildren_Title", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_GroupChildren_Description {
+ get {
+ return ResourceManager.GetString("VoiceOver_GroupChildren_Description", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_ExcludeChildren_Title {
+ get {
+ return ResourceManager.GetString("VoiceOver_ExcludeChildren_Title", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_ExcludeChildren_Description {
+ get {
+ return ResourceManager.GetString("VoiceOver_ExcludeChildren_Description", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_PatientCard_Name {
+ get {
+ return ResourceManager.GetString("VoiceOver_PatientCard_Name", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_PatientCard_Born {
+ get {
+ return ResourceManager.GetString("VoiceOver_PatientCard_Born", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_PatientCard_Phone {
+ get {
+ return ResourceManager.GetString("VoiceOver_PatientCard_Phone", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_PatientCard_PhoneNumber {
+ get {
+ return ResourceManager.GetString("VoiceOver_PatientCard_PhoneNumber", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_ProductCard_Name {
+ get {
+ return ResourceManager.GetString("VoiceOver_ProductCard_Name", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_ProductCard_Price {
+ get {
+ return ResourceManager.GetString("VoiceOver_ProductCard_Price", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_ProductCard_Stock {
+ get {
+ return ResourceManager.GetString("VoiceOver_ProductCard_Stock", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_ProductCard_Rating {
+ get {
+ return ResourceManager.GetString("VoiceOver_ProductCard_Rating", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_WithoutGrouping {
+ get {
+ return ResourceManager.GetString("VoiceOver_WithoutGrouping", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_WithGrouping {
+ get {
+ return ResourceManager.GetString("VoiceOver_WithGrouping", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_SwipeGestures {
+ get {
+ return ResourceManager.GetString("VoiceOver_SwipeGestures", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_Example {
+ get {
+ return ResourceManager.GetString("VoiceOver_Example", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_AddressCard_Street {
+ get {
+ return ResourceManager.GetString("VoiceOver_AddressCard_Street", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_AddressCard_City {
+ get {
+ return ResourceManager.GetString("VoiceOver_AddressCard_City", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_AddressCard_Country {
+ get {
+ return ResourceManager.GetString("VoiceOver_AddressCard_Country", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_ExcludeExample_Title {
+ get {
+ return ResourceManager.GetString("VoiceOver_ExcludeExample_Title", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_ExcludeExample_Description {
+ get {
+ return ResourceManager.GetString("VoiceOver_ExcludeExample_Description", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_WithoutExclude {
+ get {
+ return ResourceManager.GetString("VoiceOver_WithoutExclude", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_WithExclude {
+ get {
+ return ResourceManager.GetString("VoiceOver_WithExclude", resourceCulture);
+ }
+ }
+
+ internal static string VoiceOver_CustomSemanticDescription {
+ get {
+ return ResourceManager.GetString("VoiceOver_CustomSemanticDescription", resourceCulture);
+ }
+ }
}
}
diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx
index 5b3e9c73d..2f6a1f316 100644
--- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx
+++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx
@@ -351,4 +351,85 @@
elements
+
+ Accessibility
+
+
+ VoiceOver
+
+
+ These examples demonstrate how to use the Accessibility.Mode attached property to improve VoiceOver/TalkBack experience.
+
+
+ GroupChildren Mode
+
+
+ Groups all children of a container into a single accessibility element. Screen readers will read all content in one focus gesture instead of requiring navigation to each child individually.
+
+
+ ExcludeChildren Mode
+
+
+ Excludes all children from the accessibility tree. Useful when you have decorative elements or complex layouts that should not be read individually.
+
+
+ John Doe
+
+
+ Born: 1980-05-15
+
+
+ Phone:
+
+
+ +47 123 45 678
+
+
+ Premium Product
+
+
+ Price: $49.99
+
+
+ In stock: 15 units
+
+
+ Rating: ⭐⭐⭐⭐⭐
+
+
+ Without grouping (Default)
+
+
+ With GroupChildren
+
+
+ swipe gesture(s) required
+
+
+ Example
+
+
+ 123 Main Street
+
+
+ New York, NY 10001
+
+
+ United States
+
+
+ Product card with decorative elements
+
+
+ The decorative border around the icon is excluded from accessibility, so screen readers only focus on meaningful content.
+
+
+ Without ExcludeChildren
+
+
+ With ExcludeChildren and custom description
+
+
+ Premium product available in store
+
\ No newline at end of file
diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx
index 9c9b0ac25..e9bf76140 100644
--- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx
+++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx
@@ -356,4 +356,85 @@
elementer
+
+ Tilgjengelighet
+
+
+ VoiceOver
+
+
+ Disse eksemplene viser hvordan du bruker Accessibility.Mode attached property for å forbedre VoiceOver/TalkBack opplevelsen.
+
+
+ GroupChildren Modus
+
+
+ Grupperer alle barn i en container til et enkelt tilgjengelighetselement. Skjermlesere vil lese alt innhold med én fokusgest i stedet for å kreve navigering til hvert barn individuelt.
+
+
+ ExcludeChildren Modus
+
+
+ Ekskluderer alle barn fra tilgjengelighetstreet. Nyttig når du har dekorative elementer eller komplekse layouter som ikke skal leses individuelt.
+
+
+ Kari Nordmann
+
+
+ Født: 15.05.1980
+
+
+ Telefon:
+
+
+ +47 123 45 678
+
+
+ Premium Produkt
+
+
+ Pris: 499 kr
+
+
+ På lager: 15 stk
+
+
+ Vurdering: ⭐⭐⭐⭐⭐
+
+
+ Uten gruppering (Standard)
+
+
+ Med GroupChildren
+
+
+ sveipegest(er) kreves
+
+
+ Eksempel
+
+
+ Storgata 123
+
+
+ 0123 Oslo
+
+
+ Norge
+
+
+ Produktkort med dekorative elementer
+
+
+ Den dekorative rammen rundt ikonet er ekskludert fra tilgjengeligheten, slik at skjermlesere kun fokuserer på det meningsfulle innholdet.
+
+
+ Uten ExcludeChildren
+
+
+ Med ExcludeChildren og egendefinert beskrivelse
+
+
+ Premium produkt tilgjengelig i butikk
+
\ No newline at end of file
diff --git a/src/app/Components/SampleType.cs b/src/app/Components/SampleType.cs
index 7e3493042..99c0d3c94 100644
--- a/src/app/Components/SampleType.cs
+++ b/src/app/Components/SampleType.cs
@@ -2,6 +2,7 @@ namespace Components;
public enum SampleType
{
+ Components,
Resources,
- Components
+ Accessibility
}
\ No newline at end of file
diff --git a/src/app/Components/Services/AppCenterService.cs b/src/app/Components/Services/AppCenterService.cs
deleted file mode 100644
index 8b9389865..000000000
--- a/src/app/Components/Services/AppCenterService.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using Newtonsoft.Json;
-
-namespace Components.Services
-{
- internal class AppCenterService : IAppCenterService
- {
- private readonly HttpClient m_httpClient;
-
- public AppCenterService()
- {
- m_httpClient = new HttpClient();
- }
-
- public async Task> RetrieveAndSetAppConfig()
- {
- await using var fileStream = await FileSystem.Current.OpenAppPackageFileAsync("appconfig.json");
- using var reader = new StreamReader(fileStream);
-
- var json = await reader.ReadToEndAsync();
- var anonymous = new
- {
- AppCenter = new
- {
- ApiKey = "",
- AppName = "",
- DistributionGroup = ""
- }
- };
- var rawConfig = JsonConvert.DeserializeAnonymousType(json, anonymous);
- if (!m_httpClient.DefaultRequestHeaders.Contains("X-API-Token"))
- {
- m_httpClient.DefaultRequestHeaders.Add("X-API-Token", rawConfig.AppCenter.ApiKey);
- }
-
- if (m_httpClient.BaseAddress == null)
- {
- m_httpClient.BaseAddress = new Uri("https://api.appcenter.ms/v0.1/apps/dips-as/");
- }
-
- return new Tuple(rawConfig.AppCenter.AppName, rawConfig.AppCenter.DistributionGroup);
- }
-
-
- public async Task GetLatestVersion()
- {
- var (appName, distributionGroupName) = await RetrieveAndSetAppConfig();
- var response = await m_httpClient.GetAsync(
- $"{appName}/distribution_groups/{distributionGroupName}/releases/latest");
- response.EnsureSuccessStatusCode();
- var json = await response.Content.ReadAsStringAsync();
- var anon = new {Id = 0, short_version = "", uploaded_at = DateTime.Now, install_url="", download_url= ""};
- var release = JsonConvert.DeserializeAnonymousType(json, anon);
- var installUrl = release.install_url;
-#if __ANDROID__
- installUrl = release.download_url;
-#endif
- return release != null ? new Release(release.Id, release.short_version, release.uploaded_at, new Uri(installUrl)) : null;
- }
- }
-
- internal interface IAppCenterService
- {
- Task GetLatestVersion();
- }
-}
\ No newline at end of file
diff --git a/src/app/Components/Services/Release.cs b/src/app/Components/Services/Release.cs
deleted file mode 100644
index a46d1794f..000000000
--- a/src/app/Components/Services/Release.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Components.Services
-{
- internal class Release
- {
- public Release(int id, string version, DateTime uploadedAt, Uri installUri)
- {
- Id = id;
- Version = version;
- UploadedAt = uploadedAt;
- InstallUri = installUri;
- }
-
- public int Id { get; }
- public string Version { get; }
- public DateTime UploadedAt { get; }
- public Uri InstallUri { get; }
- }
-}
\ No newline at end of file
diff --git a/src/app/Playground/VetleSamples/VetlePage.xaml b/src/app/Playground/VetleSamples/VetlePage.xaml
index 0a2716c34..8d04d8c5b 100644
--- a/src/app/Playground/VetleSamples/VetlePage.xaml
+++ b/src/app/Playground/VetleSamples/VetlePage.xaml
@@ -25,10 +25,11 @@
-
+
-
+
+
diff --git a/src/library/DIPS.Mobile.UI/AssemblyInfo.cs b/src/library/DIPS.Mobile.UI/AssemblyInfo.cs
index f79b1e5a8..f833c5b15 100644
--- a/src/library/DIPS.Mobile.UI/AssemblyInfo.cs
+++ b/src/library/DIPS.Mobile.UI/AssemblyInfo.cs
@@ -97,6 +97,7 @@
[assembly: XmlnsDefinition("http://dips.com/mobile.ui","DIPS.Mobile.UI.Components.Tag")]
[assembly: XmlnsDefinition("http://dips.com/mobile.ui","DIPS.Mobile.UI.Components.Counters")]
[assembly: XmlnsDefinition("http://dips.com/mobile.ui","DIPS.Mobile.UI.Components.Loading.DelayedView")]
+[assembly: XmlnsDefinition("http://dips.com/mobile.ui","DIPS.Mobile.UI.Effects.Accessibility")]
diff --git a/src/library/DIPS.Mobile.UI/Effects/Accessibility/Accessibility.Properties.cs b/src/library/DIPS.Mobile.UI/Effects/Accessibility/Accessibility.Properties.cs
new file mode 100644
index 000000000..a65845ff6
--- /dev/null
+++ b/src/library/DIPS.Mobile.UI/Effects/Accessibility/Accessibility.Properties.cs
@@ -0,0 +1,57 @@
+namespace DIPS.Mobile.UI.Effects.Accessibility;
+
+public partial class Accessibility
+{
+ public static readonly BindableProperty ModeProperty = BindableProperty.CreateAttached("Mode",
+ typeof(Mode),
+ typeof(Accessibility),
+ Mode.None,
+ propertyChanged: OnModeChanged);
+}
+
+public enum Mode
+{
+ None = 0,
+ ///
+ /// Groups all children of a container into a single accessibility element.
+ /// For more information, see GroupChildren Mode
+ ///
+ ///
+ /// Note: This does not take into account if the hierarchy changes in runtime, or if any texts changes after the layout has been rendered
+ ///
+ ///
+ /// This effect combines the text content of all descendant elements into the parent container's accessibility description,
+ /// allowing screen readers to read all content in a single focus gesture instead of requiring users to navigate each child separately.
+ ///
+ /// When to use:
+ ///
+ /// - Multiple child labels or text elements that form a single logical piece of information (e.g., address blocks, contact information, product details)
+ /// - Read-only informational displays where all content should be announced together
+ /// - Card-like UI patterns where related information should be grouped
+ /// - When you want to reduce the number of swipe gestures needed for screen reader users
+ ///
+ /// When NOT to use:
+ ///
+ /// - When any child element is interactive (Button, Entry, Switch, etc.) - interactive elements need individual focus for usability
+ /// - When child elements represent separate, unrelated pieces of information that users might want to navigate individually
+ /// - When children have complex accessibility requirements or need custom hints/traits
+ /// - In lists or collections where each item should be individually navigable
+ ///
+ ///
+ ///
+ ///
+ /// <VerticalStackLayout dui:Accessibility.Mode="GroupChildren">
+ /// <Label Text="John Doe" />
+ /// <Label Text="Born: 1980-05-15" />
+ /// <HorizontalStackLayout>
+ /// <Label Text="Phone:" />
+ /// <Label Text="+47 123 45 678" />
+ /// </HorizontalStackLayout>
+ /// <Label Text="john.doe@example.com" />
+ /// </VerticalStackLayout>
+ /// <!-- VoiceOver/TalkBack will read: "John Doe, Born: 1980-05-15, Phone:, +47 123 45 678, john.doe@example.com" -->
+ ///
+ /// Without this mode, screen readers would require 5 separate swipe gestures to read all information. With GroupChildren, it's read in one focus.
+ ///
+ GroupChildren = 1
+}
\ No newline at end of file
diff --git a/src/library/DIPS.Mobile.UI/Effects/Accessibility/Accessibility.cs b/src/library/DIPS.Mobile.UI/Effects/Accessibility/Accessibility.cs
new file mode 100644
index 000000000..c08040498
--- /dev/null
+++ b/src/library/DIPS.Mobile.UI/Effects/Accessibility/Accessibility.cs
@@ -0,0 +1,41 @@
+using DIPS.Mobile.UI.Effects.Accessibility.Effects;
+
+namespace DIPS.Mobile.UI.Effects.Accessibility;
+
+///
+/// Provides accessibility features for views.
+/// For more information, see Accessibility in DIPS.Mobile.UI.
+///
+public partial class Accessibility
+{
+ public static Mode GetMode(BindableObject view)
+ {
+ return (Mode)view.GetValue(ModeProperty);
+ }
+
+ ///
+ /// Attached property that can simplify common accessibility scenarios for screen readers.
+ /// For more information, see DIPS.Mobile.UI Accessibility Mode.
+ ///
+ public static void SetMode(BindableObject view, Mode mode)
+ {
+ view.SetValue(ModeProperty, mode);
+ }
+
+ private static void OnModeChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is not View view || newValue is not Mode mode)
+ return;
+
+ switch (mode)
+ {
+ case Mode.GroupChildren:
+ view.Behaviors.Add(new GroupEffect());
+ break;
+ case Mode.None:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/library/DIPS.Mobile.UI/Effects/Accessibility/Effects/GroupEffect.cs b/src/library/DIPS.Mobile.UI/Effects/Accessibility/Effects/GroupEffect.cs
new file mode 100644
index 000000000..5f41fcdde
--- /dev/null
+++ b/src/library/DIPS.Mobile.UI/Effects/Accessibility/Effects/GroupEffect.cs
@@ -0,0 +1,96 @@
+using DIPS.Mobile.UI.Internal.Logging;
+
+namespace DIPS.Mobile.UI.Effects.Accessibility.Effects;
+
+internal class GroupEffect : Behavior
+{
+ private Microsoft.Maui.Controls.Layout? m_layout;
+
+ protected override void OnAttachedTo(BindableObject bindable)
+ {
+ base.OnAttachedTo(bindable);
+
+ if (bindable is not Microsoft.Maui.Controls.Layout layout)
+ {
+ DUILogService.LogError($"{bindable.GetType().Name} is not a Microsoft.Maui.Controls.Layout");
+ return;
+ }
+
+ m_layout = layout;
+ m_layout.HandlerChanged += OnHandlerChanged;
+ }
+
+ private void OnHandlerChanged(object? sender, EventArgs e)
+ {
+ if (m_layout?.Handler is null)
+ return;
+
+ UpdateGroupedDescription();
+ }
+
+ private void UpdateGroupedDescription()
+ {
+ if (m_layout == null)
+ return;
+
+ var combinedText = GetAllDescendants(m_layout)
+ .Select(ExtractTextFromElement)
+ .Where(text => !string.IsNullOrWhiteSpace(text))
+ .ToList();
+
+ var description = string.Join(", ", combinedText);
+ SemanticProperties.SetDescription(m_layout, description);
+ }
+
+ private static IEnumerable GetAllDescendants(IView element)
+ {
+ if (element is not Microsoft.Maui.Controls.Layout layout)
+ yield break;
+
+ foreach (var child in layout.Children)
+ {
+ yield return child;
+
+ foreach (var descendant in GetAllDescendants(child))
+ {
+ yield return descendant;
+ }
+ }
+ }
+
+ private static string? ExtractTextFromElement(IView element)
+ {
+ if (element is BindableObject bindableObject)
+ {
+ var semanticDescription = SemanticProperties.GetDescription(bindableObject);
+ if (!string.IsNullOrEmpty(semanticDescription))
+ {
+ return semanticDescription;
+ }
+ }
+
+ if (element is Label label)
+ {
+ return GetLabelText(label);
+ }
+
+ return string.Empty;
+ }
+
+ private static string? GetLabelText(Label label)
+ {
+ return label.FormattedText != null ? label.FormattedText.ToString() : label.Text;
+ }
+
+ protected override void OnDetachingFrom(BindableObject bindable)
+ {
+ base.OnDetachingFrom(bindable);
+
+ if (m_layout is not null)
+ {
+ m_layout.HandlerChanged -= OnHandlerChanged;
+ }
+
+ m_layout = null;
+ }
+}