further improved accessibility across the application and add tooltips to action buttons
This commit is contained in:
@@ -18,6 +18,14 @@ class LoginErrorBanner extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final showDetails = details != null
|
||||
? () => InfoDialog.show(
|
||||
context,
|
||||
details!,
|
||||
copyable: true,
|
||||
title: 'Fehlerdetails',
|
||||
)
|
||||
: null;
|
||||
return AnimatedSize(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOut,
|
||||
@@ -25,18 +33,20 @@ class LoginErrorBanner extends StatelessWidget {
|
||||
? const SizedBox(height: 0, width: double.infinity)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(top: 14),
|
||||
child: Semantics(
|
||||
button: showDetails != null,
|
||||
label: showDetails != null
|
||||
? '${message!}, Fehlerdetails anzeigen'
|
||||
: message,
|
||||
onTap: showDetails,
|
||||
child: ExcludeSemantics(
|
||||
child: Material(
|
||||
color: theme.colorScheme.errorContainer.withValues(alpha: 0.6),
|
||||
color: theme.colorScheme.errorContainer.withValues(
|
||||
alpha: 0.6,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: details != null
|
||||
? () => InfoDialog.show(
|
||||
context,
|
||||
details!,
|
||||
copyable: true,
|
||||
title: 'Fehlerdetails',
|
||||
)
|
||||
: null,
|
||||
onTap: showDetails,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -76,6 +86,8 @@ class LoginErrorBanner extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ class FilesSortActions extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PopupMenuButton<bool>(
|
||||
tooltip:
|
||||
'Sortierrichtung: ${ascending ? 'aufsteigend' : 'absteigend'}',
|
||||
icon: Icon(
|
||||
ascending ? Icons.text_rotate_up : Icons.text_rotation_down,
|
||||
),
|
||||
@@ -47,6 +49,8 @@ class FilesSortActions extends StatelessWidget {
|
||||
onSelected: onDirectionChanged,
|
||||
),
|
||||
PopupMenuButton<SortOption>(
|
||||
tooltip:
|
||||
'Sortieren nach: ${SortOptions.getOption(currentSort).displayName}',
|
||||
icon: const Icon(Icons.sort),
|
||||
itemBuilder: (context) => SortOptions.options.keys
|
||||
.map(
|
||||
|
||||
@@ -35,6 +35,8 @@ class GradeAveragesView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
PopupMenuButton<bool>(
|
||||
tooltip:
|
||||
'Notensystem: ${bloc.isMiddleSchool() ? 'Realschule' : 'Oberstufe'}',
|
||||
initialValue: bloc.isMiddleSchool(),
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
itemBuilder: (context) => [true, false]
|
||||
|
||||
@@ -43,6 +43,8 @@ class HolidaysView extends StatelessWidget {
|
||||
onPressed: showDisclaimer,
|
||||
),
|
||||
PopupMenuButton<bool>(
|
||||
tooltip:
|
||||
'Vergangene Ferien ${bloc.showPastHolidays() ? 'ausblenden' : 'anzeigen'}',
|
||||
initialValue: bloc.showPastHolidays(),
|
||||
icon: const Icon(Icons.history),
|
||||
itemBuilder: (context) => [true, false]
|
||||
@@ -82,9 +84,7 @@ class HolidaysView extends StatelessWidget {
|
||||
text: 'Keine Schulferien verfügbar',
|
||||
);
|
||||
}
|
||||
return ListViewUtil.fromList<McHoliday>(
|
||||
holidays,
|
||||
(holiday) {
|
||||
return ListViewUtil.fromList<McHoliday>(holidays, (holiday) {
|
||||
String holidayYear() {
|
||||
final startYear = holiday.startDate.year;
|
||||
final endYear = holiday.endDate.year;
|
||||
@@ -94,9 +94,7 @@ class HolidaysView extends StatelessWidget {
|
||||
|
||||
return ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.calendar_month)),
|
||||
title: Text(
|
||||
'${holiday.longName} ${holidayYear()}',
|
||||
),
|
||||
title: Text('${holiday.longName} ${holidayYear()}'),
|
||||
subtitle: Text(
|
||||
'${holiday.startDate.formatDate()} - ${holiday.endDate.formatDate()}',
|
||||
),
|
||||
@@ -148,8 +146,7 @@ class HolidaysView extends StatelessWidget {
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -32,8 +32,9 @@ class MarianumDatesView extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
BlocModule<MarianumDatesBloc, LoadableState<MarianumDatesState>>(
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
) => BlocModule<MarianumDatesBloc, LoadableState<MarianumDatesState>>(
|
||||
create: (context) => MarianumDatesBloc(),
|
||||
autoRebuild: true,
|
||||
child: (context, bloc, state) => Scaffold(
|
||||
@@ -41,6 +42,8 @@ class MarianumDatesView extends StatelessWidget {
|
||||
title: const Text('Marianum Termine'),
|
||||
actions: [
|
||||
PopupMenuButton<bool>(
|
||||
tooltip:
|
||||
'Vergangene Termine ${bloc.showPastEvents() ? 'ausblenden' : 'anzeigen'}',
|
||||
initialValue: bloc.showPastEvents(),
|
||||
icon: const Icon(Icons.history),
|
||||
itemBuilder: (context) => [true, false]
|
||||
@@ -57,9 +60,7 @@ class MarianumDatesView extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Text(
|
||||
e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen',
|
||||
),
|
||||
Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -91,7 +91,14 @@ class _ChatTileState extends State<ChatTile> {
|
||||
: null,
|
||||
leading: Stack(
|
||||
children: [
|
||||
circleAvatar,
|
||||
// Der Name steht bereits im Titel der Zeile – das Avatarbild selbst
|
||||
// muss der Screenreader nicht ansagen. Nur bei Gruppen ist der
|
||||
// Hinweis „Gruppe" nützlich (sonst nicht vom Einzelchat zu
|
||||
// unterscheiden).
|
||||
Semantics(
|
||||
label: isGroup ? A11yLabels.group : null,
|
||||
child: ExcludeSemantics(child: circleAvatar),
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.data.isFavorite,
|
||||
child: Positioned(
|
||||
|
||||
@@ -106,6 +106,10 @@ class _TimetableState extends State<Timetable> {
|
||||
.canViewForeignTimetables;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Der Kalender scrollt nicht (nur ziehen/reloaden), aber seine internen
|
||||
// Scrollables feuern ScrollNotifications, die sonst den Material-3
|
||||
// "scrolled under"-Farbwechsel der AppBar dauerhaft auslösen.
|
||||
notificationPredicate: (_) => false,
|
||||
title: const Text('Stunden & Vertretungsplan'),
|
||||
actions: [
|
||||
IconButton(
|
||||
@@ -114,6 +118,7 @@ class _TimetableState extends State<Timetable> {
|
||||
onPressed: atToday ? null : _jumpToToday,
|
||||
),
|
||||
PopupMenuButton<_CalendarAction>(
|
||||
tooltip: 'Kalendereinträge',
|
||||
icon: const Icon(Icons.edit_calendar_outlined),
|
||||
onSelected: _onAction,
|
||||
itemBuilder: (_) => const [
|
||||
@@ -176,6 +181,9 @@ class _TimetableState extends State<Timetable> {
|
||||
.canViewForeignTimetables;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Siehe _buildOwnPlan: den scroll-under-Farbwechsel unterdrücken, weil
|
||||
// der Kalender nicht scrollt, aber ScrollNotifications feuert.
|
||||
notificationPredicate: (_) => false,
|
||||
title: const Text('Stunden & Vertretungsplan'),
|
||||
actions: [
|
||||
IconButton(
|
||||
@@ -285,7 +293,9 @@ class _ViewingBanner extends StatelessWidget {
|
||||
),
|
||||
compactButton(
|
||||
icon: isFavorite ? Icons.star : Icons.star_border,
|
||||
tooltip: isFavorite ? 'Favorit entfernen' : 'Als Favorit markieren',
|
||||
tooltip: isFavorite
|
||||
? 'Favorit entfernen'
|
||||
: 'Als Favorit markieren',
|
||||
onPressed: () => _toggleFavorite(context),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
|
||||
@@ -34,7 +34,15 @@ class _WeekGrid extends StatelessWidget {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_PeriodRuler(schedule: schedule, layout: layout, width: rulerWidth),
|
||||
// Reine Orientierungshilfe – der Screenreader soll die Zeitleiste nicht
|
||||
// vorlesen; jede Termin-Kachel trägt ihre Uhrzeit selbst im Label.
|
||||
ExcludeSemantics(
|
||||
child: _PeriodRuler(
|
||||
schedule: schedule,
|
||||
layout: layout,
|
||||
width: rulerWidth,
|
||||
),
|
||||
),
|
||||
for (var d = 0; d < 5; d++)
|
||||
Expanded(
|
||||
child: _DayColumn(
|
||||
@@ -279,9 +287,25 @@ class _DayColumn extends StatelessWidget {
|
||||
final isTablet = MediaQuery.of(context).size.shortestSide >= 600;
|
||||
final laidOut = assignLanes(dayAppointments, maxLanes: isTablet ? 3 : 2);
|
||||
|
||||
return GestureDetector(
|
||||
final dayName = DateFormat(
|
||||
'EEEE',
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(date);
|
||||
final headerLabel = isToday
|
||||
? '$dayName, ${date.formatDateShort()}, ${A11yLabels.today}'
|
||||
: '$dayName, ${date.formatDateShort()}';
|
||||
|
||||
// container + OrdinalSortKey ⇒ der Screenreader liest Tag für Tag (Mo→Fr),
|
||||
// jeden Tag vollständig, statt zeilenweise quer über alle Spalten. Der
|
||||
// Header steckt als erstes Element in der Spalte, damit „Montag: …" vor den
|
||||
// Stunden angesagt wird.
|
||||
return Semantics(
|
||||
container: true,
|
||||
sortKey: OrdinalSortKey(date.weekday.toDouble()),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onLongPressStart: (details) => _handleLongPress(details, dayAppointments),
|
||||
onLongPressStart: (details) =>
|
||||
_handleLongPress(details, dayAppointments),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? theme.colorScheme.primary.withAlpha(14) : null,
|
||||
@@ -298,6 +322,16 @@ class _DayColumn extends StatelessWidget {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Semantics(
|
||||
header: true,
|
||||
label: headerLabel,
|
||||
child: const SizedBox(height: 1),
|
||||
),
|
||||
),
|
||||
for (final period in schedule.periods)
|
||||
Positioned(
|
||||
top: layout.topOf(period),
|
||||
@@ -352,8 +386,11 @@ class _DayColumn extends StatelessWidget {
|
||||
),
|
||||
LaidOutOverflow(:final appointments) => Semantics(
|
||||
button: true,
|
||||
label: A11yLabels.moreAppointments(appointments.length),
|
||||
onTap: () => _showOverflowSheet(context, appointments),
|
||||
label: A11yLabels.moreAppointments(
|
||||
appointments.length,
|
||||
),
|
||||
onTap: () =>
|
||||
_showOverflowSheet(context, appointments),
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -379,6 +416,7 @@ class _DayColumn extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
|
||||
@@ -103,11 +104,7 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
final newTotalWeeks =
|
||||
newLastMonday.difference(newFirstMonday).inDays ~/ 7 + 1;
|
||||
final visibleWeekStart = _firstMonday.addDays(_currentWeekIndex * 7);
|
||||
final newIndex = visibleWeekStart
|
||||
.difference(newFirstMonday)
|
||||
.inDays
|
||||
~/
|
||||
7;
|
||||
final newIndex = visibleWeekStart.difference(newFirstMonday).inDays ~/ 7;
|
||||
final clampedIndex = newIndex.clamp(0, newTotalWeeks - 1);
|
||||
final oldController = _pageController;
|
||||
_pageController = PageController(initialPage: clampedIndex);
|
||||
@@ -167,6 +164,10 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
// Die visuelle „MO/14"-Leiste ist für den Screenreader unbrauchbar;
|
||||
// die Tag-Info wird stattdessen als Header in jede Tagesspalte
|
||||
// eingehängt (siehe _DayColumn).
|
||||
child: ExcludeSemantics(
|
||||
child: _DayHeaderStrip(
|
||||
key: ValueKey(visibleWeekStart),
|
||||
weekStart: visibleWeekStart,
|
||||
@@ -175,6 +176,7 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 320),
|
||||
|
||||
@@ -16,15 +16,17 @@ abstract final class A11yLabels {
|
||||
static const draft = 'Entwurf';
|
||||
static const yourReaction = 'deine Reaktion';
|
||||
|
||||
// Bilder
|
||||
// Bilder / Kontakte
|
||||
static const profilePicture = 'Profilbild';
|
||||
static const groupPicture = 'Gruppenbild';
|
||||
static const group = 'Gruppe';
|
||||
static const roomPlan = 'Raumplan der Schule als Grafik';
|
||||
|
||||
// Stundenplan
|
||||
static const cancelled = 'Ausfall';
|
||||
static const breakTime = 'Pause';
|
||||
static const allDay = 'Ganztägig';
|
||||
static const today = 'heute';
|
||||
|
||||
/// Beschreibt eine Stundenplan-Kachel für den Screenreader, z.B.
|
||||
/// „Mathe, Raum 101, 08:00–08:45, Ausfall". Zeilenumbrüche in [location]
|
||||
|
||||
@@ -22,6 +22,10 @@ Future<void> showDetailsBottomSheet(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 16 + MediaQuery.viewInsetsOf(sheetContext).bottom,
|
||||
),
|
||||
// Ohne dieses innere Material malt ListTile-Ink aufs Sheet-Material
|
||||
// außerhalb des Scrollbereichs und stretcht beim Overscroll nicht mit.
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -32,5 +36,6 @@ Future<void> showDetailsBottomSheet(
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,8 @@ class _DownloadTrayHostState extends State<DownloadTrayHost>
|
||||
/// The chip only shows when there's something the inline UI can't already
|
||||
/// convey: multiple downloads, a finished/failed one (no inline progress
|
||||
/// left), or a lone download whose originating screen the user has left.
|
||||
bool _shouldShowChip(List<DownloadJob> jobs, int epoch) => shouldShowDownloadChip(
|
||||
bool _shouldShowChip(List<DownloadJob> jobs, int epoch) =>
|
||||
shouldShowDownloadChip(
|
||||
sheetOpen: _sheetOpen,
|
||||
jobCount: jobs.length,
|
||||
anySurfaced: jobs.any(
|
||||
@@ -311,7 +312,12 @@ class _TrayChip extends StatelessWidget {
|
||||
label = doneCount == 1 ? 'Download fertig' : '$doneCount fertig';
|
||||
}
|
||||
|
||||
return Material(
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: '$label, Downloads anzeigen',
|
||||
onTap: onTap,
|
||||
child: ExcludeSemantics(
|
||||
child: Material(
|
||||
color: colors.surfaceContainerHigh,
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
@@ -320,7 +326,10 @@ class _TrayChip extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -337,11 +346,17 @@ class _TrayChip extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.expand_less, size: 20, color: colors.onSurfaceVariant),
|
||||
Icon(
|
||||
Icons.expand_less,
|
||||
size: 20,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -186,6 +186,7 @@ class _FileViewerState extends State<FileViewer> {
|
||||
...actions,
|
||||
if (showActionsMenu)
|
||||
PopupMenuButton<FileViewingActions>(
|
||||
tooltip: 'Dateiaktionen',
|
||||
onSelected: _handleAction,
|
||||
itemBuilder: (context) => _availableActions()
|
||||
.map(
|
||||
@@ -282,8 +283,10 @@ class _FileViewerState extends State<FileViewer> {
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildPdfView() =>
|
||||
Scaffold(appBar: _appbar(), body: DeferredPdfViewer(path: widget.path));
|
||||
Widget _buildPdfView() => Scaffold(
|
||||
appBar: _appbar(),
|
||||
body: DeferredPdfViewer(path: widget.path),
|
||||
);
|
||||
|
||||
Widget _buildVideoView() => Scaffold(
|
||||
appBar: _appbar(),
|
||||
|
||||
Reference in New Issue
Block a user