implemented comprehensive accessibility (A11y) support across the app

This commit is contained in:
2026-07-13 23:41:47 +02:00
parent 8274dd46cd
commit 478f0ff20b
46 changed files with 590 additions and 226 deletions
@@ -27,6 +27,7 @@ class CustomEventsView extends StatelessWidget {
actions: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Termin erstellen',
onPressed: () => _openCreateDialog(context),
),
],
@@ -67,6 +68,7 @@ class CustomEventsView extends StatelessWidget {
children: [
IconButton(
icon: const Icon(Icons.edit_outlined),
tooltip: 'Bearbeiten',
onPressed: () => showDialog(
context: context,
builder: (_) =>
@@ -75,6 +77,7 @@ class CustomEventsView extends StatelessWidget {
),
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: 'Löschen',
onPressed: () =>
showDeleteCustomEventDialog(context, e),
),
@@ -103,6 +103,7 @@ class LessonSheet {
static Widget _roomTile(BuildContext context, McTimetableEntry lesson) {
final trailing = IconButton(
icon: const Icon(Icons.house_outlined),
tooltip: 'Raumplan öffnen',
onPressed: () => AppRoutes.openRoomplan(context),
);
@@ -14,12 +14,17 @@ class SearchSubjectColors extends SearchDelegate<void> {
@override
List<Widget>? buildActions(BuildContext context) => [
if (query.isNotEmpty)
IconButton(onPressed: () => query = '', icon: const Icon(Icons.clear)),
IconButton(
onPressed: () => query = '',
tooltip: 'Leeren',
icon: const Icon(Icons.clear),
),
];
@override
Widget? buildLeading(BuildContext context) => IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: 'Zurück',
onPressed: () => close(context, null),
);
@@ -21,6 +21,7 @@ class SubjectColorsView extends StatelessWidget {
actions: [
IconButton(
icon: const Icon(Icons.search),
tooltip: 'Suchen',
onPressed: () =>
showSearch(context: context, delegate: SearchSubjectColors()),
),
+2
View File
@@ -110,6 +110,7 @@ class _TimetableState extends State<Timetable> {
actions: [
IconButton(
icon: const Icon(Icons.home_outlined),
tooltip: 'Zur aktuellen Woche',
onPressed: atToday ? null : _jumpToToday,
),
PopupMenuButton<_CalendarAction>(
@@ -179,6 +180,7 @@ class _TimetableState extends State<Timetable> {
actions: [
IconButton(
icon: const Icon(Icons.home_outlined),
tooltip: 'Zur aktuellen Woche',
onPressed: atToday ? null : _jumpToToday,
),
if (canViewForeign)
@@ -88,7 +88,7 @@ class _OutsideDayColumn extends StatelessWidget {
}
static String _subtitleFor(Appointment a) {
if (isAllDayLike(a)) return 'Ganztägig';
if (isAllDayLike(a)) return A11yLabels.allDay;
return '${a.startTime.formatHm()}${a.endTime.formatHm()}';
}
@@ -122,6 +122,7 @@ class _OutsideDayColumn extends StatelessWidget {
height: kOutsideChipHeight,
child: _OutsideChip(
appointment: visible[i],
crossedOut: isCrossedOut(visible[i]),
onTap: () => onAppointmentTap(visible[i]),
),
),
@@ -144,15 +145,28 @@ class _OutsideDayColumn extends StatelessWidget {
class _OutsideChip extends StatelessWidget {
final Appointment appointment;
final bool crossedOut;
final VoidCallback onTap;
const _OutsideChip({required this.appointment, required this.onTap});
const _OutsideChip({
required this.appointment,
required this.crossedOut,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final allDay = isAllDayLike(appointment);
final timeLabel = allDay ? null : appointment.startTime.formatHm();
final semanticsLabel = [
appointment.subject,
if (allDay)
A11yLabels.allDay
else
'${appointment.startTime.formatHm()}${appointment.endTime.formatHm()}',
if (crossedOut) A11yLabels.cancelled,
].join(', ');
// Past chips fade further, future/ongoing ones get a more saturated tint
// so the strip no longer reads as one uniform grey block.
@@ -163,47 +177,54 @@ class _OutsideChip extends StatelessWidget {
: theme.colorScheme.onSurface;
final subjectWeight = isPast ? FontWeight.w400 : FontWeight.w600;
return Material(
color: appointment.color.withAlpha(backgroundAlpha),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7)),
),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
appointment.subject,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: theme.textTheme.labelSmall?.copyWith(
color: subjectColor,
fontWeight: subjectWeight,
),
),
),
if (timeLabel != null) ...[
const SizedBox(width: 4),
Flexible(
child: Text(
timeLabel,
maxLines: 1,
overflow: TextOverflow.fade,
softWrap: false,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontSize: 10,
return Semantics(
button: true,
label: semanticsLabel,
onTap: onTap,
child: ExcludeSemantics(
child: Material(
color: appointment.color.withAlpha(backgroundAlpha),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7)),
),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
appointment.subject,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: theme.textTheme.labelSmall?.copyWith(
color: subjectColor,
fontWeight: subjectWeight,
),
),
),
),
],
],
if (timeLabel != null) ...[
const SizedBox(width: 4),
Flexible(
child: Text(
timeLabel,
maxLines: 1,
overflow: TextOverflow.fade,
softWrap: false,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontSize: 10,
),
),
),
],
],
),
),
),
),
),
@@ -220,18 +241,25 @@ class _OutsideOverflowChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Material(
color: theme.colorScheme.secondaryContainer,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Center(
child: Text(
'+$count weitere',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
return Semantics(
button: true,
label: A11yLabels.moreAppointments(count),
onTap: onTap,
child: ExcludeSemantics(
child: Material(
color: theme.colorScheme.secondaryContainer,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Center(
child: Text(
'+$count weitere',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
),
),
),
),
),
@@ -112,6 +112,7 @@ class _PeriodLabel extends StatelessWidget {
Icons.coffee_outlined,
size: 12,
color: secondaryTextColor.withAlpha(180),
semanticLabel: A11yLabels.breakTime,
),
);
}
@@ -328,18 +329,39 @@ class _DayColumn extends StatelessWidget {
left: cell.lane * width / cell.laneCount,
width: width / cell.laneCount,
child: switch (cell) {
LaidOutAppointment(:final appointment) => GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onAppointmentTap(appointment),
child: AppointmentTile(
appointment: appointment,
LaidOutAppointment(:final appointment) => Semantics(
button: true,
label: A11yLabels.appointmentLabel(
subject: appointment.subject,
location: appointment.location ?? '',
start: appointment.startTime,
end: appointment.endTime,
crossedOut: isCrossedOut(appointment),
),
onTap: () => onAppointmentTap(appointment),
child: ExcludeSemantics(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onAppointmentTap(appointment),
child: AppointmentTile(
appointment: appointment,
crossedOut: isCrossedOut(appointment),
),
),
),
),
LaidOutOverflow(:final appointments) => GestureDetector(
behavior: HitTestBehavior.opaque,
LaidOutOverflow(:final appointments) => Semantics(
button: true,
label: A11yLabels.moreAppointments(appointments.length),
onTap: () => _showOverflowSheet(context, appointments),
child: _OverflowTile(count: appointments.length),
child: ExcludeSemantics(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
_showOverflowSheet(context, appointments),
child: _OverflowTile(count: appointments.length),
),
),
),
},
),
@@ -14,6 +14,7 @@ import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../extensions/date_time.dart';
import '../../../../utils/haptics.dart';
import '../../../../widget/a11y/a11y_labels.dart';
import '../../../../widget/details_bottom_sheet.dart';
import '../data/calendar_layout.dart';
import '../data/calendar_logic.dart';