diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 0000000..b511d42 --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2024-04-08 - Clickable List Rows +**Learning:** In SwiftUI, wrapping an HStack in a Button and adding `.contentShape(Rectangle())` significantly increases the hit target for list items, while `.accessibilityElement(children: .combine)` ensures VoiceOver reads the entire row as a single actionable element. +**Action:** When creating clickable list rows, ensure the entire row contents are wrapped in a Button with a Rectangle content shape, and apply appropriate accessibility traits like `.isSelected`. diff --git a/Sources/Cacheout/Views/CategoryRow.swift b/Sources/Cacheout/Views/CategoryRow.swift index 2653aae..14c3e23 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 {