contentDescription — la descripción verbal

// Imagen informativa — necesita descripción
Image(
    painter = painterResource(R.drawable.foto_perfil),
    contentDescription = "Foto de perfil de ${usuario.nombre}"
    // TalkBack anuncia: "Foto de perfil de Carlos, imagen"
)

// Imagen decorativa — no necesita descripción (null la oculta del árbol)
Image(
    painter = painterResource(R.drawable.fondo_decorativo),
    contentDescription = null  // TalkBack la ignora completamente
)

// IconButton — SIEMPRE necesita descripción
IconButton(onClick = { viewModel.eliminar(item) }) {
    Icon(
        imageVector = Icons.Default.Delete,
        contentDescription = "Eliminar ${item.nombre}"
        // Incluir el contexto del item — "Eliminar" solo es ambiguo en una lista
    )
}

// ❌ contentDescription vacío — peor que nada
Icon(
    imageVector = Icons.Default.Delete,
    contentDescription = ""  // TalkBack dice "Sin etiqueta" — confunde más que ayuda
)

// ❌ contentDescription redundante con el texto visible
Button(onClick = { }) {
    Text("Guardar")
}
// NO agregar contentDescription = "Guardar" — el texto ya es la descripción
// Compose fusiona automáticamente el texto de los hijos con el nodo padre

Modifier.semantics {} — control fino

// semantics {} permite agregar o modificar propiedades semánticas custom
Box(
    modifier = Modifier
        .clickable { onSeleccionar() }
        .semantics {
            // Reemplazar la descripción que Compose generaría automáticamente
            contentDescription = "Producto: ${producto.nombre}, precio: ${producto.precio}"
            // Declarar el role para que TalkBack lo anuncie correctamente
            role = Role.Button
        }
) {
    ProductoCardContent(producto)
}

// Custom action — acción que TalkBack puede ejecutar sin que sea un click
Box(
    modifier = Modifier.semantics {
        customActions = listOf(
            CustomAccessibilityAction("Agregar al carrito") {
                onAgregarAlCarrito(producto)
                true
            },
            CustomAccessibilityAction("Ver detalles") {
                onVerDetalles(producto)
                true
            }
        )
    }
) { /* contenido */ }
// Con TalkBack, el usuario puede deslizar arriba o abajo para elegir la acción
// sin tener que navegar a botones separados dentro de la card

Roles — qué tipo de control es este

// Los roles le dicen a TalkBack cómo anunciar el elemento
// y qué comportamiento esperar

// Role.Button → "Botón" al final de la descripción
Box(modifier = Modifier.semantics { role = Role.Button }) { }

// Role.Checkbox → TalkBack anuncia "marcado" o "desmarcado"
Box(modifier = Modifier.semantics {
    role = Role.Checkbox
    stateDescription = if (seleccionado) "Seleccionado" else "No seleccionado"
}) { }

// Role.Switch → igual que Checkbox pero con "activado/desactivado"
Box(modifier = Modifier.semantics { role = Role.Switch }) { }

// Role.Tab → para tabs de navegación
Box(modifier = Modifier.semantics { role = Role.Tab }) { }

// Role.RadioButton → para radio buttons en un grupo
Box(modifier = Modifier.semantics { role = Role.RadioButton }) { }

// Role.Image → para imágenes informativas
Box(modifier = Modifier.semantics { role = Role.Image }) { }

// Los componentes de Material3 ya tienen el role correcto automáticamente:
// Button → Role.Button
// Checkbox → Role.Checkbox
// Switch → Role.Switch
// Tab → Role.Tab
// Solo necesitás asignar role manualmente en componentes custom

mergeDescendants — agrupar para TalkBack

Por defecto, cada composable es un nodo separado en el árbol de accesibilidad. En una card con imagen, título y descripción, TalkBack navegaría por tres elementos separados. mergeDescendants = true los fusiona en uno solo:

// ❌ Sin mergeDescendants — TalkBack navega a cada elemento por separado:
// Foco 1: "foto de Carlos, imagen"
// Foco 2: "Carlos Pensa, texto"
// Foco 3: "Desarrollador Android, texto"
Row {
    Image(painter = ..., contentDescription = "foto de Carlos")
    Column {
        Text("Carlos Pensa")
        Text("Desarrollador Android")
    }
}

// ✓ Con mergeDescendants — TalkBack lo lee todo de una vez:
// Foco único: "foto de Carlos, imagen. Carlos Pensa. Desarrollador Android."
Row(
    modifier = Modifier.semantics(mergeDescendants = true) {
        // También podés agregar una descripción personalizada para el grupo:
        contentDescription = "Carlos Pensa, Desarrollador Android"
    }
) {
    Image(painter = ..., contentDescription = null)  // null porque el grupo tiene descripción
    Column {
        Text("Carlos Pensa")
        Text("Desarrollador Android")
    }
}

// Cuándo usar mergeDescendants:
// ✓ Cards que representan un único item (usuario, producto, noticia)
// ✓ Filas de tabla con múltiples celdas relacionadas
// ✗ NO cuando los elementos tienen acciones individuales (botones dentro de la card)

stateDescription — describir el estado actual

// stateDescription describe el estado actual del componente
// TalkBack lo anuncia junto con la descripción

@Composable
fun FavoritoBoton(esFavorito: Boolean, onToggle: () -> Unit) {
    IconButton(
        onClick = onToggle,
        modifier = Modifier.semantics {
            contentDescription = "Favorito"
            stateDescription = if (esFavorito) "Marcado como favorito" else "No es favorito"
            // TalkBack anuncia: "Favorito, Marcado como favorito, botón"
            // o: "Favorito, No es favorito, botón"
        }
    ) {
        Icon(
            imageVector = if (esFavorito) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
            contentDescription = null  // null porque el semantics del padre ya tiene la descripción
        )
    }
}

// Para texto de error en campos de formulario:
OutlinedTextField(
    value = email,
    onValueChange = { email = it },
    isError = emailError != null,
    label = { Text("Email") },
    supportingText = emailError?.let { { Text(it) } },
    modifier = Modifier.semantics {
        if (emailError != null) {
            error(emailError)  // TalkBack anuncia el error automáticamente
        }
    }
)

Elementos decorativos — ocultarlos del árbol

// Separadores, fondos decorativos, íconos puramente visuales:
Divider(
    modifier = Modifier.semantics { invisibleToUser() }
    // TalkBack lo ignora completamente
)

// Texto decorativo que no agrega información:
Text(
    text = "•",  // bullet puramente visual
    modifier = Modifier.semantics { invisibleToUser() }
)

// Contenedor decorativo que no debería enfocarse:
Box(
    modifier = Modifier.semantics { hideFromAccessibility() }
) {
    // Todo el contenido dentro también queda oculto
}

Errores comunes en semántica de Compose

// ❌ Error 1: Describir el tipo en lugar de el contenido
Image(contentDescription = "Imagen")  // inútil — TalkBack ya dice "imagen"
Icon(contentDescription = "Ícono")    // inútil

// ✓ Describir QUÉ es o QUÉ hace
Image(contentDescription = "Mapa del centro de Buenos Aires")
Icon(contentDescription = "Menú principal")

// ❌ Error 2: Descripciones que empiezan con el tipo
contentDescription = "Botón de buscar"  // TalkBack dice "Botón de buscar, botón"
// ✓
contentDescription = "Buscar"  // TalkBack dice "Buscar, botón"

// ❌ Error 3: Box clickable sin role ni descripción
Box(modifier = Modifier.clickable { }) {
    Text("Ver más")
}
// TalkBack dice el texto "Ver más" pero no sabe que es un botón

// ✓
Box(
    modifier = Modifier
        .clickable { }
        .semantics { role = Role.Button }
) {
    Text("Ver más")
}
// TalkBack dice "Ver más, botón"