From 91263674a917a77c020a527a301490d649344b3a Mon Sep 17 00:00:00 2001 From: QIU Date: Wed, 26 Aug 2026 07:13:43 +0000 Subject: [PATCH] fix(log): a screen reader could not delete a contact at all A Material 3 conformance audit measured three real defects on the logging screen. Deleting was reachable only by dragging. SwipeDeleteRow declared no semantics, so TalkBack saw a row of text with no actions - a switch or Voice Access user could not delete a record, not with difficulty but at all. The arming threshold was 75% of row width, roughly 249dp of continuous travel on a 360dp phone, against 120dp in this project's own SwipeableItem. Delete and undo are now custom accessibility actions on the row, which is the case the Compose accessibility guide names explicitly: swipe gestures should be exposed this way because they are hard or impossible for users with motor impairments. The undo affordance was a 29dp target with a five-second countdown running behind it - the worst place in the screen to be hard to hit, because a miss is unrecoverable. Now 48dp by 64dp, matching the mode and time rows. The trash glyph was the emoji U+1F5D1, which renders differently on every device and font and which this project forbids as an icon. ic_delete.xml already existed and is used in three other screens. Not addressed, and worth recording from the same audit: the table grid line at 0xFF3A3A3A computes to 1.65:1 against its background where Material asks 3:1, so the grid the Log page is built around is nearly invisible and gone in sunlight; and under the app's night filter the green upload tick collapses to 1.27:1 while being encoded in colour alone. --- .../src/main/res/values-zh/strings.xml | 1 + .../src/main/res/values/strings.xml | 1 + .../rtbishop/look4sat/feature/radar/LogTab.kt | 55 ++++++++++++++++--- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index 6769961b..e56b46b1 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -318,6 +318,7 @@ Cookie 复制 撤销 + 删除这条联络 时间 频率 卫星 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 10b7a2a9..1cf5724a 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -349,6 +349,7 @@ Cookie Copy Undo + Delete this contact Time Freq Satellite diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt index 061f344b..ed1b2320 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt @@ -18,6 +18,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectHorizontalDragGestures import com.rtbishop.look4sat.core.presentation.LocalSpacing import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.ui.res.painterResource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -59,6 +62,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.customActions import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -574,10 +580,33 @@ internal fun SwipeDeleteRow( } } + val deleteActionLabel = stringResource(id = R.string.wavelog_delete_action) + val undoActionLabel = stringResource(id = R.string.wavelog_delete_undo) Box( modifier = Modifier .fillMaxWidth() .onSizeChanged { rowWidth = it.width } + // Deleting was reachable ONLY by dragging, so TalkBack, switch access and Voice Access + // users could not delete a record at all - not with difficulty, at all. The Compose + // accessibility guide names this exact case: swipe gestures should be exposed as custom + // actions because they are hard or impossible for users with motor impairments. + .semantics { + customActions = buildList { + if (pending) { + add(CustomAccessibilityAction(undoActionLabel) { + pending = false + offsetX = 0f + true + }) + } else { + add(CustomAccessibilityAction(deleteActionLabel) { + pending = true + offsetX = -threshold + true + }) + } + } + } ) { // Background layer (right-side icons: trash / undo + countdown) Box( @@ -603,22 +632,34 @@ internal fun SwipeDeleteRow( fontSize = 13.sp, fontWeight = FontWeight.Bold, color = WaveLogYellow, + textAlign = TextAlign.Center, modifier = Modifier .clip(RoundedCornerShape(8.dp)) .background(WaveLogYellow.copy(alpha = 0.15f)) - .clickable { + // 48dp, the Material minimum. This was a 29dp target with a five-second + // countdown running behind it, so a miss was unrecoverable - the worst + // possible place in the screen to be hard to hit. + .heightIn(min = 48.dp) + .widthIn(min = 64.dp) + .clickable(role = Role.Button) { pending = false offsetX = 0f } - .padding(horizontal = 10.dp, vertical = 6.dp) + .wrapContentHeight(Alignment.CenterVertically) + .padding(horizontal = 10.dp) ) } } else { - // Trash icon (always visible while swiping, yellow) - Text( - text = "🗑", - fontSize = 20.sp, - modifier = Modifier.padding(end = 16.dp) + // A real icon, not an emoji: emoji render differently on every device and font, and + // this project forbids them as icons. contentDescription is null because the row + // already exposes delete as a custom accessibility action. + Icon( + painter = painterResource(id = R.drawable.ic_delete), + contentDescription = null, + tint = WaveLogYellow, + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp) ) } }