fix(worldclock): keep a ticking row's accessibility actions stable
With seconds on every row recomposed each second and rebuilt its custom actions from new lambdas, so TalkBack saw changed semantics every second. The callbacks are remembered per id and the action list per label and callback. The prefs isolation test now uses a barrier on the same flow, a weak settings assertion is gone, and a clock set is tested to show at once and restart the minute tick.
This commit is contained in:
1 parent
0218339161
commit
47165ce93f
5 files changed
+53
-25
No files matched your search
@@ -83,20 +83,27 @@ internal fun WorldClockRow(
|
||||
} else {
|
||||
stringResource(R.string.world_clock_row_description_unknown, row.name, summary)
|
||||
}
|
||||
val actions = listOf(
|
||||
CustomAccessibilityAction(stringResource(R.string.world_clock_row_move_up)) {
|
||||
onMoveUp()
|
||||
true
|
||||
},
|
||||
CustomAccessibilityAction(stringResource(R.string.world_clock_row_move_down)) {
|
||||
onMoveDown()
|
||||
true
|
||||
},
|
||||
CustomAccessibilityAction(stringResource(R.string.world_clock_row_remove)) {
|
||||
onRemove()
|
||||
true
|
||||
},
|
||||
)
|
||||
val moveUpLabel = stringResource(R.string.world_clock_row_move_up)
|
||||
val moveDownLabel = stringResource(R.string.world_clock_row_move_down)
|
||||
val removeLabel = stringResource(R.string.world_clock_row_remove)
|
||||
// Remembered, so a row that ticks every second keeps equal actions and its
|
||||
// semantics only change when what TalkBack would say changes.
|
||||
val actions = remember(moveUpLabel, moveDownLabel, removeLabel, onMoveUp, onMoveDown, onRemove) {
|
||||
listOf(
|
||||
CustomAccessibilityAction(moveUpLabel) {
|
||||
onMoveUp()
|
||||
true
|
||||
},
|
||||
CustomAccessibilityAction(moveDownLabel) {
|
||||
onMoveDown()
|
||||
true
|
||||
},
|
||||
CustomAccessibilityAction(removeLabel) {
|
||||
onRemove()
|
||||
true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
GroupedSurface(
|
||||
position = position,
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -158,9 +159,11 @@ fun WorldClockScreen(
|
||||
timeStyle = rowTimeStyle,
|
||||
dragHandle = dragHandle,
|
||||
isDragging = isDragging,
|
||||
onMoveUp = { viewModel.onMoveUp(row.id) },
|
||||
onMoveDown = { viewModel.onMoveDown(row.id) },
|
||||
onRemove = { pendingRemoval = row.id },
|
||||
// Remembered per id: a row that ticks every second must
|
||||
// not hand TalkBack new custom actions every second.
|
||||
onMoveUp = remember(id) { { viewModel.onMoveUp(id) } },
|
||||
onMoveDown = remember(id) { { viewModel.onMoveDown(id) } },
|
||||
onRemove = remember(id) { { pendingRemoval = id } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +90,11 @@ class ClockPrefsTest {
|
||||
assertThat(awaitItem()).isEqualTo(ClockDefaults())
|
||||
|
||||
prefs.setWorldClockSeconds(true)
|
||||
// Wait until the write is visible to a reader, so a re-emission of
|
||||
// the defaults would have had its chance to arrive.
|
||||
prefs.worldClockSeconds.first { it }
|
||||
// A barrier on this same flow: the next item must be this write,
|
||||
// so a re-emission caused by the seconds write would come first.
|
||||
prefs.setSnoozeMinutes(7)
|
||||
|
||||
assertThat(awaitItem()).isEqualTo(ClockDefaults(snoozeMinutes = 7))
|
||||
expectNoEvents()
|
||||
cancel()
|
||||
}
|
||||
|
||||
@@ -130,18 +130,15 @@ class SettingsViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the world clock's seconds switch writes through, apart from the defaults`(@TempDir tempDir: Path) =
|
||||
fun `the world clock's seconds switch writes through`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
val viewModel = collecting(newViewModel(tempDir))
|
||||
advanceUntilIdle()
|
||||
val defaultsBefore = (viewModel.state.value as SettingsUiState.Loaded).defaults
|
||||
|
||||
viewModel.onWorldClockSecondsChanged(true)
|
||||
advanceUntilIdle()
|
||||
|
||||
val loaded = viewModel.state.value as SettingsUiState.Loaded
|
||||
assertThat(loaded.worldClockSeconds).isTrue()
|
||||
assertThat(loaded.defaults).isEqualTo(defaultsBefore)
|
||||
assertThat((viewModel.state.value as SettingsUiState.Loaded).worldClockSeconds).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.ZoneId
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
@@ -414,6 +415,25 @@ class WorldClockViewModelTest {
|
||||
assertThat(harness.loaded().home.zoneId).isEqualTo("Asia/Tokyo")
|
||||
}
|
||||
|
||||
/** #23: a set clock shows at once, and the tick restarts against the new time. */
|
||||
@Test
|
||||
fun `a clock set shows at once and restarts the minute tick`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
val harness = collecting(worldClockHarness(tempDir))
|
||||
advanceUntilIdle()
|
||||
val before = harness.loaded().home.time
|
||||
|
||||
harness.wallClock.advance(5.minutes)
|
||||
harness.clockChanges.fire()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(harness.loaded().home.time).isEqualTo(before.plusMinutes(5))
|
||||
assertThat(harness.ticker.requestedUnits).containsExactly(
|
||||
WorldClockDefaults.tickFor(showSeconds = false),
|
||||
WorldClockDefaults.tickFor(showSeconds = false),
|
||||
)
|
||||
}
|
||||
|
||||
/** §5.9 #20, with #23's seconds on. */
|
||||
@Test
|
||||
fun `a second of wall clock moves the face and the rows when seconds are on`(@TempDir tempDir: Path) =
|
||||
|
||||
Reference in new issue
Block a user