From 51e43a50facfdd416490dda9a4931c4ded092083 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:33:19 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Make=20list=20rows=20?= =?UTF-8?q?fully=20clickable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wrapped CategoryRow and NodeModulesRow in Buttons - Added .contentShape(Rectangle()) to make empty space clickable - Added .buttonStyle(.plain) to prevent visual changes - Added .accessibilityElement(children: .combine) for 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..c770990 --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2026-03-27 - SwiftUI List Row Hit Targets and Accessibility +**Learning:** By default, placing a Button inside an HStack only makes the Button's icon clickable, creating a small hit target and causing VoiceOver to read elements separately. +**Action:** Wrap the entire HStack in a Button, add `.contentShape(Rectangle())` to make empty space clickable, use `.buttonStyle(.plain)` to prevent unintended styling, and add `.accessibilityElement(children: .combine)` to group the row content into a single accessible element. 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) } }