(a11y) add more screen reader labels

Close #975
This commit is contained in:
MM20 2024-07-23 22:12:54 +02:00
parent 8ae5fdfcda
commit 7ec17b7de8
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
15 changed files with 110 additions and 39 deletions

View File

@ -36,7 +36,10 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.SearchBarStyle import de.mm20.launcher2.preferences.SearchBarStyle
@ -62,6 +65,7 @@ fun SearchBar(
) { ) {
val transition = updateTransition(level, label = "Searchbar") val transition = updateTransition(level, label = "Searchbar")
val context = LocalContext.current
val elevation by transition.animateDp( val elevation by transition.animateDp(
label = "elevation", label = "elevation",
@ -169,7 +173,10 @@ fun SearchBar(
if (it.hasFocus) onFocus() if (it.hasFocus) onFocus()
} }
.focusRequester(focusRequester) .focusRequester(focusRequester)
.fillMaxWidth(), .fillMaxWidth()
.semantics {
contentDescription = context.getString(R.string.search_bar_placeholder)
},
textStyle = MaterialTheme.typography.titleMedium.copy( textStyle = MaterialTheme.typography.titleMedium.copy(
color = contentColor color = contentColor
), ),

View File

@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.integerResource import androidx.compose.ui.res.integerResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.ktx.toDp import de.mm20.launcher2.ui.ktx.toDp
@ -62,7 +63,7 @@ fun Icons(actions: List<ToolbarAction>, slots: Int) {
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
Box { Box {
IconButton(onClick = { showMenu = true }) { IconButton(onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, contentDescription = "") Icon(Icons.Rounded.MoreVert, contentDescription = stringResource(R.string.action_more_actions))
} }
Box( Box(
modifier = Modifier modifier = Modifier

View File

@ -591,7 +591,7 @@ fun PagerScaffold(
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = { viewModel.setWidgetEditMode(false) }) { IconButton(onClick = { viewModel.setWidgetEditMode(false) }) {
Icon(imageVector = Icons.Rounded.Done, contentDescription = null) Icon(imageVector = Icons.Rounded.Done, contentDescription = stringResource(R.string.action_done))
} }
}, },
) )

View File

@ -567,7 +567,7 @@ fun PullDownScaffold(
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = { viewModel.setWidgetEditMode(false) }) { IconButton(onClick = { viewModel.setWidgetEditMode(false) }) {
Icon(imageVector = Icons.Rounded.Done, contentDescription = null) Icon(imageVector = Icons.Rounded.Done, contentDescription = stringResource(R.string.action_done))
} }
} }
) )

View File

@ -341,7 +341,7 @@ fun AppItem(
) { ) {
Icon( Icon(
if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline, if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline,
null stringResource(if (isPinned) R.string.menu_favorites_unpin else R.string.menu_favorites_pin),
) )
} }
} }

View File

@ -4,9 +4,7 @@ import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
@ -43,6 +41,8 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -159,7 +159,10 @@ fun GridItem(
MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.surfaceVariant,
iconShape iconShape
) )
} else Modifier, } else Modifier then if (showLabels) Modifier else Modifier
.semantics {
contentDescription = item.label
},
) { ) {
ShapedLauncherIcon( ShapedLauncherIcon(
modifier = Modifier modifier = Modifier
@ -209,16 +212,20 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
} }
LaunchedEffect(show.targetState) { LaunchedEffect(show.targetState) {
if (!show.targetState) { if (!show.targetState) {
animationProgress.animateTo(0f, spring( animationProgress.animateTo(
0f, spring(
Spring.DampingRatioNoBouncy, Spring.DampingRatioNoBouncy,
Spring.StiffnessMediumLow, Spring.StiffnessMediumLow,
)) )
)
onDismissRequest() onDismissRequest()
} else { } else {
animationProgress.animateTo(1f, spring( animationProgress.animateTo(
1f, spring(
Spring.DampingRatioLowBouncy, Spring.DampingRatioLowBouncy,
Spring.StiffnessMediumLow, Spring.StiffnessMediumLow,
)) )
)
} }
} }
BackHandler { BackHandler {
@ -228,7 +235,14 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
Overlay { Overlay {
Box( Box(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f * animationProgress.value.coerceIn(0f, 1f))) .background(
MaterialTheme.colorScheme.scrim.copy(
alpha = 0.32f * animationProgress.value.coerceIn(
0f,
1f
)
)
)
.fillMaxSize() .fillMaxSize()
.systemBarsPadding() .systemBarsPadding()
.imePadding() .imePadding()

View File

@ -50,6 +50,9 @@ import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.roundToIntRect
@ -140,7 +143,10 @@ fun AppShortcutItem(
}.collectAsState(null) }.collectAsState(null)
InputChip( InputChip(
modifier = Modifier.width(IntrinsicSize.Max).padding(top = 8.dp), modifier = Modifier
.width(IntrinsicSize.Max)
.padding(top = 8.dp)
.semantics { role = Role.Button },
selected = false, selected = false,
onClick = { onClick = {
viewModel.launchChild(context, app) viewModel.launchChild(context, app)
@ -164,7 +170,7 @@ fun AppShortcutItem(
{ {
Icon( Icon(
if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline, if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline,
null, stringResource(if (isPinned) R.string.menu_favorites_unpin else R.string.menu_favorites_pin),
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.requiredSize(InputChipDefaults.IconSize) .requiredSize(InputChipDefaults.IconSize)

View File

@ -32,11 +32,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.SearchBarStyle import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBar import de.mm20.launcher2.ui.component.SearchBar
import de.mm20.launcher2.ui.component.SearchBarLevel import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.SearchVM
@ -115,7 +117,9 @@ fun LauncherSearchBar(
else IconButtonDefaults.iconButtonColors() else IconButtonDefaults.iconButtonColors()
) { ) {
Box { Box {
Icon(imageVector = Icons.Rounded.FilterAlt, contentDescription = null) Icon(imageVector = Icons.Rounded.FilterAlt, contentDescription = stringResource(
if (searchVM.showFilters.value) R.string.menu_hide_filters else R.string.menu_show_filters
))
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
!searchVM.filters.value.allCategoriesEnabled, !searchVM.filters.value.allCategoriesEnabled,
enter = scaleIn(tween(100)), enter = scaleIn(tween(100)),

View File

@ -55,7 +55,7 @@ fun RowScope.SearchBarMenu(
rightIcon, rightIcon,
atEnd = searchBarValue.isNotEmpty() atEnd = searchBarValue.isNotEmpty()
), ),
contentDescription = null, contentDescription = stringResource(if (searchBarValue.isNotBlank()) R.string.action_clear else R.string.action_more_actions),
tint = LocalContentColor.current tint = LocalContentColor.current
) )
} }

View File

@ -29,6 +29,10 @@ import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -136,10 +140,18 @@ fun WidgetColumn(
val editButton by viewModel.editButton.collectAsState() val editButton by viewModel.editButton.collectAsState()
if (editMode || editButton == true) { if (editMode || editButton == true) {
val title = stringResource(
if (editMode) R.string.widget_add_widget
else R.string.menu_edit_widgets
)
val icon = val icon =
AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add) AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add)
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
modifier = Modifier modifier = Modifier
.semantics {
role = Role.Button
contentDescription = title
}
.padding(16.dp) .padding(16.dp)
.align(Alignment.CenterHorizontally), .align(Alignment.CenterHorizontally),
icon = { icon = {
@ -151,12 +163,7 @@ fun WidgetColumn(
) )
}, },
text = { text = {
Text( Text(title)
stringResource(
if (editMode) R.string.widget_add_widget
else R.string.menu_edit_widgets
)
)
}, onClick = { }, onClick = {
if (!editMode) { if (!editMode) {
onEditModeChange(true) onEditModeChange(true)

View File

@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.OpenInNew
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ArrowDropDown import androidx.compose.material.icons.rounded.ArrowDropDown
import androidx.compose.material.icons.rounded.ChevronLeft import androidx.compose.material.icons.rounded.ChevronLeft
@ -85,7 +86,7 @@ fun CalendarWidget(
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 4.dp) modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 4.dp)
) { ) {
IconButton(onClick = { viewModel.previousDay() }) { IconButton(onClick = { viewModel.previousDay() }) {
Icon(imageVector = Icons.Rounded.ChevronLeft, contentDescription = null) Icon(imageVector = Icons.Rounded.ChevronLeft, contentDescription = stringResource(R.string.calendar_widget_previous_day))
} }
Box( Box(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -119,13 +120,13 @@ fun CalendarWidget(
} }
} }
IconButton(onClick = { viewModel.nextDay() }) { IconButton(onClick = { viewModel.nextDay() }) {
Icon(imageVector = Icons.Rounded.ChevronRight, contentDescription = null) Icon(imageVector = Icons.Rounded.ChevronRight, contentDescription = stringResource(R.string.calendar_widget_next_day))
} }
IconButton(onClick = { viewModel.createEvent(context) }) { IconButton(onClick = { viewModel.createEvent(context) }) {
Icon(imageVector = Icons.Rounded.Add, contentDescription = null) Icon(imageVector = Icons.Rounded.Add, contentDescription = stringResource(R.string.calendar_widget_create_event))
} }
IconButton(onClick = { viewModel.openCalendarApp(context) }) { IconButton(onClick = { viewModel.openCalendarApp(context) }) {
Icon(imageVector = Icons.Rounded.OpenInNew, contentDescription = null) Icon(imageVector = Icons.AutoMirrored.Rounded.OpenInNew, contentDescription = stringResource(R.string.calendar_widget_open_calendar))
} }
} }
val events by viewModel.calendarEvents val events by viewModel.calendarEvents

View File

@ -6,7 +6,6 @@ import android.media.session.PlaybackState.CustomAction
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -74,6 +73,8 @@ import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
@ -261,8 +262,12 @@ fun MusicWidget(widget: MusicWidget) {
}, },
onLongClick = { onLongClick = {
viewModel.openPlayerSelector(context) viewModel.openPlayerSelector(context)
} },
) )
.semantics {
contentDescription =
context.getString(R.string.music_widget_open_player)
}
.conditional( .conditional(
art == null, art == null,
Modifier.background( Modifier.background(
@ -333,7 +338,7 @@ fun MusicWidget(widget: MusicWidget) {
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.SkipPrevious, imageVector = Icons.Rounded.SkipPrevious,
null stringResource(R.string.music_widget_previous_track)
) )
} }
} }
@ -350,7 +355,11 @@ fun MusicWidget(widget: MusicWidget) {
playPauseIcon, playPauseIcon,
atEnd = playbackState == PlaybackState.Playing atEnd = playbackState == PlaybackState.Playing
), ),
contentDescription = null contentDescription = if (playbackState == PlaybackState.Playing) {
stringResource(R.string.music_widget_pause)
} else {
stringResource(R.string.music_widget_play)
}
) )
} }
if (supportedActions.skipToNext) { if (supportedActions.skipToNext) {
@ -359,7 +368,7 @@ fun MusicWidget(widget: MusicWidget) {
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.SkipNext, imageVector = Icons.Rounded.SkipNext,
null stringResource(R.string.music_widget_next_track)
) )
} }
} }

View File

@ -217,7 +217,10 @@ fun NotesWidget(
Icon( Icon(
if (widget.config.linkedFile == null) Icons.Rounded.Link if (widget.config.linkedFile == null) Icons.Rounded.Link
else Icons.Rounded.LinkOff, else Icons.Rounded.LinkOff,
null stringResource(
if (widget.config.linkedFile == null) R.string.note_widget_link_file
else R.string.note_widget_action_unlink_file
)
) )
} }
} }
@ -295,7 +298,7 @@ fun NotesWidget(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Box { Box {
IconButton(onClick = { showMenu = true }) { IconButton(onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, null) Icon(Icons.Rounded.MoreVert, stringResource(R.string.action_more_actions))
} }
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
DropdownMenuItem( DropdownMenuItem(

View File

@ -51,6 +51,8 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -400,7 +402,10 @@ fun WeatherTimeSelector(
verticalArrangement = Arrangement.SpaceEvenly verticalArrangement = Arrangement.SpaceEvenly
) { ) {
WeatherIcon( WeatherIcon(
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally)
.semantics {
contentDescription = fc.condition
},
icon = weatherIconById(fc.icon), icon = weatherIconById(fc.icon),
night = fc.night night = fc.night
) )

View File

@ -37,6 +37,9 @@
<string name="msg_item_hidden">%1$s has been hidden.</string> <string name="msg_item_hidden">%1$s has been hidden.</string>
<string name="action_undo">Undo</string> <string name="action_undo">Undo</string>
<string name="action_import">Import</string> <string name="action_import">Import</string>
<string name="action_done">Done</string>
<string name="action_more_actions">More actions</string>
<string name="action_clear">Clear</string>
<!-- Delete something (a file or a web search shortcut) --> <!-- Delete something (a file or a web search shortcut) -->
<string name="menu_delete">Delete</string> <string name="menu_delete">Delete</string>
<string name="menu_remove">Remove</string> <string name="menu_remove">Remove</string>
@ -839,6 +842,8 @@
<string name="clock_style_empty">No clock</string> <string name="clock_style_empty">No clock</string>
<string name="clock_style_custom">Custom widget</string> <string name="clock_style_custom">Custom widget</string>
<string name="clock_variant_outlined">Outlined</string> <string name="clock_variant_outlined">Outlined</string>
<string name="menu_show_filters">Show filters</string>
<string name="menu_hide_filters">Hide filters</string>
<string name="search_filter_tools">Tools</string> <string name="search_filter_tools">Tools</string>
<string name="search_filter_online">Online results</string> <string name="search_filter_online">Online results</string>
<string name="search_filter_apps">Apps</string> <string name="search_filter_apps">Apps</string>
@ -970,4 +975,13 @@
<item quantity="other">%1$s notifications</item> <item quantity="other">%1$s notifications</item>
</plurals> </plurals>
<string name="weather_widget_no_provider">No weather provider selected or the selected provider is not available</string> <string name="weather_widget_no_provider">No weather provider selected or the selected provider is not available</string>
<string name="music_widget_previous_track">Previous title</string>
<string name="music_widget_next_track">Next title</string>
<string name="music_widget_play">Play</string>
<string name="music_widget_pause">Pause</string>
<string name="music_widget_open_player">Open player</string>
<string name="calendar_widget_previous_day">Previous day</string>
<string name="calendar_widget_next_day">Next day</string>
<string name="calendar_widget_create_event">Create new event</string>
<string name="calendar_widget_open_calendar">Open calendar app</string>
</resources> </resources>