Skip to main content

Accessibility Features

Overview

FlorisBoard is designed with accessibility in mind, providing features to support users with disabilities including screen reader support, haptic feedback, audio feedback, and customizable UI elements.

Introduction

Accessibility is a core principle in FlorisBoard's design. This document covers the accessibility features implemented to ensure the keyboard is usable by everyone, including users who rely on assistive technologies like TalkBack, Switch Access, and other accessibility services.

Key Concepts

Screen Reader Support

FlorisBoard provides comprehensive support for Android's TalkBack screen reader through:

  • Content Descriptions: All interactive elements have meaningful content descriptions
  • Semantic Markup: Proper use of Compose semantics for accessibility
  • Announcement Support: Important state changes are announced to screen readers
  • Navigation Support: Logical focus order and navigation

Haptic Feedback

Tactile feedback helps users confirm key presses without visual confirmation:

  • Key Press Feedback: Vibration on key press
  • Long Press Feedback: Different vibration pattern for long press
  • Gesture Feedback: Haptic response for swipe gestures
  • Customizable Intensity: Adjustable vibration strength

Audio Feedback

Sound feedback provides auditory confirmation of actions:

  • Key Click Sounds: Audio feedback on key press
  • System Sounds: Integration with Android system sounds
  • Volume Control: Adjustable sound volume
  • Sound Customization: Different sounds for different key types

Visual Accessibility

  • High Contrast Themes: Support for high contrast color schemes
  • Customizable Font Sizes: Adjustable text size for better readability
  • Color Customization: Theme system allows custom colors
  • Clear Visual Feedback: Visual indication of key presses and states

Implementation Details

Content Descriptions in Compose

FlorisBoard uses Jetpack Compose's semantic properties to provide accessibility information:

@Composable
fun SnyggIcon(
elementName: String? = null,
attributes: SnyggQueryAttributes = emptyMap(),
selector: SnyggSelector? = null,
modifier: Modifier = Modifier,
imageVector: ImageVector,
contentDescription: String? = null,
) {
ProvideSnyggStyle(elementName, attributes, selector) { style ->
Icon(
modifier = modifier.snyggIconSize(style),
imageVector = imageVector,
contentDescription = contentDescription,
tint = style.foreground(),
)
}
}

Accessibility Class Names

Views provide accessibility class names for proper identification:

override fun getAccessibilityClassName(): CharSequence {
return javaClass.name
}

Haptic Feedback Controller

The InputFeedbackController manages haptic and audio feedback:

class InputFeedbackController {
fun keyPress(data: KeyData) {
if (prefs.inputFeedback.hapticEnabled.get()) {
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
if (prefs.inputFeedback.audioEnabled.get()) {
playKeySound(data)
}
}

fun keyLongPress(data: KeyData) {
if (prefs.inputFeedback.hapticEnabled.get()) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
}
}

Keyboard Enabled State

Keys can be enabled or disabled based on context, with proper accessibility support:

override fun evaluateEnabled(data: KeyData): Boolean {
return when (data.code) {
KeyCode.CLIPBOARD_COPY,
KeyCode.CLIPBOARD_CUT -> {
state.isSelectionMode && editorInfo.isRichInputEditor
}
KeyCode.CLIPBOARD_PASTE -> {
!androidKeyguardManager.let { it.isDeviceLocked || it.isKeyguardLocked }
&& clipboardManager.canBePasted(clipboardManager.primaryClip)
}
...
}
}

Physical Keyboard Support

FlorisBoard supports physical keyboards with accessibility features:

  • Show/Hide On-Screen Keyboard: Option to hide keyboard when physical keyboard is connected
  • Hardware Key Handling: Proper handling of hardware key events
  • Keyboard Shortcuts: Support for common keyboard shortcuts

Code Examples

Adding Content Descriptions

@Composable
fun AccessibleButton(
onClick: () -> Unit,
label: String,
contentDescription: String,
) {
SnyggButton(
onClick = onClick,
modifier = Modifier.semantics {
this.contentDescription = contentDescription
this.role = Role.Button
}
) {
Text(text = label)
}
}

Announcing State Changes

@Composable
fun KeyboardModeSwitch() {
val context = LocalContext.current
val view = LocalView.current

LaunchedEffect(keyboardMode) {
view.announceForAccessibility(
context.getString(R.string.keyboard_mode_changed, keyboardMode.name)
)
}
}

Haptic Feedback Integration

@Composable
fun EmojiKey(emoji: Emoji, onEmojiInput: (Emoji) -> Unit) {
val inputFeedbackController = LocalInputFeedbackController.current

SnyggBox(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = {
inputFeedbackController.keyPress(TextKeyData.UNSPECIFIED)
},
onTap = {
onEmojiInput(emoji)
},
onLongPress = {
inputFeedbackController.keyLongPress(TextKeyData.UNSPECIFIED)
},
)
},
) {
EmojiText(text = emoji.value)
}
}

Best Practices

1. Always Provide Content Descriptions

Every interactive element should have a meaningful content description:

Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.settings_button_description)
)

2. Use Semantic Properties

Leverage Compose's semantic properties for better accessibility:

Modifier.semantics {
contentDescription = "Delete key"
role = Role.Button
stateDescription = if (enabled) "Enabled" else "Disabled"
}

3. Announce Important Changes

Use announceForAccessibility() for important state changes:

view.announceForAccessibility("Caps lock enabled")

4. Support Multiple Feedback Modes

Provide haptic, audio, and visual feedback:

fun provideKeyFeedback(key: KeyData) {
// Visual feedback
showKeyHighlight(key)

// Haptic feedback
if (hapticsEnabled) performHapticFeedback()

// Audio feedback
if (soundEnabled) playKeySound()
}

5. Respect System Accessibility Settings

Check and respect system accessibility preferences:

val accessibilityManager = context.getSystemService(AccessibilityManager::class.java)
if (accessibilityManager.isEnabled) {
// Adjust UI for accessibility
}

Common Patterns

Accessible Custom Views

@Composable
fun AccessibleKeyboardKey(
key: TextKey,
onKeyPress: () -> Unit,
) {
val label = key.computedData.label
val contentDesc = buildString {
append(label)
if (key.computedData.type == KeyType.SHIFT) {
append(", shift key")
}
}

Box(
modifier = Modifier
.semantics {
contentDescription = contentDesc
role = Role.Button
}
.clickable(onClick = onKeyPress)
) {
Text(text = label)
}
}

Focus Management

@Composable
fun KeyboardLayout() {
val focusManager = LocalFocusManager.current

LaunchedEffect(isVisible) {
if (isVisible) {
// Request focus on first key
focusManager.moveFocus(FocusDirection.Next)
}
}
}

Troubleshooting

TalkBack Not Reading Keys

Problem: Screen reader doesn't announce key presses.

Solutions:

  • Ensure content descriptions are set
  • Check semantic properties are properly applied
  • Verify accessibility services are enabled
  • Test with Modifier.semantics { }

Haptic Feedback Not Working

Problem: No vibration on key press.

Solutions:

  • Check if haptic feedback is enabled in preferences
  • Verify device supports haptic feedback
  • Ensure isHapticFeedbackEnabled = true on view
  • Check system haptic feedback settings

High Contrast Mode Issues

Problem: UI elements not visible in high contrast mode.

Solutions:

  • Use theme system for colors
  • Avoid hardcoded colors
  • Test with high contrast themes
  • Ensure sufficient color contrast ratios

Next Steps


Note: This documentation is continuously being improved. Contributions are welcome!