Accessibility
Making truly accessible apps is complex and requires familiarity with a wide range of concepts. The Web Content Accessibility Guidelines provide recommendations for web apps.
You must include the AccessibilityModule
(Web, Desktop) in your application in order to use these features.
package accessibility.module
import io.nacular.doodle.application.Modules.Companion.AccessibilityModule
import io.nacular.doodle.application.application
import org.kodein.di.instance
import rendering.MyApp
fun main() {
//sampleStart
application(modules = listOf(AccessibilityModule)) {
MyApp(instance(), instance())
}
//sampleEnd
}
Doodle uses opt-in modules like this to improve bundle size.
package accessibility
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.Modules.Companion.AccessibilityModule
import io.nacular.doodle.application.application
//sampleStart
class MyApp(/*...*/): Application {
init {
// use accessibility features
}
override fun shutdown() {}
}
fun main () {
// Include the AccessibilityModule to enable features
application(modules = listOf(AccessibilityModule)) {
MyApp(/*...*/)
}
}
//sampleEnd
Descriptive Text
accessibilityLabel
Authors can provide short, descriptive text that is used by assistive technologies to announce a View when it is selected. This property helps in cases where a View contains no meaningful text.
package accessibility
import io.nacular.doodle.controls.buttons.PushButton
//sampleStart
val button = PushButton("x").apply {
accessibilityLabel = "Close the window"
}
//sampleEnd
accessibilityLabelProvider
In many cases the app presents descriptive text to the user directly using other Views, like labels for a text fields. The accessibilityLabelProvider
points to another View that should be used as a "label" for the current one.
package accessibility
import io.nacular.doodle.controls.text.Label
import io.nacular.doodle.controls.text.TextField
//sampleStart
val label = Label("Enter your name")
val textField = TextField().apply {
accessibilityLabelProvider = label
}
//sampleEnd
Views can be linked this way at any time, even if they are not both currently displayed. Doodle will track the relationship, and surface it to assistive technologies if the Views are simultaneously displayed.
accessibilityDescriptionProvider
Labels should be short descriptive names for a View. But it is possible to provide more detailed descriptions as well via the
accessibilityDescriptionProvider
. This property behaves like accessibilityLabelProvider
, but is intended for longer, more detailed text that describes the View.
Accessibility Roles
Authors can indicate that a View plays a well-defined role in the app by tagging it with an accessibility role. This enables assistive technologies to change the presentation of the View to the user as she navigates a scene. This is done by setting the View's accessibilityRole
.
Here is an example of creating a View that will serve as a ButtonRole
.
package accessibility
import io.nacular.doodle.accessibility.ButtonRole
import io.nacular.doodle.core.View
//sampleStart
class CustomButton: View(accessibilityRole = ButtonRole()) {
// ...
}
//sampleEnd
This View will now be treated as a button by accessibility technologies (i.e. screen readers). The button
role itself does not have additional properties, so simply adopting it is sufficient.
Other roles have state and must be synchronized with the View to ensure proper assistive support.
package accessibility
import io.nacular.doodle.accessibility.ToggleButtonRole
import io.nacular.doodle.core.View
import io.nacular.doodle.utils.observable
//sampleStart
class CustomToggle(private val role: ToggleButtonRole = ToggleButtonRole()): View(accessibilityRole = role) {
var selected by observable(false) { old, new ->
role.pressed = new
}
}
//sampleEnd
Many of the widgets in the Controls
library ship with accessibility support (though this continues to improve). The library also provides bindings for some roles and models, which makes it easier to synchronize roles with their widgets. ValueSlider
, for example, binds its role to the ConfinedValueModel
that underlies it. This way the role and View are always in sync.