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