diff --git a/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/WorldClockInstrumentedTest.kt b/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/WorldClockInstrumentedTest.kt index ee70c42..a8ece3b 100644 --- a/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/WorldClockInstrumentedTest.kt +++ b/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/WorldClockInstrumentedTest.kt @@ -5,7 +5,6 @@ import android.content.Intent import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.junit4.v2.createEmptyComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput @@ -94,7 +93,9 @@ class WorldClockInstrumentedTest { val home = java.time.ZoneId.systemDefault().id.substringAfterLast('/') - composeTestRule.onNodeWithContentDescription(home, substring = true).assertExists() + // The face itself is hidden from TalkBack; the readout and caption + // under it are one merged node that names the city. + composeTestRule.onNodeWithText(home, substring = true).assertExists() } /** §5.11 #2 */ diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockFace.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockFace.kt index 674d3fa..2c9ea05 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockFace.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockFace.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.unit.dp import androidx.graphics.shapes.Morph import de.jeanlucmakiola.clockula.domain.worldclock.AnalogFace @@ -40,17 +39,15 @@ import java.time.LocalTime * colour, space and motion" means. M7 refused a decorative arc on the stopwatch * for the mirror-image reason: there, there was nothing to be a fraction of. * - * Scheme tokens only, no literal colour. The semantics are set outright — an - * analog face has nothing a screen reader can read off it — and deliberately - * **not** a live region: at 1 Hz it would recite the clock for as long as the - * tab was open, the mistake M4 already refused for the live pill. + * Scheme tokens only, no literal colour. The face is hidden from screen + * readers: the time readout under it says the same time as text, so + * describing the face as well would read the time twice. */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable internal fun WorldClockFace( time: LocalTime, dayFraction: Float, - contentDescription: String, modifier: Modifier = Modifier, ) { val dialColor = MaterialTheme.colorScheme.secondaryContainer @@ -81,7 +78,7 @@ internal fun WorldClockFace( Canvas( modifier = modifier .size(WorldClockDefaults.HeroFaceSize) - .clearAndSetSemantics { this.contentDescription = contentDescription }, + .clearAndSetSemantics {}, ) { drawDial(morph, progress, dialPath, dialMatrix, dialColor) drawTicks(markColor) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt index 4c54bbf..abac9b9 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt @@ -46,10 +46,13 @@ import java.time.LocalTime * The World clock tab: a hero analog face for **home**, then the cities, each * with its offset and day relative to home. * - * The face is the home readout and the digital time is deliberately not - * repeated under it — an analog face that also prints its own time is a face - * nobody looks at (D11). The caption is where the ICU long zone name is shown, - * which is the one place it earns its length. + * Under the face, the home time is repeated as a digital readout in the app's + * 12/24-hour format. This reverses D11, which left it out because an analog + * face that also prints its own time is a face nobody looks at: reading the + * exact minute off a dial is slower than reading it off digits, and that + * minute is what most visits to the tab are for. The face still carries what + * digits cannot: day or night at home, at a glance. The caption is where the + * ICU long zone name is shown, which is the one place it earns its length. * * `scrollable` stays true: `ReorderableColumn` is not lazy and cannot live * inside a `LazyColumn`, and a list capped at twenty-four rows is exactly the @@ -187,40 +190,52 @@ private fun HomeFace(home: HomeFaceState, formatTime: (LocalTime) -> String) { .fillMaxWidth() .padding(horizontal = GroupedListInset, vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp), ) { + // Hidden from TalkBack: the readout below says the same time. WorldClockFace( time = home.time, dayFraction = home.dayFraction, - contentDescription = stringResource( - R.string.world_clock_face_description, - home.city, - formatTime(home.time), - ), modifier = Modifier.padding(bottom = 12.dp), ) - // The face has no other label saying whose time it is, and a city here - // is otherwise indistinguishable from a city in the list below. - Text( - text = stringResource(R.string.world_clock_home), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - Text( - text = home.city, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - ) - // The one place the ICU long zone name earns its length. - home.zoneName?.let { name -> + // One TalkBack stop for the time and whose time it is ("14:05, Home, + // Berlin, Central European Standard Time") rather than four. Not a + // live region: it would recite the clock every minute for as long as + // the tab was open, the mistake M4 already refused for the live pill. + Column( + modifier = Modifier.semantics(mergeDescendants = true) {}, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { Text( - text = name, - style = MaterialTheme.typography.bodyMedium, + text = formatTime(home.time), + style = MaterialTheme.typography.displaySmall, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 8.dp), + ) + // The face has no other label saying whose time it is, and a city + // here is otherwise indistinguishable from a city in the list below. + Text( + text = stringResource(R.string.world_clock_home), + style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, ) + Text( + text = home.city, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + // The one place the ICU long zone name earns its length. + home.zoneName?.let { name -> + Text( + text = name, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 733f2cb..96fac52 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -227,7 +227,6 @@ Today Tomorrow This time zone is no longer on your device - %1$s, %2$s %1$s, %2$s, %3$s %1$s, %2$s diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6d12835..3ed5097 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2383,12 +2383,17 @@ The rest is ordinary `Canvas` work over `AnalogFace`'s pure angles — twelve ho marks with the four quarters longer, three hands, a centre cap — in scheme tokens only. The morph is built once and the path re-derived into a reused `Path` per frame through `toPath(Morph, Float, Path)`, which is not -`@Composable` and is therefore callable from a `DrawScope`. The face is the home -readout and the digital time is **not** repeated under it: an analog face that -also prints its own time is a face nobody looks at. It carries a content -description naming the city and the time, and is deliberately **not** a live -region — at 1 Hz it would recite the clock for as long as the tab was open, -which is the mistake M4 already refused for the live pill (§12). +`@Composable` and is therefore callable from a `DrawScope`. M8 left the digital +time out from under the face on the grounds that an analog face that also +prints its own time is a face nobody looks at (D11). **That is reversed:** the +home time now sits under the face as a `displaySmall` readout in the app's +12/24-hour format, because the exact minute is what most visits to the tab are +for and a dial is the slow way to read it. The face keeps the day/night shape +digits cannot carry. With the time on screen as text, the face is hidden from +TalkBack, and the readout, "Home", the city and the zone name are one merged +node, so the time is read once. Neither is a live region — that would recite +the clock for as long as the tab was open, which is the mistake M4 already +refused for the live pill (§12). ### The cadence