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
@@ -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';