Jetpack Compose

Vlastní contentDescription místo viditelného textu tlačítka (Jetpack Compose)

Někdy viditelný text prvku (např. název s počtem v závorce) nezní čtečce přečtený nahlas přirozeně, nebo chcete odečítači předat bohatší informaci, než kolik je vidět na obrazovce. Výchozí sémantika Compose prostě přečte to, co je uvnitř — pro úplné nahrazení je potřeba Modifier.clearAndSetSemantics, který ale zahodí sémantiku celého podstromu, včetně akce kliknutí, kterou by jinak sám vystavil clickable.

1. Špatně — spoléhat na výchozí přečtení viditelného textu

Row(
    modifier = Modifier
        .fillMaxWidth()
        .clickable { onClick() }
        .padding(horizontal = 16.dp, vertical = 14.dp),
    verticalAlignment = Alignment.CenterVertically
) {
    Icon(icon, contentDescription = null)
    Spacer(modifier = Modifier.width(16.dp))
    Text("Všechny položky (42)")
}

TalkBack přečte přesně to, co je vidět — "Všechny položky, otevírající závorka, 42, zavírající závorka" (přesná podoba závisí na syntetizátoru), místo srozumitelné věty o počtu nepřečtených.

2. Správně — clearAndSetSemantics s vlastním popisem

@Composable
fun DashboardRow(icon: ImageVector, title: String, unreadCount: Int, focusRequester: FocusRequester, onClick: () -> Unit) {
    val view = LocalView.current
    val countText = if (unreadCount > 999) "999+" else unreadCount.toString()
    val displayTitle = if (unreadCount > 0) "$title ($countText)" else title
    val description = if (unreadCount > 0) "$title, $unreadCount nepřečtených" else "$title, vše přečteno"

    Row(
        modifier = Modifier.fillMaxWidth()
            .focusRequester(focusRequester)
            .clickable { view.clickSound(); onClick() }
            .padding(horizontal = 16.dp, vertical = 14.dp)
            .clearAndSetSemantics {
                contentDescription = description
                role = Role.Button
                this.onClick { view.clickSound(); onClick(); true }
            },
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
        Spacer(modifier = Modifier.width(16.dp))
        Text(displayTitle, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
    }
}

clearAndSetSemantics zahodí sémantiku celého podstromu (včetně toho, co by do ní jinak přispěl vnořený Text) a nahradí ji přesně tím, co je v bloku. Proto description používá jiný, plynulejší tvar ("42 nepřečtených" / "vše přečteno") než viditelný text ("(42)") — čtečka ho nikdy nespojuje s viditelným textem, jde o dvě úplně nezávislé věci.

3. Kde se to používá

V mé aplikaci pro čtení RSS kanálů tímhle způsobem funguje úplně každá položka na domovské obrazovce — "Všechny položky", "S hvězdičkou", každá složka i každý jednotlivý kanál. Viditelný text má počet v závorce, přístupný popis má plnou větu ("42 nepřečtených" / "vše přečteno").

Takový řádek je zároveň klikatelný, takže potřebuje i zvukovou odezvu na dotyk — viz Zvuk při tapnutí na interaktivní prvky (Jetpack Compose).

Proč to takhle je

  • clearAndSetSemantics zahazuje sémantiku VŠECH potomků, ne jen doplňuje — proto se Text uvnitř nikdy nepřečte souběžně s description. Kdyby se místo toho použil obyčejný Modifier.semantics { contentDescription = ... } (bez "clearAndSet"), TalkBack by přečetl obojí za sebou — vlastní popis i viditelný text znovu.
  • role = Role.Button uvnitř blokuclearAndSetSemantics zahodí i roli, kterou by řádek jinak mohl mít, takže je potřeba ji nastavit znovu, jinak TalkBack řádek neohlásí jako tlačítko.
  • this.onClick { ... } uvnitř bloku — ze stejného důvodu: Modifier.clickable sám o sobě vystavuje do accessibility stromu i akci "kliknutí", ale clearAndSetSemantics ji zahodí spolu se vším ostatním. Bez ručního doplnění by dvojité poklepání v TalkBacku na řádku přestalo fungovat, i když by běžný dotyk (přes clickable) fungoval dál — proto se onClick() volá na obou místech, v clickable i uvnitř clearAndSetSemantics.
  • Přístupný popis a viditelný text se mohou lišit obsahem i formulací — nemusí to být to samé, jen jinak formátované; přístupná verze může nést informaci navíc (např. "vše přečteno" pro nulový počet, což viditelný text vůbec neřeší).

Pravidla

  • WCAG 4.1.2 (Název, role, hodnota) — přístupný název, role tlačítka i (v tomto případě prostřednictvím textu) stav musí být programově zjistitelné jako jeden konzistentní celek.

Shrnutí: Modifier.clearAndSetSemantics { contentDescription = ...; role = Role.Button; onClick {...} } úplně nahradí, co čtečka o prvku řekne — včetně role a akce kliknutí, které by jinak zajistil clickable. Používá se, když má přístupný popis nést jiné/bohatší informace než holý viditelný text, a je potřeba zabránit tomu, aby se ten viditelný text ještě přečetl navíc.

Štítky

Komentáře

Zatím zde nejsou žádné komentáře. Buďte první, kdo napíše svůj názor.

Přidat komentář k „Vlastní contentDescription místo viditelného textu tlačítka (Jetpack Compose)“