further improved accessibility across the application and add tooltips to action buttons
This commit is contained in:
@@ -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,104 +287,134 @@ class _DayColumn extends StatelessWidget {
|
||||
final isTablet = MediaQuery.of(context).size.shortestSide >= 600;
|
||||
final laidOut = assignLanes(dayAppointments, maxLanes: isTablet ? 3 : 2);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onLongPressStart: (details) => _handleLongPress(details, dayAppointments),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? theme.colorScheme.primary.withAlpha(14) : null,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: theme.dividerColor.withAlpha(90),
|
||||
width: 0.5,
|
||||
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),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? theme.colorScheme.primary.withAlpha(14) : null,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: theme.dividerColor.withAlpha(90),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
for (final period in schedule.periods)
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
top: layout.topOf(period),
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 0.5,
|
||||
color: theme.dividerColor.withAlpha(60),
|
||||
child: Semantics(
|
||||
header: true,
|
||||
label: headerLabel,
|
||||
child: const SizedBox(height: 1),
|
||||
),
|
||||
),
|
||||
for (final region in dayRegions)
|
||||
Positioned(
|
||||
top: layout.yOfDateTime(region.start),
|
||||
height:
|
||||
(layout.yOfDateTime(region.end) -
|
||||
layout.yOfDateTime(region.start))
|
||||
.clamp(0, double.infinity),
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: TimeRegionTile(region: region.region),
|
||||
),
|
||||
for (final cell in laidOut)
|
||||
Positioned(
|
||||
top: layout.yOfDateTime(cell.startTime),
|
||||
height:
|
||||
(layout.yOfDateTime(cell.endTime) -
|
||||
layout.yOfDateTime(cell.startTime))
|
||||
.clamp(0, double.infinity),
|
||||
left: cell.lane * width / cell.laneCount,
|
||||
width: width / cell.laneCount,
|
||||
child: switch (cell) {
|
||||
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),
|
||||
for (final period in schedule.periods)
|
||||
Positioned(
|
||||
top: layout.topOf(period),
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 0.5,
|
||||
color: theme.dividerColor.withAlpha(60),
|
||||
),
|
||||
),
|
||||
for (final region in dayRegions)
|
||||
Positioned(
|
||||
top: layout.yOfDateTime(region.start),
|
||||
height:
|
||||
(layout.yOfDateTime(region.end) -
|
||||
layout.yOfDateTime(region.start))
|
||||
.clamp(0, double.infinity),
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: TimeRegionTile(region: region.region),
|
||||
),
|
||||
for (final cell in laidOut)
|
||||
Positioned(
|
||||
top: layout.yOfDateTime(cell.startTime),
|
||||
height:
|
||||
(layout.yOfDateTime(cell.endTime) -
|
||||
layout.yOfDateTime(cell.startTime))
|
||||
.clamp(0, double.infinity),
|
||||
left: cell.lane * width / cell.laneCount,
|
||||
width: width / cell.laneCount,
|
||||
child: switch (cell) {
|
||||
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) => Semantics(
|
||||
button: true,
|
||||
label: A11yLabels.moreAppointments(appointments.length),
|
||||
onTap: () => _showOverflowSheet(context, appointments),
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () =>
|
||||
_showOverflowSheet(context, appointments),
|
||||
child: _OverflowTile(count: appointments.length),
|
||||
LaidOutOverflow(:final appointments) => Semantics(
|
||||
button: true,
|
||||
label: A11yLabels.moreAppointments(
|
||||
appointments.length,
|
||||
),
|
||||
onTap: () =>
|
||||
_showOverflowSheet(context, appointments),
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () =>
|
||||
_showOverflowSheet(context, appointments),
|
||||
child: _OverflowTile(count: appointments.length),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
if (isToday)
|
||||
ValueListenableBuilder<DateTime>(
|
||||
valueListenable: nowNotifier,
|
||||
builder: (_, now, child) => _CurrentTimeMarker(
|
||||
now: now,
|
||||
layout: layout,
|
||||
theme: theme,
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
if (isToday)
|
||||
ValueListenableBuilder<DateTime>(
|
||||
valueListenable: nowNotifier,
|
||||
builder: (_, now, child) => _CurrentTimeMarker(
|
||||
now: now,
|
||||
layout: layout,
|
||||
theme: theme,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,11 +164,16 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: _DayHeaderStrip(
|
||||
key: ValueKey(visibleWeekStart),
|
||||
weekStart: visibleWeekStart,
|
||||
today: _today,
|
||||
rulerWidth: _rulerWidth,
|
||||
// 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,
|
||||
today: _today,
|
||||
rulerWidth: _rulerWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user