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 {
5050
5151 var id: String { "\(oldNumber ?? 0):\(newNumber ?? 0):\(text)" }
5252
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
5363 /// Whether a review thread hangs off this line. `side` picks
5464 /// which numbering the thread was recorded against; a stale
5565 /// thread anchors nowhere, because the head it referenced is
gitbay/Theme/Theme.swift +1
@@ -148,5 +148,6 @@ struct GBChip: View {
148148 .padding(.vertical, 1)
149149 .background(color.opacity(0.07), in: gbChipShape)
150150 .overlay(gbChipShape.stroke(color.opacity(0.35), lineWidth: 1))
151 .fixedSize()
151152 }
152153}
gitbay/Views/Builds/BuildListView.swift +2
@@ -35,6 +35,7 @@ struct BuildListView: View {
3535 ? "line.3.horizontal.decrease.circle.fill"
3636 : "line.3.horizontal.decrease.circle")
3737 }
38 .accessibilityLabel("Filter")
3839 .accessibilityIdentifier("build-filter-button")
3940 }
4041 ToolbarItem(placement: .topBarTrailing) {
@@ -49,6 +50,7 @@ struct BuildListView: View {
4950 } label: {
5051 Image(systemName: "play.circle")
5152 }
53 .accessibilityLabel("Run Job")
5254 .disabled(model.working || model.jobs.isEmpty)
5355 .accessibilityIdentifier("build-trigger-button")
5456 }
gitbay/Views/Dashboard/DashboardView.swift +1
@@ -63,6 +63,7 @@ struct DashboardView: View {
6363 } label: {
6464 Image(systemName: "plus")
6565 }
66 .accessibilityLabel("New Repository")
6667 .accessibilityIdentifier("dashboard-create-button")
6768 }
6869 }
gitbay/Views/Discovery/ProfileView.swift +2
@@ -58,6 +58,7 @@ struct ProfileView: View {
5858 } label: {
5959 Image(systemName: "pencil")
6060 }
61 .accessibilityLabel("Edit Profile")
6162 .disabled(model.working)
6263 .accessibilityIdentifier("profile-edit-button")
6364 }
@@ -71,6 +72,7 @@ struct ProfileView: View {
7172 } label: {
7273 Image(systemName: "plus.circle")
7374 }
75 .accessibilityLabel("New Organization")
7476 .disabled(model.working)
7577 .accessibilityIdentifier("org-create-button")
7678 }
gitbay/Views/Issues/IssueListView.swift +29 −7
@@ -46,6 +46,7 @@ struct IssueListView: View {
4646 ? "line.3.horizontal.decrease.circle.fill"
4747 : "line.3.horizontal.decrease.circle")
4848 }
49 .accessibilityLabel("Filter")
4950 .accessibilityIdentifier("issue-filter-button")
5051 }
5152 ToolbarItem(placement: .topBarTrailing) {
@@ -54,6 +55,7 @@ struct IssueListView: View {
5455 } label: {
5556 Image(systemName: "plus")
5657 }
58 .accessibilityLabel("New Issue")
5759 .accessibilityIdentifier("issue-create-button")
5860 }
5961 }
@@ -109,8 +111,13 @@ struct IssueListView: View {
109111private struct IssueRow: View {
110112 let issue: Issue
111113 let colors: LabelColors
114 @Environment(\.dynamicTypeSize) private var dynamicTypeSize
112115
113116 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))
114121 VStack(alignment: .leading, spacing: 4) {
115122 HStack(alignment: .firstTextBaseline, spacing: 6) {
116123 Text("#\(issue.number)")
@@ -120,14 +127,23 @@ private struct IssueRow: View {
120127 .font(.gbSans(.subheadline).weight(.medium))
121128 .lineLimit(2)
122129 }
123 HStack(spacing: 6) {
124 Image(systemName: issue.isOpen ? "circle" : "checkmark.circle.fill")
125 .font(.gbSans(.caption2))
126 .foregroundStyle(issue.isOpen ? Color.gbOK : Color.gbBad)
127 ForEach(issue.labels ?? [], id: \.self) { label in
128 GBChip(label, colors.color(label))
130 layout {
131 HStack(spacing: 6) {
132 Image(systemName: issue.isOpen ? "circle" : "checkmark.circle.fill")
133 .font(.gbSans(.caption2))
134 .foregroundStyle(issue.isOpen ? Color.gbOK : Color.gbBad)
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()
129146 }
130 Spacer()
131147 if let assignees = issue.assignees, !assignees.isEmpty {
132148 Text(assignees.joined(separator: ", "))
133149 .font(.gbSans(.caption))
@@ -141,6 +157,12 @@ private struct IssueRow: View {
141157 }
142158 .padding(.vertical, 2)
143159 }
160
161 private var labels: some View {
162 ForEach(issue.labels ?? [], id: \.self) { label in
163 GBChip(label, colors.color(label))
164 }
165 }
144166}
145167
146168nonisolated enum IssueRoute: Hashable {
gitbay/Views/Issues/IssueView.swift +4
@@ -106,6 +106,7 @@ struct IssueView: View {
106106 } label: {
107107 Image(systemName: "plus.circle.fill")
108108 }
109 .accessibilityLabel("Add label")
109110 .disabled(editingLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
110111 }
111112 }
@@ -124,6 +125,7 @@ struct IssueView: View {
124125 } label: {
125126 Image(systemName: "plus.circle.fill")
126127 }
128 .accessibilityLabel("Add assignee")
127129 .disabled(editingAssignee.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
128130 }
129131 }
@@ -189,6 +191,7 @@ struct IssueView: View {
189191 } label: {
190192 Image(systemName: "arrow.up.circle.fill")
191193 }
194 .accessibilityLabel("Send comment")
192195 .disabled(commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
193196 || model.working)
194197 }
@@ -223,6 +226,7 @@ struct IssueView: View {
223226 Image(systemName: "ellipsis.circle")
224227 }
225228 }
229 .accessibilityLabel("Actions")
226230 .disabled(model.working)
227231 .accessibilityIdentifier("issue-actions-menu")
228232 }
gitbay/Views/Issues/LabelListView.swift +2
@@ -37,6 +37,7 @@ struct LabelListView: View {
3737 } label: {
3838 Image(systemName: "plus.circle.fill")
3939 }
40 .accessibilityLabel("Add label")
4041 .disabled(
4142 newLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working
4243 )
@@ -113,6 +114,7 @@ private struct LabelRow: View {
113114 } label: {
114115 Image(systemName: "ellipsis.circle")
115116 }
117 .accessibilityLabel("Actions for \(label.name)")
116118 .disabled(working)
117119 }
118120 }
gitbay/Views/Issues/MilestoneListView.swift +1
@@ -61,6 +61,7 @@ struct MilestoneListView: View {
6161 } label: {
6262 Image(systemName: "plus")
6363 }
64 .accessibilityLabel("New Milestone")
6465 .disabled(model.working)
6566 .accessibilityIdentifier("milestone-create-button")
6667 }
gitbay/Views/MRs/DiffView.swift +7 −3
@@ -137,23 +137,27 @@ private struct HunkView: View {
137137private struct LineView: View {
138138
139139 let line: UnifiedDiff.Line
140 @ScaledMetric(relativeTo: .caption2) private var numberWidth: CGFloat = 34
141 @ScaledMetric(relativeTo: .caption2) private var markerWidth: CGFloat = 14
140142
141143 var body: some View {
142144 HStack(spacing: 0) {
143145 Text(line.oldNumber.map(String.init) ?? "")
144 .frame(width: 34, alignment: .trailing)
146 .frame(width: numberWidth, alignment: .trailing)
145147 .foregroundStyle(.tertiary)
146148 Text(line.newNumber.map(String.init) ?? "")
147 .frame(width: 34, alignment: .trailing)
149 .frame(width: numberWidth, alignment: .trailing)
148150 .foregroundStyle(.tertiary)
149151 Text(marker)
150 .frame(width: 14)
152 .frame(width: markerWidth)
151153 .foregroundStyle(markerColor)
152154 Text(line.text.isEmpty ? " " : line.text)
153155 .foregroundStyle(textColor)
154156 }
155157 .font(.gbMono(.caption2))
156158 .background(background)
159 .accessibilityElement(children: .ignore)
160 .accessibilityLabel(line.accessibilityLabel)
157161 }
158162
159163 private var marker: String {
gitbay/Views/MRs/MRListView.swift +24 −11
@@ -43,6 +43,7 @@ struct MRListView: View {
4343 ? "line.3.horizontal.decrease.circle.fill"
4444 : "line.3.horizontal.decrease.circle")
4545 }
46 .accessibilityLabel("Filter")
4647 .accessibilityIdentifier("mr-filter-button")
4748 }
4849 ToolbarItem(placement: .topBarTrailing) {
@@ -51,6 +52,7 @@ struct MRListView: View {
5152 } label: {
5253 Image(systemName: "plus")
5354 }
55 .accessibilityLabel("New Merge Request")
5456 .accessibilityIdentifier("mr-create-button")
5557 }
5658 }
@@ -189,8 +191,13 @@ private struct MRCreateSheet: View {
189191
190192struct MRRow: View {
191193 let mr: MergeRequest
194 @Environment(\.dynamicTypeSize) private var dynamicTypeSize
192195
193196 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))
194201 VStack(alignment: .leading, spacing: 4) {
195202 HStack(alignment: .firstTextBaseline, spacing: 6) {
196203 Text("!\(mr.number)")
@@ -200,17 +207,23 @@ struct MRRow: View {
200207 .font(.gbSans(.subheadline).weight(.medium))
201208 .lineLimit(2)
202209 }
203 HStack(spacing: 6) {
204 MRStateBadge(state: mr.state)
205 if mr.draft {
206 GBChip("draft", .secondary)
207 }
208 Text(mr.source.isEmpty ? "(source gone)" : mr.source)
209 .lineLimit(1)
210 Image(systemName: "arrow.right")
211 .font(.gbSans(.caption2))
212 Text(mr.targetRef)
213 Spacer()
210 layout {
211 HStack(spacing: 6) {
212 MRStateBadge(state: mr.state)
213 if mr.draft {
214 GBChip("draft", .secondary)
215 }
216 }
217 HStack(spacing: 6) {
218 Text(mr.source.isEmpty ? "(source gone)" : mr.source)
219 .lineLimit(1)
220 Image(systemName: "arrow.right")
221 .font(.gbSans(.caption2))
222 Text(mr.targetRef)
223 }
224 if !large {
225 Spacer()
226 }
214227 Text(mr.createdAt, format: .relative(presentation: .named))
215228 .foregroundStyle(.secondary)
216229 }
gitbay/Views/MRs/MRView.swift +5
@@ -267,6 +267,7 @@ struct MRView: View {
267267 } label: {
268268 Image(systemName: "plus.circle.fill")
269269 }
270 .accessibilityLabel("Add label")
270271 .disabled(editingLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
271272 .accessibilityIdentifier("mr-label-add")
272273 }
@@ -362,6 +363,7 @@ struct MRView: View {
362363 } label: {
363364 Image(systemName: "plus.circle.fill")
364365 }
366 .accessibilityLabel("Request review")
365367 .disabled(editingReviewer.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
366368 }
367369 }
@@ -554,6 +556,7 @@ struct MRView: View {
554556 } label: {
555557 Image(systemName: "arrow.up.circle.fill")
556558 }
559 .accessibilityLabel("Send comment")
557560 .disabled(commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
558561 || model.working)
559562 }
@@ -621,6 +624,7 @@ struct MRView: View {
621624 Image(systemName: "ellipsis.circle")
622625 }
623626 }
627 .accessibilityLabel("Actions")
624628 .disabled(model.working)
625629 .accessibilityIdentifier("mr-actions-menu")
626630 }
@@ -699,6 +703,7 @@ struct ReviewThreadView: View {
699703 } label: {
700704 Image(systemName: "arrow.up.circle.fill")
701705 }
706 .accessibilityLabel("Send reply")
702707 .disabled(replyText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
703708 || model.working)
704709 }
gitbay/Views/Orgs/OrgView.swift +3
@@ -41,6 +41,7 @@ struct OrgView: View {
4141 } label: {
4242 Image(systemName: "pencil")
4343 }
44 .accessibilityLabel("Rename Organization")
4445 .disabled(model.working)
4546 .accessibilityIdentifier("org-rename-button")
4647 }
@@ -132,6 +133,7 @@ struct OrgView: View {
132133 } label: {
133134 Image(systemName: "plus.circle.fill")
134135 }
136 .accessibilityLabel("Add member")
135137 .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
136138 }
137139 }
@@ -181,6 +183,7 @@ struct OrgView: View {
181183 } label: {
182184 Image(systemName: "plus.circle.fill")
183185 }
186 .accessibilityLabel("Create team")
184187 .disabled(newTeam.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
185188 .accessibilityIdentifier("org-new-team-submit")
186189 }
gitbay/Views/Orgs/TeamView.swift +1
@@ -44,6 +44,7 @@ struct TeamView: View {
4444 } label: {
4545 Image(systemName: "plus.circle.fill")
4646 }
47 .accessibilityLabel("Add member")
4748 .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty
4849 || model.working)
4950 }
gitbay/Views/Releases/ReleaseListView.swift +1
@@ -50,6 +50,7 @@ struct ReleaseListView: View {
5050 } label: {
5151 Image(systemName: "plus")
5252 }
53 .accessibilityLabel("New Release")
5354 .accessibilityIdentifier("release-create-button")
5455 }
5556 }
gitbay/Views/Repos/FileView.swift +1
@@ -49,6 +49,7 @@ struct FileView: View {
4949 } label: {
5050 Image(systemName: "ellipsis.circle")
5151 }
52 .accessibilityLabel("Actions")
5253 .accessibilityIdentifier("file-actions-menu")
5354 }
5455 }
gitbay/Views/Repos/RepoListView.swift +3
@@ -32,6 +32,7 @@ struct RepoListView: View {
3232 NavigationLink(value: RepoRoute.bookmarks) {
3333 Image(systemName: "bookmark")
3434 }
35 .accessibilityLabel("Bookmarks")
3536 .accessibilityIdentifier("repo-bookmarks-button")
3637 }
3738 ToolbarItem(placement: .topBarTrailing) {
@@ -41,6 +42,7 @@ struct RepoListView: View {
4142 } label: {
4243 Image(systemName: "plus")
4344 }
45 .accessibilityLabel("Add Repository")
4446 .accessibilityIdentifier("repo-create-button")
4547 }
4648 }
@@ -297,6 +299,7 @@ struct AccountMenu: ToolbarContent {
297299 } label: {
298300 Image(systemName: "person.crop.circle")
299301 }
302 .accessibilityLabel("Account")
300303 .accessibilityIdentifier("account-menu")
301304 }
302305 }
gitbay/Views/Repos/RepoSettingsView.swift +4
@@ -176,6 +176,7 @@ struct RepoSettingsView: View {
176176 Image(systemName: "xmark.circle.fill")
177177 .foregroundStyle(.tertiary)
178178 }
179 .accessibilityLabel("Remove \(topic)")
179180 .disabled(model.working)
180181 .accessibilityIdentifier("settings-remove-topic-\(topic)")
181182 }
@@ -201,6 +202,7 @@ struct RepoSettingsView: View {
201202 } label: {
202203 Image(systemName: "plus.circle.fill")
203204 }
205 .accessibilityLabel("Add topic")
204206 .disabled(newTopic.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
205207 .accessibilityIdentifier("settings-add-topic-submit")
206208 }
@@ -247,6 +249,7 @@ struct RepoSettingsView: View {
247249 } label: {
248250 Image(systemName: "plus.circle.fill")
249251 }
252 .accessibilityLabel("Protect branch")
250253 .disabled(newBranch.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
251254 }
252255 toggle("Merge requests only", loaded.settings.requireMR ?? false) {
@@ -318,6 +321,7 @@ struct RepoSettingsView: View {
318321 } label: {
319322 Image(systemName: "plus.circle.fill")
320323 }
324 .accessibilityLabel("Protect tag")
321325 .disabled(newTagGlob.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
322326 .accessibilityIdentifier("settings-protect-tag-submit")
323327 }
gitbay/Views/Repos/RepoView.swift +1
@@ -226,6 +226,7 @@ struct RepoView: View {
226226 Image(systemName: "ellipsis.circle")
227227 }
228228 }
229 .accessibilityLabel("Actions")
229230 .disabled(model.working || actionsModel.working)
230231 .accessibilityIdentifier("repo-actions-menu")
231232 }
gitbay/Views/Repos/TreeView.swift +1
@@ -44,6 +44,7 @@ struct TreeView: View {
4444 )) {
4545 Image(systemName: "clock")
4646 }
47 .accessibilityLabel("History")
4748 .accessibilityIdentifier("tree-history-button")
4849 }
4950 }
gitbay/Views/Repos/WebhookListView.swift +1
@@ -62,6 +62,7 @@ struct WebhookListView: View {
6262 } label: {
6363 Image(systemName: "plus")
6464 }
65 .accessibilityLabel("Add Webhook")
6566 .disabled(model.working)
6667 .accessibilityIdentifier("webhook-add-button")
6768 }
gitbay/Views/Shared/RemovableChips.swift +10
@@ -11,6 +11,10 @@ struct RemovableChips: View {
1111 var color: (String) -> Color = { _ in .secondary }
1212 let remove: (String) -> Void
1313
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
1418 var body: some View {
1519 if !items.isEmpty {
1620 ScrollView(.horizontal, showsIndicators: false) {
@@ -23,8 +27,12 @@ struct RemovableChips: View {
2327 } label: {
2428 Image(systemName: "xmark.circle.fill")
2529 .foregroundStyle(.tertiary)
30 .padding(Self.hitPadding)
31 .contentShape(Rectangle())
32 .padding(-Self.hitPadding)
2633 }
2734 .disabled(disabled)
35 .accessibilityLabel("Remove \(item)")
2836 .accessibilityIdentifier("\(idPrefix)-remove-\(item)")
2937 }
3038 .font(.gbSans(.caption))
@@ -34,7 +42,9 @@ struct RemovableChips: View {
3442 .overlay(gbChipShape.stroke(color(item).opacity(0.35), lineWidth: 1))
3543 }
3644 }
45 .padding(.vertical, Self.hitPadding)
3746 }
47 .padding(.vertical, -Self.hitPadding)
3848 }
3949 }
4050}
gitbay/Views/Snippets/SnippetListView.swift +1
@@ -30,6 +30,7 @@ struct SnippetListView: View {
3030 } label: {
3131 Image(systemName: "plus")
3232 }
33 .accessibilityLabel("New Snippet")
3334 .accessibilityIdentifier("snippet-create-button")
3435 }
3536 }
gitbay/Views/Snippets/SnippetView.swift +2
@@ -73,6 +73,7 @@ struct SnippetView: View {
7373 Image(systemName: "ellipsis.circle")
7474 }
7575 }
76 .accessibilityLabel("Actions")
7677 .disabled(model.working)
7778 .accessibilityIdentifier("snippet-actions-menu")
7879 }
@@ -191,6 +192,7 @@ struct SnippetView: View {
191192 } label: {
192193 Image(systemName: "ellipsis.circle")
193194 }
195 .accessibilityLabel("Actions for \(file.name)")
194196 .textCase(nil)
195197 .accessibilityIdentifier("snippet-file-menu-\(file.name)")
196198 }
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}