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.
This commit is contained in:
mckero committed 2026-08-26 07:13:43 +00:00
1 parent 9576607fc6
commit 91263674a9
3 files changed
+50 -7

No files matched your search

@@ -318,6 +318,7 @@
<string name="prefs_wavelog_qrz_label">Cookie</string>
<string name="wavelog_copy">复制</string>
<string name="wavelog_delete_undo">撤销</string>
<string name="wavelog_delete_action">删除这条联络</string>
<string name="wavelog_col_time">时间</string>
<string name="wavelog_col_freq">频率</string>
<string name="wavelog_col_sat">卫星</string>
@@ -349,6 +349,7 @@
<string name="prefs_wavelog_qrz_label">Cookie</string>
<string name="wavelog_copy">Copy</string>
<string name="wavelog_delete_undo">Undo</string>
<string name="wavelog_delete_action">Delete this contact</string>
<string name="wavelog_col_time">Time</string>
<string name="wavelog_col_freq">Freq</string>
<string name="wavelog_col_sat">Satellite</string>
@@ -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)
)
}
}