mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 13:22:49 +02:00
fix(a11y): label interactive controls
This commit is contained in:
@@ -7434,6 +7434,18 @@ abstract class AppLocalizations {
|
||||
/// **'Up next'**
|
||||
String get nowPlayingUpNext;
|
||||
|
||||
/// Tooltip for the previous-track playback control
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Previous track'**
|
||||
String get nowPlayingPreviousTrack;
|
||||
|
||||
/// Tooltip for the next-track playback control
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Next track'**
|
||||
String get nowPlayingNextTrack;
|
||||
|
||||
/// Menu item and section title for track metadata details
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -4580,6 +4580,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Vorheriger Titel';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Nächster Titel';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
|
||||
@@ -4535,6 +4535,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Previous track';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Next track';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
|
||||
@@ -4529,6 +4529,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Pista anterior';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Pista siguiente';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
@@ -9255,6 +9261,12 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Pista anterior';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Pista siguiente';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
|
||||
@@ -4649,6 +4649,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'À suivre';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Piste précédente';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Piste suivante';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Détails';
|
||||
|
||||
|
||||
@@ -4533,6 +4533,12 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Lagu sebelumnya';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Lagu berikutnya';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
|
||||
@@ -4524,6 +4524,12 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => '前の曲';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => '次の曲';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
|
||||
@@ -4409,6 +4409,12 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => '다음 곡';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => '이전 곡';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => '다음 곡';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => '트랙 세부 정보';
|
||||
|
||||
|
||||
@@ -4529,6 +4529,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Faixa anterior';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Próxima faixa';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
@@ -9220,6 +9226,12 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Faixa anterior';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Próxima faixa';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
|
||||
@@ -4566,6 +4566,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Предыдущий трек';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Следующий трек';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
|
||||
@@ -4565,6 +4565,12 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Önceki parça';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Sonraki parça';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
|
||||
@@ -4583,6 +4583,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get nowPlayingUpNext => 'Up next';
|
||||
|
||||
@override
|
||||
String get nowPlayingPreviousTrack => 'Попередній трек';
|
||||
|
||||
@override
|
||||
String get nowPlayingNextTrack => 'Наступний трек';
|
||||
|
||||
@override
|
||||
String get nowPlayingDetails => 'Details';
|
||||
|
||||
|
||||
@@ -4395,6 +4395,8 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "Vorheriger Titel",
|
||||
"nowPlayingNextTrack": "Nächster Titel",
|
||||
"logAutoScrollOff": "Auto-Scrollen AUS",
|
||||
"trackConvertFailed": "Konvertierung fehlgeschlagen",
|
||||
"@trackConvertFailed": {
|
||||
|
||||
@@ -5861,6 +5861,14 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "Previous track",
|
||||
"@nowPlayingPreviousTrack": {
|
||||
"description": "Tooltip for the previous-track playback control"
|
||||
},
|
||||
"nowPlayingNextTrack": "Next track",
|
||||
"@nowPlayingNextTrack": {
|
||||
"description": "Tooltip for the next-track playback control"
|
||||
},
|
||||
"nowPlayingDetails": "Details",
|
||||
"@nowPlayingDetails": {
|
||||
"description": "Menu item and section title for track metadata details"
|
||||
|
||||
@@ -4286,5 +4286,7 @@
|
||||
"settingsGroupContent": "Contenido y metadatos",
|
||||
"settingsGroupDownloads": "Descargas y archivos",
|
||||
"settingsGroupSystem": "Sistema",
|
||||
"settingsGroupHelp": "Información y soporte"
|
||||
"settingsGroupHelp": "Información y soporte",
|
||||
"nowPlayingPreviousTrack": "Pista anterior",
|
||||
"nowPlayingNextTrack": "Pista siguiente"
|
||||
}
|
||||
|
||||
@@ -5502,6 +5502,8 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "Pista anterior",
|
||||
"nowPlayingNextTrack": "Pista siguiente",
|
||||
"nowPlayingDetails": "Details",
|
||||
"@nowPlayingDetails": {
|
||||
"description": "Menu item and section title for track metadata details"
|
||||
|
||||
@@ -4395,6 +4395,8 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "Piste précédente",
|
||||
"nowPlayingNextTrack": "Piste suivante",
|
||||
"logAutoScrollOff": "Défilement automatique désactivé",
|
||||
"trackConvertFailed": "Échec de la conversion",
|
||||
"@trackConvertFailed": {
|
||||
|
||||
@@ -4440,6 +4440,14 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "Lagu sebelumnya",
|
||||
"@nowPlayingPreviousTrack": {
|
||||
"description": "Tooltip for the previous-track playback control"
|
||||
},
|
||||
"nowPlayingNextTrack": "Lagu berikutnya",
|
||||
"@nowPlayingNextTrack": {
|
||||
"description": "Tooltip for the next-track playback control"
|
||||
},
|
||||
"logAutoScrollOff": "Auto-scroll OFF",
|
||||
"trackConvertFailed": "Conversion failed",
|
||||
"@trackConvertFailed": {
|
||||
|
||||
@@ -4395,6 +4395,8 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "前の曲",
|
||||
"nowPlayingNextTrack": "次の曲",
|
||||
"logAutoScrollOff": "Auto-scroll OFF",
|
||||
"trackConvertFailed": "変換に失敗しました",
|
||||
"@trackConvertFailed": {
|
||||
|
||||
@@ -5641,6 +5641,8 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "이전 곡",
|
||||
"nowPlayingNextTrack": "다음 곡",
|
||||
"nowPlayingDetails": "트랙 세부 정보",
|
||||
"@nowPlayingDetails": {
|
||||
"description": "Menu item and section title for track metadata details"
|
||||
|
||||
@@ -4286,5 +4286,7 @@
|
||||
"settingsGroupContent": "Conteúdo e metadados",
|
||||
"settingsGroupDownloads": "Downloads e arquivos",
|
||||
"settingsGroupSystem": "Sistema",
|
||||
"settingsGroupHelp": "Sobre e suporte"
|
||||
"settingsGroupHelp": "Sobre e suporte",
|
||||
"nowPlayingPreviousTrack": "Faixa anterior",
|
||||
"nowPlayingNextTrack": "Próxima faixa"
|
||||
}
|
||||
|
||||
@@ -4395,6 +4395,8 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "Faixa anterior",
|
||||
"nowPlayingNextTrack": "Próxima faixa",
|
||||
"logAutoScrollOff": "Auto-scroll OFF",
|
||||
"trackConvertFailed": "Conversion failed",
|
||||
"@trackConvertFailed": {
|
||||
|
||||
@@ -4395,6 +4395,8 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "Предыдущий трек",
|
||||
"nowPlayingNextTrack": "Следующий трек",
|
||||
"logAutoScrollOff": "Auto-scroll OFF",
|
||||
"trackConvertFailed": "Ошибка конвертации",
|
||||
"@trackConvertFailed": {
|
||||
|
||||
@@ -4395,6 +4395,8 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "Önceki parça",
|
||||
"nowPlayingNextTrack": "Sonraki parça",
|
||||
"logAutoScrollOff": "Auto-scroll OFF",
|
||||
"trackConvertFailed": "Dönüştürme başarısız oldu",
|
||||
"@trackConvertFailed": {
|
||||
|
||||
@@ -4395,6 +4395,8 @@
|
||||
"@nowPlayingUpNext": {
|
||||
"description": "Title for the playback queue sheet"
|
||||
},
|
||||
"nowPlayingPreviousTrack": "Попередній трек",
|
||||
"nowPlayingNextTrack": "Наступний трек",
|
||||
"logAutoScrollOff": "Auto-scroll OFF",
|
||||
"trackConvertFailed": "Конвертація не вдалася",
|
||||
"@trackConvertFailed": {
|
||||
|
||||
@@ -639,6 +639,7 @@ class _LibraryTracksFolderScreenState
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.collectionPlaylistChangeCover,
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -337,6 +337,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
tooltip: context.l10n.nowPlayingMinimize,
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
@@ -1217,6 +1218,7 @@ class _PlaybackControls extends ConsumerWidget {
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
iconSize: 44,
|
||||
tooltip: context.l10n.nowPlayingPreviousTrack,
|
||||
icon: const Icon(Icons.skip_previous),
|
||||
onPressed: controller.previous,
|
||||
),
|
||||
@@ -1230,6 +1232,9 @@ class _PlaybackControls extends ConsumerWidget {
|
||||
iconSize: 44,
|
||||
padding: const EdgeInsets.all(12),
|
||||
color: colorScheme.onPrimary,
|
||||
tooltip: isPlaying
|
||||
? context.l10n.actionPause
|
||||
: context.l10n.tooltipPlay,
|
||||
icon: isLoading
|
||||
? const SizedBox.square(
|
||||
dimension: 32,
|
||||
@@ -1244,6 +1249,7 @@ class _PlaybackControls extends ConsumerWidget {
|
||||
const SizedBox(width: 20),
|
||||
IconButton(
|
||||
iconSize: 44,
|
||||
tooltip: context.l10n.nowPlayingNextTrack,
|
||||
icon: const Icon(Icons.skip_next),
|
||||
onPressed: controller.next,
|
||||
),
|
||||
|
||||
@@ -627,16 +627,26 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
child: Row(
|
||||
children: [
|
||||
if (_isPlaylistSelectionMode)
|
||||
GestureDetector(
|
||||
onTap: () => _togglePlaylistSelection(playlist.id),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: AnimatedSelectionCheckbox(
|
||||
visible: true,
|
||||
selected: isSelected,
|
||||
colorScheme: colorScheme,
|
||||
size: 24,
|
||||
Semantics(
|
||||
button: true,
|
||||
checked: isSelected,
|
||||
label: playlist.name,
|
||||
child: GestureDetector(
|
||||
onTap: () => _togglePlaylistSelection(playlist.id),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints.tightFor(
|
||||
width: context.tokens.minTouchTarget,
|
||||
height: context.tokens.minTouchTarget,
|
||||
),
|
||||
child: Center(
|
||||
child: AnimatedSelectionCheckbox(
|
||||
visible: true,
|
||||
selected: isSelected,
|
||||
colorScheme: colorScheme,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1063,21 +1063,17 @@ extension _QueueTabItemWidgets on _QueueTabState {
|
||||
valueListenable: fileExistsListenable,
|
||||
builder: (context, fileExists, child) {
|
||||
if (fileExists) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: context.l10n.a11yPlayTrackByArtist(
|
||||
return IconButton.filled(
|
||||
tooltip: context.l10n.a11yPlayTrackByArtist(
|
||||
item.trackName,
|
||||
item.artistName,
|
||||
),
|
||||
child: IconButton.filled(
|
||||
onPressed: () => _playLibraryItem(item, libraryItems),
|
||||
visualDensity: VisualDensity.compact,
|
||||
iconSize: 18,
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: const Size.square(36),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: () => _playLibraryItem(item, libraryItems),
|
||||
iconSize: 18,
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: Size.square(context.tokens.minTouchTarget),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -728,7 +728,7 @@ class _ExtensionItem extends StatelessWidget {
|
||||
onPressed: onUpdate,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
minimumSize: const Size(0, 40),
|
||||
minimumSize: Size(0, context.tokens.minTouchTarget),
|
||||
),
|
||||
child: Text(context.l10n.storeUpdate),
|
||||
)
|
||||
@@ -737,7 +737,7 @@ class _ExtensionItem extends StatelessWidget {
|
||||
onPressed: null,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
minimumSize: const Size(0, 40),
|
||||
minimumSize: Size(0, context.tokens.minTouchTarget),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -755,7 +755,7 @@ class _ExtensionItem extends StatelessWidget {
|
||||
onPressed: onInstall,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
minimumSize: const Size(0, 40),
|
||||
minimumSize: Size(0, context.tokens.minTouchTarget),
|
||||
),
|
||||
child: Text(context.l10n.storeInstall),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/music_player_provider.dart';
|
||||
import 'package:spotiflac_android/screens/now_playing_screen.dart';
|
||||
import 'package:spotiflac_android/widgets/player_artwork.dart';
|
||||
@@ -113,6 +114,9 @@ class _MiniPlayerState extends ConsumerState<MiniPlayer> {
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: isPlaying
|
||||
? context.l10n.actionPause
|
||||
: context.l10n.tooltipPlay,
|
||||
icon: isLoading
|
||||
? const SizedBox.square(
|
||||
dimension: 20,
|
||||
@@ -126,6 +130,7 @@ class _MiniPlayerState extends ConsumerState<MiniPlayer> {
|
||||
: () => controller.togglePlayPause(isPlaying),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.nowPlayingNextTrack,
|
||||
icon: const Icon(Icons.skip_next),
|
||||
onPressed: controller.next,
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/theme/app_tokens.dart';
|
||||
|
||||
/// Icon+label pill used in selection-mode bottom bars. Disabled state dims
|
||||
/// both the fill and the content to 50% alpha.
|
||||
@@ -19,42 +20,55 @@ class SelectionActionButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDisabled = onPressed == null;
|
||||
return Material(
|
||||
color: isDisabled
|
||||
? colorScheme.surfaceContainerHighest.withValues(alpha: 0.5)
|
||||
: colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: !isDisabled,
|
||||
label: label,
|
||||
excludeSemantics: true,
|
||||
child: Material(
|
||||
color: isDisabled
|
||||
? colorScheme.surfaceContainerHighest.withValues(alpha: 0.5)
|
||||
: colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: isDisabled
|
||||
? colorScheme.onSurfaceVariant.withValues(alpha: 0.5)
|
||||
: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: context.tokens.minTouchTarget,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: isDisabled
|
||||
? colorScheme.onSurfaceVariant.withValues(alpha: 0.5)
|
||||
: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDisabled
|
||||
? colorScheme.onSurfaceVariant.withValues(
|
||||
alpha: 0.5,
|
||||
)
|
||||
: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.3.14"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7"
|
||||
|
||||
@@ -72,6 +72,7 @@ dependencies:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
analyzer: ^12.1.0
|
||||
flutter_lints: ^6.0.0
|
||||
build_runner: ^2.15.1
|
||||
riverpod_lint: ^3.1.4
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:analyzer/dart/analysis/features.dart';
|
||||
import 'package:analyzer/dart/analysis/utilities.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/ast/visitor.dart';
|
||||
import 'package:analyzer/source/line_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||
@@ -10,6 +15,7 @@ import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
|
||||
import 'package:spotiflac_android/widgets/app_search_field.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
import 'package:spotiflac_android/widgets/collection_scaffold.dart';
|
||||
import 'package:spotiflac_android/widgets/selection_action_button.dart';
|
||||
import 'package:spotiflac_android/widgets/selection_bottom_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/track_card.dart';
|
||||
@@ -27,6 +33,34 @@ List<File> _libSources() {
|
||||
|
||||
String _basename(File file) => file.uri.pathSegments.last;
|
||||
|
||||
class _IconButtonTooltipVisitor extends RecursiveAstVisitor<void> {
|
||||
_IconButtonTooltipVisitor({
|
||||
required this.file,
|
||||
required this.lineInfo,
|
||||
required this.offenders,
|
||||
});
|
||||
|
||||
final File file;
|
||||
final LineInfo lineInfo;
|
||||
final List<String> offenders;
|
||||
|
||||
@override
|
||||
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
||||
if (node.constructorName.type.name.lexeme == 'IconButton') {
|
||||
final hasTooltip = node.argumentList.arguments.any(
|
||||
(argument) =>
|
||||
argument is NamedExpression &&
|
||||
argument.name.label.name == 'tooltip',
|
||||
);
|
||||
if (!hasTooltip) {
|
||||
final line = lineInfo.getLocation(node.offset).lineNumber;
|
||||
offenders.add('${file.path}:$line');
|
||||
}
|
||||
}
|
||||
super.visitInstanceCreationExpression(node);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _hostSliver(Widget sliver) {
|
||||
return MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
@@ -84,6 +118,72 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('accessibility contracts', () {
|
||||
test('every IconButton has a tooltip-backed accessible name', () {
|
||||
final offenders = <String>[];
|
||||
for (final file in _libSources()) {
|
||||
final result = parseFile(
|
||||
path: file.absolute.path,
|
||||
featureSet: FeatureSet.latestLanguageVersion(),
|
||||
);
|
||||
result.unit.accept(
|
||||
_IconButtonTooltipVisitor(
|
||||
file: file,
|
||||
lineInfo: result.lineInfo,
|
||||
offenders: offenders,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
expect(
|
||||
offenders,
|
||||
isEmpty,
|
||||
reason:
|
||||
'Icon-only controls must expose a tooltip so TalkBack, VoiceOver, '
|
||||
'keyboard users, and pointer users receive the same action name.',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('selection actions expose their label and disabled state', (
|
||||
tester,
|
||||
) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
try {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: SelectionActionButton(
|
||||
icon: Icons.delete,
|
||||
label: 'Delete selected tracks',
|
||||
onPressed: null,
|
||||
colorScheme: AppTheme.light().colorScheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
tester.getSemantics(find.byType(SelectionActionButton)),
|
||||
matchesSemantics(
|
||||
label: 'Delete selected tracks',
|
||||
isButton: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: false,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
tester.getSize(find.byType(SelectionActionButton)).height,
|
||||
greaterThanOrEqualTo(AppTokens.standard.minTouchTarget),
|
||||
);
|
||||
} finally {
|
||||
semantics.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('AppSearchField', () {
|
||||
testWidgets('uses the shared filled search style and clears input', (
|
||||
tester,
|
||||
|
||||
Reference in New Issue
Block a user