Skip to content

Commit 55eb63c

Browse files
authored
Merge pull request #683 from dotnet/copilot/fix-682
Apply accessibility fixes from PR #611 to .NET 10 DeveloperBalance app
2 parents 7ac7b8e + 815bd49 commit 55eb63c

19 files changed

+223
-150
lines changed

10.0/Apps/DeveloperBalance/AppShell.xaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
SegmentWidth="40" SegmentHeight="40">
3434
<sf:SfSegmentedControl.ItemsSource>
3535
<x:Array Type="{x:Type sf:SfSegmentItem}">
36-
<sf:SfSegmentItem ImageSource="{StaticResource IconLight}"/>
37-
<sf:SfSegmentItem ImageSource="{StaticResource IconDark}"/>
36+
<sf:SfSegmentItem ImageSource="{StaticResource IconLight}" SemanticProperties.Description="Light mode"/>
37+
<sf:SfSegmentItem ImageSource="{StaticResource IconDark}" SemanticProperties.Description="Dark mode"/>
3838
</x:Array>
3939
</sf:SfSegmentedControl.ItemsSource>
4040
</sf:SfSegmentedControl>

10.0/Apps/DeveloperBalance/AppShell.xaml.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ public AppShell()
1010
InitializeComponent();
1111
var currentTheme = Application.Current!.RequestedTheme;
1212
ThemeSegmentedControl.SelectedIndex = currentTheme == AppTheme.Light ? 0 : 1;
13+
#if ANDROID || WINDOWS
14+
SemanticProperties.SetDescription(ThemeSegmentedControl, "Theme selection");
15+
#endif
1316
}
1417
public static async Task DisplaySnackbarAsync(string message)
1518
{

10.0/Apps/DeveloperBalance/Data/TagRepository.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ public async Task<int> SaveItemAsync(Tag item, int projectID)
200200
await Init();
201201
await SaveItemAsync(item);
202202

203+
var isAssociated = await IsAssociated(item, projectID);
204+
if (isAssociated)
205+
{
206+
return 0; // No need to save again if already associated
207+
}
208+
203209
await using var connection = new SqliteConnection(Constants.DatabasePath);
204210
await connection.OpenAsync();
205211

@@ -212,6 +218,33 @@ public async Task<int> SaveItemAsync(Tag item, int projectID)
212218
return await saveCmd.ExecuteNonQueryAsync();
213219
}
214220

221+
/// <summary>
222+
/// Checks if a tag is already associated with a specific project.
223+
/// </summary>
224+
/// <param name="item">The tag to save.</param>
225+
/// <param name="projectID">The ID of the project.</param>
226+
/// <returns>If tag is already associated with this project</returns>
227+
async Task<bool> IsAssociated(Tag item, int projectID)
228+
{
229+
await Init();
230+
await SaveItemAsync(item);
231+
232+
await using var connection = new SqliteConnection(Constants.DatabasePath);
233+
await connection.OpenAsync();
234+
235+
// First check if the association already exists
236+
var checkCmd = connection.CreateCommand();
237+
checkCmd.CommandText = @"
238+
SELECT COUNT(*) FROM ProjectsTags
239+
WHERE ProjectID = @projectID AND TagID = @tagID";
240+
checkCmd.Parameters.AddWithValue("@projectID", projectID);
241+
checkCmd.Parameters.AddWithValue("@tagID", item.ID);
242+
243+
int existingCount = Convert.ToInt32(await checkCmd.ExecuteScalarAsync());
244+
245+
return existingCount != 0;
246+
}
247+
215248
/// <summary>
216249
/// Deletes a tag from the database.
217250
/// </summary>

10.0/Apps/DeveloperBalance/DeveloperBalance.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<ApplicationTitle>DeveloperBalance</ApplicationTitle>
2828

2929
<!-- App Identifier -->
30-
<ApplicationId>com.companyname.developerbalance</ApplicationId>
30+
<ApplicationId>com.companyname.developerbalance</ApplicationId>
3131

3232
<!-- Versions -->
3333
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>

10.0/Apps/DeveloperBalance/MauiProgram.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using CommunityToolkit.Maui;
22
using Microsoft.Extensions.Logging;
3+
using Microsoft.Maui.Handlers;
4+
using Microsoft.Maui.Platform;
35
using Syncfusion.Maui.Toolkit.Hosting;
46

57
namespace DeveloperBalance;
@@ -16,9 +18,23 @@ public static MauiApp CreateMauiApp()
1618
.ConfigureMauiHandlers(handlers =>
1719
{
1820
#if IOS || MACCATALYST
19-
handlers.AddHandler<Microsoft.Maui.Controls.CollectionView, Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2>();
21+
handlers.AddHandler<Microsoft.Maui.Controls.CollectionView, Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2>();
2022
#endif
21-
})
23+
#if WINDOWS
24+
Microsoft.Maui.Controls.Handlers.Items.CollectionViewHandler.Mapper.AppendToMapping("KeyboardAccessibleCollectionView", (handler, view) =>
25+
{
26+
handler.PlatformView.SingleSelectionFollowsFocus = false;
27+
});
28+
29+
Microsoft.Maui.Handlers.ContentViewHandler.Mapper.AppendToMapping(nameof(Pages.Controls.CategoryChart), (handler, view) =>
30+
{
31+
if (view is Pages.Controls.CategoryChart && handler.PlatformView is ContentPanel contentPanel)
32+
{
33+
contentPanel.IsTabStop = true;
34+
}
35+
});
36+
#endif
37+
})
2238
.ConfigureFonts(fonts =>
2339
{
2440
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");

10.0/Apps/DeveloperBalance/PageModels/MainPageModel.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public partial class MainPageModel : ObservableObject, IProjectTaskPageModel
3535
[ObservableProperty]
3636
private string _today = DateTime.Now.ToString("dddd, MMM d");
3737

38+
[ObservableProperty]
39+
private Project? selectedProject;
40+
3841
public bool HasCompletedTasks
3942
=> Tasks?.Any(t => t.IsCompleted) ?? false;
4043

@@ -149,8 +152,8 @@ private Task AddTask()
149152
=> Shell.Current.GoToAsync($"task");
150153

151154
[RelayCommand]
152-
private Task NavigateToProject(Project project)
153-
=> Shell.Current.GoToAsync($"project?id={project.ID}");
155+
private Task? NavigateToProject(Project project)
156+
=> project is null ? null : Shell.Current.GoToAsync($"project?id={project.ID}");
154157

155158
[RelayCommand]
156159
private Task NavigateToTask(ProjectTask task)

10.0/Apps/DeveloperBalance/PageModels/ProjectDetailPageModel.cs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using CommunityToolkit.Mvvm.ComponentModel;
22
using CommunityToolkit.Mvvm.Input;
33
using DeveloperBalance.Models;
4+
using System.Collections.ObjectModel;
5+
using System.Windows.Input;
46

57
namespace DeveloperBalance.PageModels;
68

@@ -34,6 +36,8 @@ public partial class ProjectDetailPageModel : ObservableObject, IQueryAttributab
3436
[ObservableProperty]
3537
private List<Tag> _allTags = [];
3638

39+
public IList<object> SelectedTags { get; set; } = new List<object>();
40+
3741
[ObservableProperty]
3842
private IconData _icon;
3943

@@ -147,6 +151,10 @@ private async Task LoadData(int id)
147151
foreach (var tag in allTags)
148152
{
149153
tag.IsSelected = _project.Tags.Any(t => t.ID == tag.ID);
154+
if (tag.IsSelected)
155+
{
156+
SelectedTags.Add(tag);
157+
}
150158
}
151159
AllTags = new(allTags);
152160
}
@@ -186,14 +194,11 @@ private async Task Save()
186194
_project.Icon = Icon.Icon ?? FluentUI.ribbon_24_regular;
187195
await _projectRepository.SaveItemAsync(_project);
188196

189-
if (_project.IsNullOrNew())
197+
foreach (var tag in AllTags)
190198
{
191-
foreach (var tag in AllTags)
199+
if (tag.IsSelected)
192200
{
193-
if (tag.IsSelected)
194-
{
195-
await _tagRepository.SaveItemAsync(tag, _project.ID);
196-
}
201+
await _tagRepository.SaveItemAsync(tag, _project.ID);
197202
}
198203
}
199204

@@ -248,7 +253,7 @@ private Task NavigateToTask(ProjectTask task) =>
248253
Shell.Current.GoToAsync($"task?id={task.ID}");
249254

250255
[RelayCommand]
251-
private async Task ToggleTag(Tag tag)
256+
internal async Task ToggleTag(Tag tag)
252257
{
253258
tag.IsSelected = !tag.IsSelected;
254259

@@ -257,20 +262,15 @@ private async Task ToggleTag(Tag tag)
257262
if (tag.IsSelected)
258263
{
259264
await _tagRepository.SaveItemAsync(tag, _project.ID);
260-
AllTags = new(AllTags);
261-
SemanticScreenReader.Announce($"{tag.Title} selected");
262265
}
263266
else
264267
{
265268
await _tagRepository.DeleteItemAsync(tag, _project.ID);
266-
AllTags = new(AllTags);
267-
SemanticScreenReader.Announce($"{tag.Title} unselected");
268269
}
269270
}
270-
else
271-
{
272-
AllTags = new(AllTags);
273-
}
271+
272+
AllTags = new(AllTags);
273+
SemanticScreenReader.Announce($"{tag.Title} {(tag.IsSelected ? "selected" : "unselected")}");
274274
}
275275

276276
[RelayCommand]
@@ -293,4 +293,19 @@ private async Task CleanTasks()
293293
OnPropertyChanged(nameof(HasCompletedTasks));
294294
await AppShell.DisplayToastAsync("All cleaned up!");
295295
}
296+
297+
[RelayCommand]
298+
private async Task SelectionChanged(object parameter)
299+
{
300+
if (parameter is IEnumerable<object> enumerableParameter)
301+
{
302+
var changed = enumerableParameter.OfType<Tag>().ToList();
303+
304+
if (changed.Count == 0 && SelectedTags is not null)
305+
changed = SelectedTags.OfType<Tag>().Except(enumerableParameter.OfType<Tag>()).ToList();
306+
307+
if (changed.Count == 1)
308+
await ToggleTag(changed[0]);
309+
}
310+
}
296311
}

10.0/Apps/DeveloperBalance/PageModels/ProjectListPageModel.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#nullable disable
21
using CommunityToolkit.Mvvm.ComponentModel;
32
using CommunityToolkit.Mvvm.Input;
43
using DeveloperBalance.Data;
@@ -14,6 +13,9 @@ public partial class ProjectListPageModel : ObservableObject
1413
[ObservableProperty]
1514
private List<Project> _projects = [];
1615

16+
[ObservableProperty]
17+
private Project? selectedProject;
18+
1719
public ProjectListPageModel(ProjectRepository projectRepository)
1820
{
1921
_projectRepository = projectRepository;
@@ -26,8 +28,8 @@ private async Task Appearing()
2628
}
2729

2830
[RelayCommand]
29-
Task NavigateToProject(Project project)
30-
=> Shell.Current.GoToAsync($"project?id={project.ID}");
31+
Task? NavigateToProject(Project project)
32+
=> project is null ? null : Shell.Current.GoToAsync($"project?id={project.ID}");
3133

3234
[RelayCommand]
3335
async Task AddProject()

10.0/Apps/DeveloperBalance/Pages/Controls/CategoryChart.xaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
HeightRequest="{OnIdiom 300, Phone=200}"
1010
Style="{StaticResource CardStyle}">
1111
<shimmer:SfShimmer
12+
AutomationProperties.IsInAccessibleTree="False"
1213
BackgroundColor="Transparent"
13-
VerticalOptions="FillAndExpand"
14+
VerticalOptions="Fill"
1415
IsActive ="{Binding IsBusy}">
1516
<shimmer:SfShimmer.CustomView>
1617
<Grid>
1718
<BoxView
1819
CornerRadius="12"
19-
VerticalOptions="FillAndExpand"
20+
VerticalOptions="Fill"
2021
Style="{StaticResource ShimmerCustomViewStyle}"/>
2122
</Grid>
2223
</shimmer:SfShimmer.CustomView>
@@ -38,7 +39,7 @@
3839
<Label Text="{Binding Item.Title}" TextColor="{AppThemeBinding
3940
Light={StaticResource DarkOnLightBackground},
4041
Dark={StaticResource LightOnDarkBackground}}" FontSize="{OnIdiom 18, Phone=14}"/>
41-
<Label Text=" : " TextColor="{AppThemeBinding
42+
<Label Text=": " TextColor="{AppThemeBinding
4243
Light={StaticResource DarkOnLightBackground},
4344
Dark={StaticResource LightOnDarkBackground}}" FontSize="{OnIdiom 18, Phone=14}"/>
4445
<Label Text="{Binding Item.Count}" TextColor="{AppThemeBinding

10.0/Apps/DeveloperBalance/Pages/Controls/ProjectCardView.xaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
x:DataType="models:Project">
1313
<shimmer:SfShimmer
1414
BackgroundColor="Transparent"
15-
VerticalOptions="FillAndExpand"
1615
IsActive="{Binding IsBusy, Source={RelativeSource AncestorType={x:Type pageModels:MainPageModel}}, x:DataType=pageModels:IProjectTaskPageModel}">
1716
<shimmer:SfShimmer.CustomView>
1817
<VerticalStackLayout Spacing="15">
@@ -48,13 +47,13 @@
4847
</Image>
4948
<Label Text="{Binding Name}" TextColor="{AppThemeBinding Light={StaticResource Gray600}, Dark={StaticResource Gray400}}" FontSize="14" TextTransform="Uppercase"/>
5049
<Label Text="{Binding Description}" LineBreakMode="WordWrap"/>
51-
<HorizontalStackLayout Spacing="15" BindableLayout.ItemsSource="{Binding Tags}">
50+
<FlexLayout Wrap="Wrap" Direction="Row" AlignItems="Start" JustifyContent="Start" BindableLayout.ItemsSource="{Binding Tags}">
5251
<BindableLayout.ItemTemplate>
5352
<DataTemplate x:DataType="models:Tag">
5453
<controls:TagView />
5554
</DataTemplate>
5655
</BindableLayout.ItemTemplate>
57-
</HorizontalStackLayout>
56+
</FlexLayout>
5857
</VerticalStackLayout>
5958
</shimmer:SfShimmer.Content>
6059
</shimmer:SfShimmer>

0 commit comments

Comments
 (0)