Commit 41d60b2d2b
Unsigned
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 { | |||
| 109 | private struct IssueRow: View { | 111 | private 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 | ||
| 146 | nonisolated enum IssueRoute: Hashable { | 168 | nonisolated 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 { | |||
| 137 | private struct LineView: View { | 137 | private 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 | ||
| 190 | struct MRRow: View { | 192 | struct 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 @@ | |||
| 1 | import Testing | ||
| 2 | @testable import gitbay | ||
| 3 | |||
| 4 | struct 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 | } | ||