Commit 41d60b2d2b

41d60b2d2b9126881e96ec58cb1515faae2f392d

parent: dba776f59e

Unsigned

cmc <hello@cleberg.net> · 2026-09-30 02:16 UTC

Accessibility: icon labels, large-text rows, diff lines (!138)

- accessibilityLabel on every icon-only button and menu (actions menus, account, bookmarks, new/add/filter/send buttons, chip remove).
- RemovableChips: "Remove <item>" label; remove button hit area padded to ~44pt without changing the chip's layout.
- GBChip: `.fixedSize()` so chips don't wrap per letter.
- MRRow and IssueRow: metadata stacks vertically at accessibility text sizes; issue labels wrap to a column when they don't fit.
- DiffView lines: one accessibility element labelled "Added line 13: …" / "Removed line 12: …" / "Line 14: …"; number and marker columns use @ScaledMetric.

Closes #17

Layout: unified · split

gitbay/MRs/UnifiedDiff.swift +10
@@ -50,6 +50,16 @@ nonisolated struct UnifiedDiff: Sendable, Hashable {
50 50
51 var id: String { "\(oldNumber ?? 0):\(newNumber ?? 0):\(text)" } 51 var id: String { "\(oldNumber ?? 0):\(newNumber ?? 0):\(text)" }
52 52
53 /// What VoiceOver reads for the line, e.g. "Added line 13: …".
54 var accessibilityLabel: String {
55 let spoken = text.trimmingCharacters(in: .whitespaces).isEmpty ? "blank" : text
56 return switch kind {
57 case .addition: "Added line \(newNumber ?? 0): \(spoken)"
58 case .deletion: "Removed line \(oldNumber ?? 0): \(spoken)"
59 case .context: "Line \(newNumber ?? 0): \(spoken)"
60 }
61 }
62
53 /// Whether a review thread hangs off this line. `side` picks 63 /// Whether a review thread hangs off this line. `side` picks
54 /// which numbering the thread was recorded against; a stale 64 /// which numbering the thread was recorded against; a stale
55 /// thread anchors nowhere, because the head it referenced is 65 /// thread anchors nowhere, because the head it referenced is
gitbay/Theme/Theme.swift +1
@@ -148,5 +148,6 @@ struct GBChip: View {
148 .padding(.vertical, 1) 148 .padding(.vertical, 1)
149 .background(color.opacity(0.07), in: gbChipShape) 149 .background(color.opacity(0.07), in: gbChipShape)
150 .overlay(gbChipShape.stroke(color.opacity(0.35), lineWidth: 1)) 150 .overlay(gbChipShape.stroke(color.opacity(0.35), lineWidth: 1))
151 .fixedSize()
151 } 152 }
152} 153}
gitbay/Views/Builds/BuildListView.swift +2
@@ -35,6 +35,7 @@ struct BuildListView: View {
35 ? "line.3.horizontal.decrease.circle.fill" 35 ? "line.3.horizontal.decrease.circle.fill"
36 : "line.3.horizontal.decrease.circle") 36 : "line.3.horizontal.decrease.circle")
37 } 37 }
38 .accessibilityLabel("Filter")
38 .accessibilityIdentifier("build-filter-button") 39 .accessibilityIdentifier("build-filter-button")
39 } 40 }
40 ToolbarItem(placement: .topBarTrailing) { 41 ToolbarItem(placement: .topBarTrailing) {
@@ -49,6 +50,7 @@ struct BuildListView: View {
49 } label: { 50 } label: {
50 Image(systemName: "play.circle") 51 Image(systemName: "play.circle")
51 } 52 }
53 .accessibilityLabel("Run Job")
52 .disabled(model.working || model.jobs.isEmpty) 54 .disabled(model.working || model.jobs.isEmpty)
53 .accessibilityIdentifier("build-trigger-button") 55 .accessibilityIdentifier("build-trigger-button")
54 } 56 }
gitbay/Views/Dashboard/DashboardView.swift +1
@@ -63,6 +63,7 @@ struct DashboardView: View {
63 } label: { 63 } label: {
64 Image(systemName: "plus") 64 Image(systemName: "plus")
65 } 65 }
66 .accessibilityLabel("New Repository")
66 .accessibilityIdentifier("dashboard-create-button") 67 .accessibilityIdentifier("dashboard-create-button")
67 } 68 }
68 } 69 }
gitbay/Views/Discovery/ProfileView.swift +2
@@ -58,6 +58,7 @@ struct ProfileView: View {
58 } label: { 58 } label: {
59 Image(systemName: "pencil") 59 Image(systemName: "pencil")
60 } 60 }
61 .accessibilityLabel("Edit Profile")
61 .disabled(model.working) 62 .disabled(model.working)
62 .accessibilityIdentifier("profile-edit-button") 63 .accessibilityIdentifier("profile-edit-button")
63 } 64 }
@@ -71,6 +72,7 @@ struct ProfileView: View {
71 } label: { 72 } label: {
72 Image(systemName: "plus.circle") 73 Image(systemName: "plus.circle")
73 } 74 }
75 .accessibilityLabel("New Organization")
74 .disabled(model.working) 76 .disabled(model.working)
75 .accessibilityIdentifier("org-create-button") 77 .accessibilityIdentifier("org-create-button")
76 } 78 }
gitbay/Views/Issues/IssueListView.swift +29 −7
@@ -46,6 +46,7 @@ struct IssueListView: View {
46 ? "line.3.horizontal.decrease.circle.fill" 46 ? "line.3.horizontal.decrease.circle.fill"
47 : "line.3.horizontal.decrease.circle") 47 : "line.3.horizontal.decrease.circle")
48 } 48 }
49 .accessibilityLabel("Filter")
49 .accessibilityIdentifier("issue-filter-button") 50 .accessibilityIdentifier("issue-filter-button")
50 } 51 }
51 ToolbarItem(placement: .topBarTrailing) { 52 ToolbarItem(placement: .topBarTrailing) {
@@ -54,6 +55,7 @@ struct IssueListView: View {
54 } label: { 55 } label: {
55 Image(systemName: "plus") 56 Image(systemName: "plus")
56 } 57 }
58 .accessibilityLabel("New Issue")
57 .accessibilityIdentifier("issue-create-button") 59 .accessibilityIdentifier("issue-create-button")
58 } 60 }
59 } 61 }
@@ -109,8 +111,13 @@ struct IssueListView: View {
109private struct IssueRow: View { 111private struct IssueRow: View {
110 let issue: Issue 112 let issue: Issue
111 let colors: LabelColors 113 let colors: LabelColors
114 @Environment(\.dynamicTypeSize) private var dynamicTypeSize
112 115
113 var body: some View { 116 var body: some View {
117 let large = dynamicTypeSize.isAccessibilitySize
118 let layout = large
119 ? AnyLayout(VStackLayout(alignment: .leading, spacing: 4))
120 : AnyLayout(HStackLayout(spacing: 6))
114 VStack(alignment: .leading, spacing: 4) { 121 VStack(alignment: .leading, spacing: 4) {
115 HStack(alignment: .firstTextBaseline, spacing: 6) { 122 HStack(alignment: .firstTextBaseline, spacing: 6) {
116 Text("#\(issue.number)") 123 Text("#\(issue.number)")
@@ -120,14 +127,23 @@ private struct IssueRow: View {
120 .font(.gbSans(.subheadline).weight(.medium)) 127 .font(.gbSans(.subheadline).weight(.medium))
121 .lineLimit(2) 128 .lineLimit(2)
122 } 129 }
123 HStack(spacing: 6) { 130 layout {
124 Image(systemName: issue.isOpen ? "circle" : "checkmark.circle.fill") 131 HStack(spacing: 6) {
125 .font(.gbSans(.caption2)) 132 Image(systemName: issue.isOpen ? "circle" : "checkmark.circle.fill")
126 .foregroundStyle(issue.isOpen ? Color.gbOK : Color.gbBad) 133 .font(.gbSans(.caption2))
127 ForEach(issue.labels ?? [], id: \.self) { label in 134 .foregroundStyle(issue.isOpen ? Color.gbOK : Color.gbBad)
128 GBChip(label, colors.color(label)) 135 if large {
136 ViewThatFits(in: .horizontal) {
137 HStack(spacing: 6) { labels }
138 VStack(alignment: .leading, spacing: 4) { labels }
139 }
140 } else {
141 labels
142 }
143 }
144 if !large {
145 Spacer()
129 } 146 }
130 Spacer()
131 if let assignees = issue.assignees, !assignees.isEmpty { 147 if let assignees = issue.assignees, !assignees.isEmpty {
132 Text(assignees.joined(separator: ", ")) 148 Text(assignees.joined(separator: ", "))
133 .font(.gbSans(.caption)) 149 .font(.gbSans(.caption))
@@ -141,6 +157,12 @@ private struct IssueRow: View {
141 } 157 }
142 .padding(.vertical, 2) 158 .padding(.vertical, 2)
143 } 159 }
160
161 private var labels: some View {
162 ForEach(issue.labels ?? [], id: \.self) { label in
163 GBChip(label, colors.color(label))
164 }
165 }
144} 166}
145 167
146nonisolated enum IssueRoute: Hashable { 168nonisolated enum IssueRoute: Hashable {
gitbay/Views/Issues/IssueView.swift +4
@@ -106,6 +106,7 @@ struct IssueView: View {
106 } label: { 106 } label: {
107 Image(systemName: "plus.circle.fill") 107 Image(systemName: "plus.circle.fill")
108 } 108 }
109 .accessibilityLabel("Add label")
109 .disabled(editingLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working) 110 .disabled(editingLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
110 } 111 }
111 } 112 }
@@ -124,6 +125,7 @@ struct IssueView: View {
124 } label: { 125 } label: {
125 Image(systemName: "plus.circle.fill") 126 Image(systemName: "plus.circle.fill")
126 } 127 }
128 .accessibilityLabel("Add assignee")
127 .disabled(editingAssignee.trimmingCharacters(in: .whitespaces).isEmpty || model.working) 129 .disabled(editingAssignee.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
128 } 130 }
129 } 131 }
@@ -189,6 +191,7 @@ struct IssueView: View {
189 } label: { 191 } label: {
190 Image(systemName: "arrow.up.circle.fill") 192 Image(systemName: "arrow.up.circle.fill")
191 } 193 }
194 .accessibilityLabel("Send comment")
192 .disabled(commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 195 .disabled(commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
193 || model.working) 196 || model.working)
194 } 197 }
@@ -223,6 +226,7 @@ struct IssueView: View {
223 Image(systemName: "ellipsis.circle") 226 Image(systemName: "ellipsis.circle")
224 } 227 }
225 } 228 }
229 .accessibilityLabel("Actions")
226 .disabled(model.working) 230 .disabled(model.working)
227 .accessibilityIdentifier("issue-actions-menu") 231 .accessibilityIdentifier("issue-actions-menu")
228 } 232 }
gitbay/Views/Issues/LabelListView.swift +2
@@ -37,6 +37,7 @@ struct LabelListView: View {
37 } label: { 37 } label: {
38 Image(systemName: "plus.circle.fill") 38 Image(systemName: "plus.circle.fill")
39 } 39 }
40 .accessibilityLabel("Add label")
40 .disabled( 41 .disabled(
41 newLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working 42 newLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working
42 ) 43 )
@@ -113,6 +114,7 @@ private struct LabelRow: View {
113 } label: { 114 } label: {
114 Image(systemName: "ellipsis.circle") 115 Image(systemName: "ellipsis.circle")
115 } 116 }
117 .accessibilityLabel("Actions for \(label.name)")
116 .disabled(working) 118 .disabled(working)
117 } 119 }
118 } 120 }
gitbay/Views/Issues/MilestoneListView.swift +1
@@ -61,6 +61,7 @@ struct MilestoneListView: View {
61 } label: { 61 } label: {
62 Image(systemName: "plus") 62 Image(systemName: "plus")
63 } 63 }
64 .accessibilityLabel("New Milestone")
64 .disabled(model.working) 65 .disabled(model.working)
65 .accessibilityIdentifier("milestone-create-button") 66 .accessibilityIdentifier("milestone-create-button")
66 } 67 }
gitbay/Views/MRs/DiffView.swift +7 −3
@@ -137,23 +137,27 @@ private struct HunkView: View {
137private struct LineView: View { 137private struct LineView: View {
138 138
139 let line: UnifiedDiff.Line 139 let line: UnifiedDiff.Line
140 @ScaledMetric(relativeTo: .caption2) private var numberWidth: CGFloat = 34
141 @ScaledMetric(relativeTo: .caption2) private var markerWidth: CGFloat = 14
140 142
141 var body: some View { 143 var body: some View {
142 HStack(spacing: 0) { 144 HStack(spacing: 0) {
143 Text(line.oldNumber.map(String.init) ?? "") 145 Text(line.oldNumber.map(String.init) ?? "")
144 .frame(width: 34, alignment: .trailing) 146 .frame(width: numberWidth, alignment: .trailing)
145 .foregroundStyle(.tertiary) 147 .foregroundStyle(.tertiary)
146 Text(line.newNumber.map(String.init) ?? "") 148 Text(line.newNumber.map(String.init) ?? "")
147 .frame(width: 34, alignment: .trailing) 149 .frame(width: numberWidth, alignment: .trailing)
148 .foregroundStyle(.tertiary) 150 .foregroundStyle(.tertiary)
149 Text(marker) 151 Text(marker)
150 .frame(width: 14) 152 .frame(width: markerWidth)
151 .foregroundStyle(markerColor) 153 .foregroundStyle(markerColor)
152 Text(line.text.isEmpty ? " " : line.text) 154 Text(line.text.isEmpty ? " " : line.text)
153 .foregroundStyle(textColor) 155 .foregroundStyle(textColor)
154 } 156 }
155 .font(.gbMono(.caption2)) 157 .font(.gbMono(.caption2))
156 .background(background) 158 .background(background)
159 .accessibilityElement(children: .ignore)
160 .accessibilityLabel(line.accessibilityLabel)
157 } 161 }
158 162
159 private var marker: String { 163 private var marker: String {
gitbay/Views/MRs/MRListView.swift +24 −11
@@ -43,6 +43,7 @@ struct MRListView: View {
43 ? "line.3.horizontal.decrease.circle.fill" 43 ? "line.3.horizontal.decrease.circle.fill"
44 : "line.3.horizontal.decrease.circle") 44 : "line.3.horizontal.decrease.circle")
45 } 45 }
46 .accessibilityLabel("Filter")
46 .accessibilityIdentifier("mr-filter-button") 47 .accessibilityIdentifier("mr-filter-button")
47 } 48 }
48 ToolbarItem(placement: .topBarTrailing) { 49 ToolbarItem(placement: .topBarTrailing) {
@@ -51,6 +52,7 @@ struct MRListView: View {
51 } label: { 52 } label: {
52 Image(systemName: "plus") 53 Image(systemName: "plus")
53 } 54 }
55 .accessibilityLabel("New Merge Request")
54 .accessibilityIdentifier("mr-create-button") 56 .accessibilityIdentifier("mr-create-button")
55 } 57 }
56 } 58 }
@@ -189,8 +191,13 @@ private struct MRCreateSheet: View {
189 191
190struct MRRow: View { 192struct MRRow: View {
191 let mr: MergeRequest 193 let mr: MergeRequest
194 @Environment(\.dynamicTypeSize) private var dynamicTypeSize
192 195
193 var body: some View { 196 var body: some View {
197 let large = dynamicTypeSize.isAccessibilitySize
198 let layout = large
199 ? AnyLayout(VStackLayout(alignment: .leading, spacing: 4))
200 : AnyLayout(HStackLayout(spacing: 6))
194 VStack(alignment: .leading, spacing: 4) { 201 VStack(alignment: .leading, spacing: 4) {
195 HStack(alignment: .firstTextBaseline, spacing: 6) { 202 HStack(alignment: .firstTextBaseline, spacing: 6) {
196 Text("!\(mr.number)") 203 Text("!\(mr.number)")
@@ -200,17 +207,23 @@ struct MRRow: View {
200 .font(.gbSans(.subheadline).weight(.medium)) 207 .font(.gbSans(.subheadline).weight(.medium))
201 .lineLimit(2) 208 .lineLimit(2)
202 } 209 }
203 HStack(spacing: 6) { 210 layout {
204 MRStateBadge(state: mr.state) 211 HStack(spacing: 6) {
205 if mr.draft { 212 MRStateBadge(state: mr.state)
206 GBChip("draft", .secondary) 213 if mr.draft {
207 } 214 GBChip("draft", .secondary)
208 Text(mr.source.isEmpty ? "(source gone)" : mr.source) 215 }
209 .lineLimit(1) 216 }
210 Image(systemName: "arrow.right") 217 HStack(spacing: 6) {
211 .font(.gbSans(.caption2)) 218 Text(mr.source.isEmpty ? "(source gone)" : mr.source)
212 Text(mr.targetRef) 219 .lineLimit(1)
213 Spacer() 220 Image(systemName: "arrow.right")
221 .font(.gbSans(.caption2))
222 Text(mr.targetRef)
223 }
224 if !large {
225 Spacer()
226 }
214 Text(mr.createdAt, format: .relative(presentation: .named)) 227 Text(mr.createdAt, format: .relative(presentation: .named))
215 .foregroundStyle(.secondary) 228 .foregroundStyle(.secondary)
216 } 229 }
gitbay/Views/MRs/MRView.swift +5
@@ -267,6 +267,7 @@ struct MRView: View {
267 } label: { 267 } label: {
268 Image(systemName: "plus.circle.fill") 268 Image(systemName: "plus.circle.fill")
269 } 269 }
270 .accessibilityLabel("Add label")
270 .disabled(editingLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working) 271 .disabled(editingLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
271 .accessibilityIdentifier("mr-label-add") 272 .accessibilityIdentifier("mr-label-add")
272 } 273 }
@@ -362,6 +363,7 @@ struct MRView: View {
362 } label: { 363 } label: {
363 Image(systemName: "plus.circle.fill") 364 Image(systemName: "plus.circle.fill")
364 } 365 }
366 .accessibilityLabel("Request review")
365 .disabled(editingReviewer.trimmingCharacters(in: .whitespaces).isEmpty || model.working) 367 .disabled(editingReviewer.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
366 } 368 }
367 } 369 }
@@ -554,6 +556,7 @@ struct MRView: View {
554 } label: { 556 } label: {
555 Image(systemName: "arrow.up.circle.fill") 557 Image(systemName: "arrow.up.circle.fill")
556 } 558 }
559 .accessibilityLabel("Send comment")
557 .disabled(commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 560 .disabled(commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
558 || model.working) 561 || model.working)
559 } 562 }
@@ -621,6 +624,7 @@ struct MRView: View {
621 Image(systemName: "ellipsis.circle") 624 Image(systemName: "ellipsis.circle")
622 } 625 }
623 } 626 }
627 .accessibilityLabel("Actions")
624 .disabled(model.working) 628 .disabled(model.working)
625 .accessibilityIdentifier("mr-actions-menu") 629 .accessibilityIdentifier("mr-actions-menu")
626 } 630 }
@@ -699,6 +703,7 @@ struct ReviewThreadView: View {
699 } label: { 703 } label: {
700 Image(systemName: "arrow.up.circle.fill") 704 Image(systemName: "arrow.up.circle.fill")
701 } 705 }
706 .accessibilityLabel("Send reply")
702 .disabled(replyText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 707 .disabled(replyText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
703 || model.working) 708 || model.working)
704 } 709 }
gitbay/Views/Orgs/OrgView.swift +3
@@ -41,6 +41,7 @@ struct OrgView: View {
41 } label: { 41 } label: {
42 Image(systemName: "pencil") 42 Image(systemName: "pencil")
43 } 43 }
44 .accessibilityLabel("Rename Organization")
44 .disabled(model.working) 45 .disabled(model.working)
45 .accessibilityIdentifier("org-rename-button") 46 .accessibilityIdentifier("org-rename-button")
46 } 47 }
@@ -132,6 +133,7 @@ struct OrgView: View {
132 } label: { 133 } label: {
133 Image(systemName: "plus.circle.fill") 134 Image(systemName: "plus.circle.fill")
134 } 135 }
136 .accessibilityLabel("Add member")
135 .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty || model.working) 137 .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
136 } 138 }
137 } 139 }
@@ -181,6 +183,7 @@ struct OrgView: View {
181 } label: { 183 } label: {
182 Image(systemName: "plus.circle.fill") 184 Image(systemName: "plus.circle.fill")
183 } 185 }
186 .accessibilityLabel("Create team")
184 .disabled(newTeam.trimmingCharacters(in: .whitespaces).isEmpty || model.working) 187 .disabled(newTeam.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
185 .accessibilityIdentifier("org-new-team-submit") 188 .accessibilityIdentifier("org-new-team-submit")
186 } 189 }
gitbay/Views/Orgs/TeamView.swift +1
@@ -44,6 +44,7 @@ struct TeamView: View {
44 } label: { 44 } label: {
45 Image(systemName: "plus.circle.fill") 45 Image(systemName: "plus.circle.fill")
46 } 46 }
47 .accessibilityLabel("Add member")
47 .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty 48 .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty
48 || model.working) 49 || model.working)
49 } 50 }
gitbay/Views/Releases/ReleaseListView.swift +1
@@ -50,6 +50,7 @@ struct ReleaseListView: View {
50 } label: { 50 } label: {
51 Image(systemName: "plus") 51 Image(systemName: "plus")
52 } 52 }
53 .accessibilityLabel("New Release")
53 .accessibilityIdentifier("release-create-button") 54 .accessibilityIdentifier("release-create-button")
54 } 55 }
55 } 56 }
gitbay/Views/Repos/FileView.swift +1
@@ -49,6 +49,7 @@ struct FileView: View {
49 } label: { 49 } label: {
50 Image(systemName: "ellipsis.circle") 50 Image(systemName: "ellipsis.circle")
51 } 51 }
52 .accessibilityLabel("Actions")
52 .accessibilityIdentifier("file-actions-menu") 53 .accessibilityIdentifier("file-actions-menu")
53 } 54 }
54 } 55 }
gitbay/Views/Repos/RepoListView.swift +3
@@ -32,6 +32,7 @@ struct RepoListView: View {
32 NavigationLink(value: RepoRoute.bookmarks) { 32 NavigationLink(value: RepoRoute.bookmarks) {
33 Image(systemName: "bookmark") 33 Image(systemName: "bookmark")
34 } 34 }
35 .accessibilityLabel("Bookmarks")
35 .accessibilityIdentifier("repo-bookmarks-button") 36 .accessibilityIdentifier("repo-bookmarks-button")
36 } 37 }
37 ToolbarItem(placement: .topBarTrailing) { 38 ToolbarItem(placement: .topBarTrailing) {
@@ -41,6 +42,7 @@ struct RepoListView: View {
41 } label: { 42 } label: {
42 Image(systemName: "plus") 43 Image(systemName: "plus")
43 } 44 }
45 .accessibilityLabel("Add Repository")
44 .accessibilityIdentifier("repo-create-button") 46 .accessibilityIdentifier("repo-create-button")
45 } 47 }
46 } 48 }
@@ -297,6 +299,7 @@ struct AccountMenu: ToolbarContent {
297 } label: { 299 } label: {
298 Image(systemName: "person.crop.circle") 300 Image(systemName: "person.crop.circle")
299 } 301 }
302 .accessibilityLabel("Account")
300 .accessibilityIdentifier("account-menu") 303 .accessibilityIdentifier("account-menu")
301 } 304 }
302 } 305 }
gitbay/Views/Repos/RepoSettingsView.swift +4
@@ -176,6 +176,7 @@ struct RepoSettingsView: View {
176 Image(systemName: "xmark.circle.fill") 176 Image(systemName: "xmark.circle.fill")
177 .foregroundStyle(.tertiary) 177 .foregroundStyle(.tertiary)
178 } 178 }
179 .accessibilityLabel("Remove \(topic)")
179 .disabled(model.working) 180 .disabled(model.working)
180 .accessibilityIdentifier("settings-remove-topic-\(topic)") 181 .accessibilityIdentifier("settings-remove-topic-\(topic)")
181 } 182 }
@@ -201,6 +202,7 @@ struct RepoSettingsView: View {
201 } label: { 202 } label: {
202 Image(systemName: "plus.circle.fill") 203 Image(systemName: "plus.circle.fill")
203 } 204 }
205 .accessibilityLabel("Add topic")
204 .disabled(newTopic.trimmingCharacters(in: .whitespaces).isEmpty || model.working) 206 .disabled(newTopic.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
205 .accessibilityIdentifier("settings-add-topic-submit") 207 .accessibilityIdentifier("settings-add-topic-submit")
206 } 208 }
@@ -247,6 +249,7 @@ struct RepoSettingsView: View {
247 } label: { 249 } label: {
248 Image(systemName: "plus.circle.fill") 250 Image(systemName: "plus.circle.fill")
249 } 251 }
252 .accessibilityLabel("Protect branch")
250 .disabled(newBranch.trimmingCharacters(in: .whitespaces).isEmpty || model.working) 253 .disabled(newBranch.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
251 } 254 }
252 toggle("Merge requests only", loaded.settings.requireMR ?? false) { 255 toggle("Merge requests only", loaded.settings.requireMR ?? false) {
@@ -318,6 +321,7 @@ struct RepoSettingsView: View {
318 } label: { 321 } label: {
319 Image(systemName: "plus.circle.fill") 322 Image(systemName: "plus.circle.fill")
320 } 323 }
324 .accessibilityLabel("Protect tag")
321 .disabled(newTagGlob.trimmingCharacters(in: .whitespaces).isEmpty || model.working) 325 .disabled(newTagGlob.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
322 .accessibilityIdentifier("settings-protect-tag-submit") 326 .accessibilityIdentifier("settings-protect-tag-submit")
323 } 327 }
gitbay/Views/Repos/RepoView.swift +1
@@ -226,6 +226,7 @@ struct RepoView: View {
226 Image(systemName: "ellipsis.circle") 226 Image(systemName: "ellipsis.circle")
227 } 227 }
228 } 228 }
229 .accessibilityLabel("Actions")
229 .disabled(model.working || actionsModel.working) 230 .disabled(model.working || actionsModel.working)
230 .accessibilityIdentifier("repo-actions-menu") 231 .accessibilityIdentifier("repo-actions-menu")
231 } 232 }
gitbay/Views/Repos/TreeView.swift +1
@@ -44,6 +44,7 @@ struct TreeView: View {
44 )) { 44 )) {
45 Image(systemName: "clock") 45 Image(systemName: "clock")
46 } 46 }
47 .accessibilityLabel("History")
47 .accessibilityIdentifier("tree-history-button") 48 .accessibilityIdentifier("tree-history-button")
48 } 49 }
49 } 50 }
gitbay/Views/Repos/WebhookListView.swift +1
@@ -62,6 +62,7 @@ struct WebhookListView: View {
62 } label: { 62 } label: {
63 Image(systemName: "plus") 63 Image(systemName: "plus")
64 } 64 }
65 .accessibilityLabel("Add Webhook")
65 .disabled(model.working) 66 .disabled(model.working)
66 .accessibilityIdentifier("webhook-add-button") 67 .accessibilityIdentifier("webhook-add-button")
67 } 68 }
gitbay/Views/Shared/RemovableChips.swift +10
@@ -11,6 +11,10 @@ struct RemovableChips: View {
11 var color: (String) -> Color = { _ in .secondary } 11 var color: (String) -> Color = { _ in .secondary }
12 let remove: (String) -> Void 12 let remove: (String) -> Void
13 13
14 /// Extends the remove button's hit area to about 44pt without
15 /// changing the chip's layout.
16 private static let hitPadding: CGFloat = 16
17
14 var body: some View { 18 var body: some View {
15 if !items.isEmpty { 19 if !items.isEmpty {
16 ScrollView(.horizontal, showsIndicators: false) { 20 ScrollView(.horizontal, showsIndicators: false) {
@@ -23,8 +27,12 @@ struct RemovableChips: View {
23 } label: { 27 } label: {
24 Image(systemName: "xmark.circle.fill") 28 Image(systemName: "xmark.circle.fill")
25 .foregroundStyle(.tertiary) 29 .foregroundStyle(.tertiary)
30 .padding(Self.hitPadding)
31 .contentShape(Rectangle())
32 .padding(-Self.hitPadding)
26 } 33 }
27 .disabled(disabled) 34 .disabled(disabled)
35 .accessibilityLabel("Remove \(item)")
28 .accessibilityIdentifier("\(idPrefix)-remove-\(item)") 36 .accessibilityIdentifier("\(idPrefix)-remove-\(item)")
29 } 37 }
30 .font(.gbSans(.caption)) 38 .font(.gbSans(.caption))
@@ -34,7 +42,9 @@ struct RemovableChips: View {
34 .overlay(gbChipShape.stroke(color(item).opacity(0.35), lineWidth: 1)) 42 .overlay(gbChipShape.stroke(color(item).opacity(0.35), lineWidth: 1))
35 } 43 }
36 } 44 }
45 .padding(.vertical, Self.hitPadding)
37 } 46 }
47 .padding(.vertical, -Self.hitPadding)
38 } 48 }
39 } 49 }
40} 50}
gitbay/Views/Snippets/SnippetListView.swift +1
@@ -30,6 +30,7 @@ struct SnippetListView: View {
30 } label: { 30 } label: {
31 Image(systemName: "plus") 31 Image(systemName: "plus")
32 } 32 }
33 .accessibilityLabel("New Snippet")
33 .accessibilityIdentifier("snippet-create-button") 34 .accessibilityIdentifier("snippet-create-button")
34 } 35 }
35 } 36 }
gitbay/Views/Snippets/SnippetView.swift +2
@@ -73,6 +73,7 @@ struct SnippetView: View {
73 Image(systemName: "ellipsis.circle") 73 Image(systemName: "ellipsis.circle")
74 } 74 }
75 } 75 }
76 .accessibilityLabel("Actions")
76 .disabled(model.working) 77 .disabled(model.working)
77 .accessibilityIdentifier("snippet-actions-menu") 78 .accessibilityIdentifier("snippet-actions-menu")
78 } 79 }
@@ -191,6 +192,7 @@ struct SnippetView: View {
191 } label: { 192 } label: {
192 Image(systemName: "ellipsis.circle") 193 Image(systemName: "ellipsis.circle")
193 } 194 }
195 .accessibilityLabel("Actions for \(file.name)")
194 .textCase(nil) 196 .textCase(nil)
195 .accessibilityIdentifier("snippet-file-menu-\(file.name)") 197 .accessibilityIdentifier("snippet-file-menu-\(file.name)")
196 } 198 }
gitbayTests/AccessibilityTests.swift added +25
@@ -0,0 +1,25 @@
1import Testing
2@testable import gitbay
3
4struct DiffLineAccessibilityTests {
5
6 @Test func additionReadsNewNumber() {
7 let line = UnifiedDiff.Line(kind: .addition, oldNumber: nil, newNumber: 13, text: "let x = 1")
8 #expect(line.accessibilityLabel == "Added line 13: let x = 1")
9 }
10
11 @Test func deletionReadsOldNumber() {
12 let line = UnifiedDiff.Line(kind: .deletion, oldNumber: 12, newNumber: nil, text: "let x = 0")
13 #expect(line.accessibilityLabel == "Removed line 12: let x = 0")
14 }
15
16 @Test func contextReadsNewNumber() {
17 let line = UnifiedDiff.Line(kind: .context, oldNumber: 11, newNumber: 14, text: "}")
18 #expect(line.accessibilityLabel == "Line 14: }")
19 }
20
21 @Test func blankLineSaysBlank() {
22 let line = UnifiedDiff.Line(kind: .addition, oldNumber: nil, newNumber: 3, text: " ")
23 #expect(line.accessibilityLabel == "Added line 3: blank")
24 }
25}