From ea821b338602a9425da6d9474340be449cc8cee9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 06:42:05 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Expand=20list=20row?= =?UTF-8?q?=20hit=20targets=20and=20improve=20VoiceOver=20accessibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrapped the entire `HStack` in `CategoryRow` and `NodeModulesRow` within `Button` components and applied `.contentShape(Rectangle())` to make the empty spaces clickable. Additionally, `.accessibilityElement(children: .combine)` was added to both to group the row's child elements for a better VoiceOver experience. Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/palette.md | 3 + Sources/Cacheout/Views/CategoryRow.swift | 76 ++++++++++--------- .../Cacheout/Views/NodeModulesSection.swift | 62 +++++++-------- 3 files changed, 74 insertions(+), 67 deletions(-) create mode 100644 .jules/palette.md diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 0000000..9acf5b2 --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2024-03-26 - SwiftUI List Row Hit Targets +**Learning:** In custom SwiftUI list rows, if only a small element like a checkbox is wrapped in a `Button`, the rest of the row space is unclickable and the VoiceOver experience is fragmented. +**Action:** Always wrap the entire row structure (e.g., `HStack`) in a `Button`, apply `.contentShape(Rectangle())` so empty spaces register clicks, use `.buttonStyle(.plain)` to prevent default styling, and add `.accessibilityElement(children: .combine)` so screen readers announce the row content cohesively. diff --git a/Sources/Cacheout/Views/CategoryRow.swift b/Sources/Cacheout/Views/CategoryRow.swift index 2653aae..ca08ddb 100644 --- a/Sources/Cacheout/Views/CategoryRow.swift +++ b/Sources/Cacheout/Views/CategoryRow.swift @@ -24,55 +24,57 @@ struct CategoryRow: View { let onToggle: () -> Void var body: some View { - HStack(spacing: 12) { - // Checkbox - Button(action: onToggle) { + Button(action: onToggle) { + HStack(spacing: 12) { + // Checkbox Image(systemName: result.isSelected ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundStyle(result.isSelected ? .blue : .secondary) - } - .buttonStyle(.plain) - .disabled(result.isEmpty) - // Icon - Image(systemName: result.category.icon) - .font(.title3) - .frame(width: 24) - .foregroundStyle(iconColor) + // Icon + Image(systemName: result.category.icon) + .font(.title3) + .frame(width: 24) + .foregroundStyle(iconColor) - // Name + description - VStack(alignment: .leading, spacing: 2) { - Text(result.category.name) - .font(.body.weight(.medium)) - if result.isEmpty { - Text("Not found") - .font(.caption) - .foregroundStyle(.tertiary) - } else { - Text(result.category.description) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) + // Name + description + VStack(alignment: .leading, spacing: 2) { + Text(result.category.name) + .font(.body.weight(.medium)) + if result.isEmpty { + Text("Not found") + .font(.caption) + .foregroundStyle(.tertiary) + } else { + Text(result.category.description) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } } - } - Spacer() + Spacer() - // Size - if !result.isEmpty { - Text(result.formattedSize) - .font(.body.monospacedDigit()) - .foregroundStyle(.primary) - } + // Size + if !result.isEmpty { + Text(result.formattedSize) + .font(.body.monospacedDigit()) + .foregroundStyle(.primary) + } - // Risk badge - if !result.isEmpty { - RiskBadge(level: result.category.riskLevel) + // Risk badge + if !result.isEmpty { + RiskBadge(level: result.category.riskLevel) + } } + .padding(.vertical, 6) + .padding(.horizontal, 10) + .contentShape(Rectangle()) } - .padding(.vertical, 6) - .padding(.horizontal, 10) + .buttonStyle(.plain) + .disabled(result.isEmpty) .opacity(result.isEmpty ? 0.5 : 1) + .accessibilityElement(children: .combine) } private var iconColor: Color { diff --git a/Sources/Cacheout/Views/NodeModulesSection.swift b/Sources/Cacheout/Views/NodeModulesSection.swift index 2cba822..7728d62 100644 --- a/Sources/Cacheout/Views/NodeModulesSection.swift +++ b/Sources/Cacheout/Views/NodeModulesSection.swift @@ -120,44 +120,46 @@ struct NodeModulesRow: View { let onToggle: () -> Void var body: some View { - HStack(spacing: 10) { - Button(action: onToggle) { + Button(action: onToggle) { + HStack(spacing: 10) { Image(systemName: item.isSelected ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundStyle(item.isSelected ? .purple : .secondary) - } - .buttonStyle(.plain) - Image(systemName: "shippingbox.fill") - .foregroundStyle(.purple.opacity(0.7)) - .frame(width: 20) + Image(systemName: "shippingbox.fill") + .foregroundStyle(.purple.opacity(0.7)) + .frame(width: 20) - VStack(alignment: .leading, spacing: 1) { - Text(item.projectName) - .font(.body.weight(.medium)) - Text(item.projectPath.path.replacingOccurrences(of: FileManager.default.homeDirectoryForCurrentUser.path, with: "~")) - .font(.caption2) - .foregroundStyle(.tertiary) - .lineLimit(1) - .truncationMode(.middle) - } + VStack(alignment: .leading, spacing: 1) { + Text(item.projectName) + .font(.body.weight(.medium)) + Text(item.projectPath.path.replacingOccurrences(of: FileManager.default.homeDirectoryForCurrentUser.path, with: "~")) + .font(.caption2) + .foregroundStyle(.tertiary) + .lineLimit(1) + .truncationMode(.middle) + } - Spacer() + Spacer() - // Stale badge - if let badge = item.staleBadge { - Text(badge) - .font(.caption2.weight(.semibold)) - .padding(.horizontal, 5) - .padding(.vertical, 2) - .background(Color.orange.opacity(0.15), in: Capsule()) - .foregroundStyle(.orange) - } + // Stale badge + if let badge = item.staleBadge { + Text(badge) + .font(.caption2.weight(.semibold)) + .padding(.horizontal, 5) + .padding(.vertical, 2) + .background(Color.orange.opacity(0.15), in: Capsule()) + .foregroundStyle(.orange) + } - Text(item.formattedSize) - .font(.body.monospacedDigit()) + Text(item.formattedSize) + .font(.body.monospacedDigit()) + } + .padding(.vertical, 4) + .padding(.horizontal, 10) + .contentShape(Rectangle()) } - .padding(.vertical, 4) - .padding(.horizontal, 10) + .buttonStyle(.plain) + .accessibilityElement(children: .combine) } }