diff --git a/android/app/src/main/java/io/narl/protonstream/ui/LibraryScreen.kt b/android/app/src/main/java/io/narl/protonstream/ui/LibraryScreen.kt index 832dd72..85d156d 100644 --- a/android/app/src/main/java/io/narl/protonstream/ui/LibraryScreen.kt +++ b/android/app/src/main/java/io/narl/protonstream/ui/LibraryScreen.kt @@ -73,6 +73,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight @@ -681,13 +684,14 @@ private fun ContinueCard(entry: Resumable, onClick: () -> Unit, onLongClick: () Modifier .width(220.dp) .clip(MaterialTheme.shapes.medium) - .combinedClickable(onClick = onClick, onLongClick = onLongClick), + .combinedClickable(onClick = onClick, onLongClick = onLongClick, onLongClickLabel = "More for this episode"), ) { Box(Modifier.fillMaxWidth().aspectRatio(16f / 9f).clip(MaterialTheme.shapes.medium)) { + // Named by the text under it; read as well, the name came twice. RemoteArtwork( entry.episode.stillUrl ?: entry.title.backdropUrl, entry.title.displayName, - Modifier.fillMaxSize(), + Modifier.fillMaxSize().clearAndSetSemantics {}, fallback = entry.episode.thumbnailSource, ) entry.episode.progress?.let { progress -> @@ -738,7 +742,7 @@ private fun PosterTile( Column( modifier .clip(MaterialTheme.shapes.medium) - .combinedClickable(onClick = onClick, onLongClick = onLongClick), + .combinedClickable(onClick = onClick, onLongClick = onLongClick, onLongClickLabel = "More for this title"), ) { Box { RemoteArtwork( @@ -747,7 +751,8 @@ private fun PosterTile( (if (shared) Modifier.sharedArt(title.key) else Modifier) .fillMaxWidth() .aspectRatio(2f / 3f) - .clip(MaterialTheme.shapes.medium), + .clip(MaterialTheme.shapes.medium) + .clearAndSetSemantics {}, fallback = title.thumbnailSource, ) if (folded > 0) { @@ -760,7 +765,8 @@ private fun PosterTile( .padding(6.dp) .clip(MaterialTheme.shapes.small) .background(Color.Black.copy(alpha = 0.7f)) - .padding(horizontal = 6.dp, vertical = 2.dp), + .padding(horizontal = 6.dp, vertical = 2.dp) + .semantics { contentDescription = "and $folded more" }, ) } } diff --git a/ios/ProtonStream/App/ProtonStreamApp.swift b/ios/ProtonStream/App/ProtonStreamApp.swift index a370d13..6c8513a 100644 --- a/ios/ProtonStream/App/ProtonStreamApp.swift +++ b/ios/ProtonStream/App/ProtonStreamApp.swift @@ -107,6 +107,9 @@ struct RootView: View { .animation(.easeInOut(duration: 0.2), value: model.activity) } .background(scheme.background.ignoresSafeArea()) + // Covered by the player, but still there to VoiceOver, which + // would otherwise walk the library behind the picture. + .accessibilityHidden(host.isOpen && !host.minimized) } .overlay { // Over the shell rather than instead of it: the page behind stays diff --git a/ios/ProtonStream/UI/LibraryView.swift b/ios/ProtonStream/UI/LibraryView.swift index 4e7d7dd..1aeef21 100644 --- a/ios/ProtonStream/UI/LibraryView.swift +++ b/ios/ProtonStream/UI/LibraryView.swift @@ -541,7 +541,9 @@ private struct ContinueCard: View { var body: some View { VStack(alignment: .leading, spacing: 0) { + // Named by the text under it; read as well, the name came twice. RemoteArtwork(entry.episode.stillUrl ?? entry.title.backdropUrl, entry.title.displayName, fallback: entry.episode.thumbnailSource) + .accessibilityHidden(true) .aspectRatio(16 / 9, contentMode: .fit) .overlay(alignment: .bottom) { if let progress = entry.episode.progress { AccentProgress(progress: progress) } @@ -571,6 +573,7 @@ struct PosterTile: View { var body: some View { VStack(alignment: .leading, spacing: 0) { RemoteArtwork(title.posterUrl ?? title.backdropUrl, title.displayName, fallback: title.thumbnailSource) + .accessibilityHidden(true) .aspectRatio(2 / 3, contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: Corner.standard)) .overlay(alignment: .topTrailing) { @@ -581,6 +584,7 @@ struct PosterTile: View { .padding(.horizontal, 6).padding(.vertical, 2) .background(.black.opacity(0.7), in: RoundedRectangle(cornerRadius: Corner.standard)) .padding(6) + .accessibilityLabel("and \(folded) more") } } Text(title.displayName).textStyle(.bodyMedium, semibold: true).foregroundStyle(scheme.onSurface) diff --git a/ios/ProtonStream/UI/Theme/Accent.swift b/ios/ProtonStream/UI/Theme/Accent.swift index 204894b..e8ef53d 100644 --- a/ios/ProtonStream/UI/Theme/Accent.swift +++ b/ios/ProtonStream/UI/Theme/Accent.swift @@ -115,6 +115,14 @@ private struct TapAndHold: ViewModifier { .animation(.easeOut(duration: 0.1), value: pressed) .onTapGesture(perform: onTap) .onLongPressGesture(perform: { onHold?() }, onPressingChanged: { pressed = $0 }) + // One element to VoiceOver, a button, with the long press as an + // action: a gesture is not something it can perform. + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isButton) + .accessibilityAction(.default, onTap) + .accessibilityActions { + if let onHold { Button("More", action: onHold) } + } } }