implemented comprehensive accessibility (A11y) support across the app
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user