From c8c1c37dbf80b1d92fe2b8ebf8227929cd9c0ca8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 06:36:17 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Improve=20list=20row?= =?UTF-8?q?=20hit=20targets=20and=20VoiceOver=20support?= 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 | 79 ++++++++++--------- .../Cacheout/Views/NodeModulesSection.swift | 63 ++++++++------- 3 files changed, 77 insertions(+), 68 deletions(-) create mode 100644 .jules/palette.md diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 0000000..6f4e03d --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2024-05-24 - SwiftUI List Row Hit Targets & Accessibility +**Learning:** In SwiftUI, list rows containing interactive elements (like a small checkbox) and descriptive text often have poor UX because users expect the entire row to be clickable. Screen readers also treat the text and the button as separate elements, cluttering navigation. +**Action:** Wrap the entire row contents (e.g., `HStack`) in a `Button`, apply `.contentShape(Rectangle())` to make empty space clickable, use `.buttonStyle(.plain)` to avoid unwanted styling, and add `.accessibilityElement(children: .combine)` along with `.accessibilityAddTraits(.isSelected)` to unify the VoiceOver experience. diff --git a/Sources/Cacheout/Views/CategoryRow.swift b/Sources/Cacheout/Views/CategoryRow.swift index 2653aae..bfabfc6 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) + .opacity(result.isEmpty ? 0.5 : 1) } - .padding(.vertical, 6) - .padding(.horizontal, 10) - .opacity(result.isEmpty ? 0.5 : 1) + .buttonStyle(.plain) + .contentShape(Rectangle()) + .disabled(result.isEmpty) + .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..fbcac83 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) } - .padding(.vertical, 4) - .padding(.horizontal, 10) + .buttonStyle(.plain) + .contentShape(Rectangle()) + .accessibilityElement(children: .combine) + .accessibilityAddTraits(item.isSelected ? [.isSelected] : []) } }