From 7ec17b7de81c51b9396585927c5e8419d444ade7 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Tue, 23 Jul 2024 22:12:54 +0200 Subject: [PATCH] (a11y) add more screen reader labels Close #975 --- .../mm20/launcher2/ui/component/SearchBar.kt | 9 ++++- .../de/mm20/launcher2/ui/component/Toolbar.kt | 3 +- .../launcher2/ui/launcher/PagerScaffold.kt | 2 +- .../launcher2/ui/launcher/PullDownScaffold.kt | 2 +- .../ui/launcher/search/apps/AppItem.kt | 2 +- .../launcher/search/common/grid/GridItem.kt | 38 +++++++++++++------ .../launcher/search/shortcut/ShortcutItem.kt | 10 ++++- .../launcher/searchbar/LauncherSearchBar.kt | 6 ++- .../ui/launcher/searchbar/SearchBarMenu.kt | 2 +- .../ui/launcher/widgets/WidgetColumn.kt | 19 +++++++--- .../widgets/calendar/CalendarWidget.kt | 9 +++-- .../ui/launcher/widgets/music/MusicWidget.kt | 19 +++++++--- .../ui/launcher/widgets/notes/NotesWidget.kt | 7 +++- .../launcher/widgets/weather/WeatherWidget.kt | 7 +++- core/i18n/src/main/res/values/strings.xml | 14 +++++++ 15 files changed, 110 insertions(+), 39 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt index 8056da3f..b2f83efc 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt @@ -36,7 +36,10 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext 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.unit.dp import de.mm20.launcher2.preferences.SearchBarStyle @@ -62,6 +65,7 @@ fun SearchBar( ) { val transition = updateTransition(level, label = "Searchbar") + val context = LocalContext.current val elevation by transition.animateDp( label = "elevation", @@ -169,7 +173,10 @@ fun SearchBar( if (it.hasFocus) onFocus() } .focusRequester(focusRequester) - .fillMaxWidth(), + .fillMaxWidth() + .semantics { + contentDescription = context.getString(R.string.search_bar_placeholder) + }, textStyle = MaterialTheme.typography.titleMedium.copy( color = contentColor ), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt index 1cf960e7..b063c2a5 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.integerResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.ktx.toDp @@ -62,7 +63,7 @@ fun Icons(actions: List, slots: Int) { var showMenu by remember { mutableStateOf(false) } Box { IconButton(onClick = { showMenu = true }) { - Icon(Icons.Rounded.MoreVert, contentDescription = "") + Icon(Icons.Rounded.MoreVert, contentDescription = stringResource(R.string.action_more_actions)) } Box( modifier = Modifier diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt index 78da2f4a..831a93a8 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt @@ -591,7 +591,7 @@ fun PagerScaffold( }, navigationIcon = { IconButton(onClick = { viewModel.setWidgetEditMode(false) }) { - Icon(imageVector = Icons.Rounded.Done, contentDescription = null) + Icon(imageVector = Icons.Rounded.Done, contentDescription = stringResource(R.string.action_done)) } }, ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt index 5d33b9bb..9f043e4b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt @@ -567,7 +567,7 @@ fun PullDownScaffold( }, navigationIcon = { IconButton(onClick = { viewModel.setWidgetEditMode(false) }) { - Icon(imageVector = Icons.Rounded.Done, contentDescription = null) + Icon(imageVector = Icons.Rounded.Done, contentDescription = stringResource(R.string.action_done)) } } ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt index 5ce06f11..2a9390be 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt @@ -341,7 +341,7 @@ fun AppItem( ) { Icon( if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline, - null + stringResource(if (isPinned) R.string.menu_favorites_unpin else R.string.menu_favorites_pin), ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt index c5a90dea..53f1a521 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt @@ -4,9 +4,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable 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.LocalDensity 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.TextOverflow import androidx.compose.ui.unit.dp @@ -159,7 +159,10 @@ fun GridItem( MaterialTheme.colorScheme.surfaceVariant, iconShape ) - } else Modifier, + } else Modifier then if (showLabels) Modifier else Modifier + .semantics { + contentDescription = item.label + }, ) { ShapedLauncherIcon( modifier = Modifier @@ -209,16 +212,20 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit } LaunchedEffect(show.targetState) { if (!show.targetState) { - animationProgress.animateTo(0f, spring( - Spring.DampingRatioNoBouncy, - Spring.StiffnessMediumLow, - )) + animationProgress.animateTo( + 0f, spring( + Spring.DampingRatioNoBouncy, + Spring.StiffnessMediumLow, + ) + ) onDismissRequest() } else { - animationProgress.animateTo(1f, spring( - Spring.DampingRatioLowBouncy, - Spring.StiffnessMediumLow, - )) + animationProgress.animateTo( + 1f, spring( + Spring.DampingRatioLowBouncy, + Spring.StiffnessMediumLow, + ) + ) } } BackHandler { @@ -228,7 +235,14 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit Overlay { Box( 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() .systemBarsPadding() .imePadding() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt index cbc81028..01df0227 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt @@ -50,6 +50,9 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.platform.LocalContext 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.unit.dp import androidx.compose.ui.unit.roundToIntRect @@ -140,7 +143,10 @@ fun AppShortcutItem( }.collectAsState(null) 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, onClick = { viewModel.launchChild(context, app) @@ -164,7 +170,7 @@ fun AppShortcutItem( { Icon( 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 .clip(CircleShape) .requiredSize(InputChipDefaults.IconSize) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt index 0737791a..0f26d2a1 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt @@ -32,11 +32,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.preferences.SearchBarStyle 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.SearchBarLevel import de.mm20.launcher2.ui.launcher.search.SearchVM @@ -115,7 +117,9 @@ fun LauncherSearchBar( else IconButtonDefaults.iconButtonColors() ) { 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( !searchVM.filters.value.allCategoriesEnabled, enter = scaleIn(tween(100)), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarMenu.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarMenu.kt index 007674b8..e197cbd3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarMenu.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarMenu.kt @@ -55,7 +55,7 @@ fun RowScope.SearchBarMenu( rightIcon, atEnd = searchBarValue.isNotEmpty() ), - contentDescription = null, + contentDescription = stringResource(if (searchBarValue.isNotBlank()) R.string.action_clear else R.string.action_more_actions), tint = LocalContentColor.current ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt index 635fdf27..ebc921df 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt @@ -29,6 +29,10 @@ import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner 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.dp import androidx.lifecycle.Lifecycle @@ -136,10 +140,18 @@ fun WidgetColumn( val editButton by viewModel.editButton.collectAsState() if (editMode || editButton == true) { + val title = stringResource( + if (editMode) R.string.widget_add_widget + else R.string.menu_edit_widgets + ) val icon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add) ExtendedFloatingActionButton( modifier = Modifier + .semantics { + role = Role.Button + contentDescription = title + } .padding(16.dp) .align(Alignment.CenterHorizontally), icon = { @@ -151,12 +163,7 @@ fun WidgetColumn( ) }, text = { - Text( - stringResource( - if (editMode) R.string.widget_add_widget - else R.string.menu_edit_widgets - ) - ) + Text(title) }, onClick = { if (!editMode) { onEditModeChange(true) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt index dfb67592..819b6b09 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding 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.ArrowDropDown 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) ) { 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( modifier = Modifier.weight(1f), @@ -119,13 +120,13 @@ fun CalendarWidget( } } 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) }) { - 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) }) { - 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 diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt index 810e672c..5e3424ed 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt @@ -6,7 +6,6 @@ import android.media.session.PlaybackState.CustomAction import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn 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.platform.LocalContext 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.unit.dp import androidx.core.content.res.ResourcesCompat @@ -261,8 +262,12 @@ fun MusicWidget(widget: MusicWidget) { }, onLongClick = { viewModel.openPlayerSelector(context) - } + }, ) + .semantics { + contentDescription = + context.getString(R.string.music_widget_open_player) + } .conditional( art == null, Modifier.background( @@ -333,7 +338,7 @@ fun MusicWidget(widget: MusicWidget) { }) { Icon( imageVector = Icons.Rounded.SkipPrevious, - null + stringResource(R.string.music_widget_previous_track) ) } } @@ -350,7 +355,11 @@ fun MusicWidget(widget: MusicWidget) { playPauseIcon, 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) { @@ -359,7 +368,7 @@ fun MusicWidget(widget: MusicWidget) { }) { Icon( imageVector = Icons.Rounded.SkipNext, - null + stringResource(R.string.music_widget_next_track) ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt index 1060933a..03dab61b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt @@ -217,7 +217,10 @@ fun NotesWidget( Icon( if (widget.config.linkedFile == null) Icons.Rounded.Link 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)) Box { 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 }) { DropdownMenuItem( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt index ddd9ebc1..6af6665f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt @@ -51,6 +51,8 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext 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.sp import androidx.lifecycle.Lifecycle @@ -400,7 +402,10 @@ fun WeatherTimeSelector( verticalArrangement = Arrangement.SpaceEvenly ) { WeatherIcon( - modifier = Modifier.align(Alignment.CenterHorizontally), + modifier = Modifier.align(Alignment.CenterHorizontally) + .semantics { + contentDescription = fc.condition + }, icon = weatherIconById(fc.icon), night = fc.night ) diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index d7abca98..3d5590cc 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -37,6 +37,9 @@ %1$s has been hidden. Undo Import + Done + More actions + Clear Delete Remove @@ -839,6 +842,8 @@ No clock Custom widget Outlined + Show filters + Hide filters Tools Online results Apps @@ -970,4 +975,13 @@ %1$s notifications No weather provider selected or the selected provider is not available + Previous title + Next title + Play + Pause + Open player + Previous day + Next day + Create new event + Open calendar app \ No newline at end of file