From 4ee27ed0dd613bf8b7afc193a2c2a1f35c083353 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 06:40:10 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Expand=20list=20row?= =?UTF-8?q?=20click=20targets=20and=20add=20a11y=20traits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/palette.md | 3 + Sources/Cacheout/Views/CategoryRow.swift | 77 ++++++++++--------- .../Cacheout/Views/NodeModulesSection.swift | 63 +++++++-------- 3 files changed, 76 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..d52e104 --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2025-02-14 - Expanded Hit Targets and Accessibility for List Rows +**Learning:** In SwiftUI, placing a small button (like a checkbox) inside an HStack row forces users to click exactly on the small icon, which is poor UX for lists. Additionally, VoiceOver reads each text element in the row separately unless combined. +**Action:** Wrap the entire HStack in a `Button`, use `.contentShape(Rectangle())` to make the entire row clickable (including empty space), apply `.buttonStyle(.plain)`, and use `.accessibilityElement(children: .combine)` with `.accessibilityAddTraits(.isSelected)` to provide a single, informative VoiceOver element. diff --git a/Sources/Cacheout/Views/CategoryRow.swift b/Sources/Cacheout/Views/CategoryRow.swift index 2653aae..89656d8 100644 --- a/Sources/Cacheout/Views/CategoryRow.swift +++ b/Sources/Cacheout/Views/CategoryRow.swift @@ -24,55 +24,58 @@ 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) + .accessibilityAddTraits(result.isSelected ? .isSelected : []) } private var iconColor: Color { diff --git a/Sources/Cacheout/Views/NodeModulesSection.swift b/Sources/Cacheout/Views/NodeModulesSection.swift index 2cba822..c5110e6 100644 --- a/Sources/Cacheout/Views/NodeModulesSection.swift +++ b/Sources/Cacheout/Views/NodeModulesSection.swift @@ -120,44 +120,47 @@ 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) + .accessibilityAddTraits(item.isSelected ? .isSelected : []) } }