Jetpack Compose

Přístupný přepínač (Switch) v Jetpack Compose

Přepínač a jeho popisek vedle sebe vypadají jako jeden ovládací prvek, ale pokud jsou to dva samostatné composables, TalkBack je vidí jako dva samostatné uzly. Uživatel se dotykem/gestem dostane na Switch a uslyší jen "přepínač, zapnuto" nebo "přepínač, vypnuto" — bez popisku, bez kontextu, co vlastně přepíná. Popisek se pak ohlásí zvlášť jako neinteraktivní text o krok dál.

1. Špatně — Switch a Text vedle sebe (dvě samostatné položky)

Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically,
) {
    Text("Zvuk při dosažení poslední položky")
    Switch(checked = enabled, onCheckedChange = { enabled = it })
}

Text a Switch jsou tu dvě nezávislé komponenty, každá se svým vlastním sémantickým uzlem. TalkBack je ohlásí odděleně — a protože Switch sám o sobě žádný text nenese, dostane se k němu uživatel jen jako k "přepínači" bez jakéhokoli popisu. Navíc reaguje na dotyk jen přesně nad grafikou přepínače, ne nad celým řádkem.

2. Správně — sloučené do jednoho fokusovatelného kontejneru

@Composable
fun SwitchRow(
    label: String,
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
) {
    Row(
        modifier = modifier
            .fillMaxWidth()
            .toggleable(
                value = checked,
                onValueChange = onCheckedChange,
                role = Role.Switch,
            ),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(label)
        Switch(checked = checked, onCheckedChange = null)
    }
}

Modifier.toggleable(...) s role = Role.Switch udělá z celého Row jeden sémantický uzel s rolí přepínače a jeho aktuálním stavem. Switch uvnitř dostane onCheckedChange = null — díky tomu sám o sobě nevytváří vlastní klikatelný/fokusovatelný uzel, takže nevznikne druhá, vnořená položka se stejnou funkcí.

3. Kde se to v mých projektech používá

V mých aplikacích je tohle standardní vzor pro každý přepínač v nastavení — sdílená komponenta (SwitchRow), přes kterou jde každý labelovaný přepínač, žádný Row { Text(...); Switch(...) } napřímo. Stejný princip (celý řádek = jeden interaktivní uzel, vnitřní komponenta s onClick = null) používám i pro řádky s RadioButton přes Modifier.selectable(...).

Zvuková odezva na klepnutí (playSoundEffect) patří ke stejné skupině zvyků — viz Zvuk při tapnutí na interaktivní prvky (Jetpack Compose).

Proč to takhle je

  • role = Role.Switch na celém řádkuTalkBack ohlásí "popisek, přepínač, zapnuto/vypnuto" jedním krokem, ne jako dvě oddělené zastávky.
  • Switch(onCheckedChange = null) — vnitřní komponenta bez vlastního handleru nevytváří druhý klikatelný uzel. Kdyby handler zůstal i tady, TalkBack by na stejnou funkci narazil dvakrát: jednou na celém řádku, podruhé na vnořeném přepínači.
  • Celý řádek reaguje na dotyk — netýká se to jen odečítačů obrazovky; pomáhá to i lidem s menší přesností dotyku nebo třesem rukou, kteří by se jinak museli trefit přesně do malé grafiky přepínače.
  • Popisek musí být obsahem toho samého kontejneru (Text jako dítě Row), ne vedle něj mimo toggleable modifier — jinak sémantika role a stavu nemá k čemu přiřadit název.

Pravidla

  • WCAG 4.1.2 (Název, role, hodnota) — přepínač musí programově vystavovat svou roli, aktuální stav (zapnuto/vypnuto) i svůj název jako jeden celek, ne jako tři nesouvisející kusy informace.

Shrnutí: přepínač a jeho popisek patří do jednoho Modifier.toggleable(...) kontejneru s role = Role.Switch, vnitřní Switch dostane onCheckedChange = null. Bez toho TalkBack vidí dva prvky místo jednoho a uživatel se dozví "přepínač" bez kontextu, co přepíná.

Š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 „Přístupný přepínač (Switch) v Jetpack Compose“