From e43c0a9ec411485bd828ff99663da69cecdbf2cc Mon Sep 17 00:00:00 2001 From: Vince Varga Date: Sun, 30 Oct 2022 19:43:12 +0100 Subject: [PATCH 1/2] Format changelong --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebadaa4..7b97688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ ## 1.1.0 -Minor improvements, such as -* Make `WeekdaySelector` stateless (it had no reason to be stateful to begin with) -* Add assertion to make sure that displayed days are in a valid format +Minor improvements, such as: +* Make `WeekdaySelector` stateless (it had no reason to be stateful to begin with). +* Add assertion to make sure that displayed days are in a valid format. ## 1.0.0 @@ -14,9 +14,9 @@ Migrate to null-safety. ## 0.4.0 -Remove `DiagnosticableMixin` and use the `Diagnosticable` mixin. [`#2`](https://github.com/smaho-engineering/weekday_selector/pull/2) +Remove `DiagnosticableMixin` and use the `Diagnosticable` mixin. [`#2`](https://github.com/smaho-engineering/weekday_selector/pull/2). -Remove enormous example videos from the repository, use hashed URLs. For more info, see: [`pub-dev #3849`](https://github.com/dart-lang/pub-dev/issues/3849) +Remove enormous example videos from the repository, use hashed URLs. For more info, see: [`pub-dev #3849`](https://github.com/dart-lang/pub-dev/issues/3849). ## 0.3.1 From ab5519de064b5d1b28e74a5b24b54b9cb0f0b242 Mon Sep 17 00:00:00 2001 From: Vince Varga Date: Sun, 30 Oct 2022 19:44:12 +0100 Subject: [PATCH 2/2] Add accesibility proof-of-concept --- CHANGELOG.md | 4 ++ example/.metadata | 8 +-- example/lib/main.dart | 71 ++++++++++++++++++---- lib/src/weekday_selector.dart | 109 ++++++++++++++++++++++++++++++++-- 4 files changed, 170 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b97688..737ff09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.0 + +Accessibility improvements + ## 1.1.0 Minor improvements, such as: diff --git a/example/.metadata b/example/.metadata index 6e2cb40..1179587 100644 --- a/example/.metadata +++ b/example/.metadata @@ -15,13 +15,7 @@ migration: - platform: root create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - - platform: linux - create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - - platform: macos - create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - - platform: windows + - platform: ios create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a diff --git a/example/lib/main.dart b/example/lib/main.dart index a8ba2de..e807c0a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbols.dart'; @@ -83,8 +84,10 @@ class _MyAppState extends State { locales = dateTimeSymbolMap() .keys .cast() - .map((String k) => Locale( - k.split('_')[0], k.split('_').length > 1 ? k.split('_')[1] : null)) + .map((k) => Locale( + k.split('_')[0], + k.split('_').length > 1 ? k.split('_')[1] : null, + )) .toList(); super.initState(); } @@ -113,7 +116,8 @@ class UsageExamples extends StatelessWidget { initializeDateFormatting(); final examples = [ SimpleExampleWeekendsStatic(), - SelectedDaysUpdateExample(), + CheckboxLikeExample(), + RadioLikeExample(), DisabledExample(), DisplayedDaysExample(), // TODO: use with setstate @@ -202,7 +206,7 @@ printIntAsDay(int day) { String intDayToEnglish(int day) { if (day % 7 == DateTime.monday % 7) return 'Monday'; - if (day % 7 == DateTime.tuesday % 7) return 'Tueday'; + if (day % 7 == DateTime.tuesday % 7) return 'Tuesday'; if (day % 7 == DateTime.wednesday % 7) return 'Wednesday'; if (day % 7 == DateTime.thursday % 7) return 'Thursday'; if (day % 7 == DateTime.friday % 7) return 'Friday'; @@ -260,6 +264,10 @@ class _SimpleExampleWeekendsStaticState // We display the last tapped value in the example app onChanged: (v) { printIntAsDay(v); + SemanticsService.announce( + 'onChanged callback was last called with $v', + TextDirection.ltr, + ); setState(() => lastTapped = v); }, values: [ @@ -311,13 +319,12 @@ class _DisabledExampleState extends State { } } -class SelectedDaysUpdateExample extends StatefulWidget { +class CheckboxLikeExample extends StatefulWidget { @override - _SelectedDaysUpdateExampleState createState() => - _SelectedDaysUpdateExampleState(); + _CheckboxLikeExampleState createState() => _CheckboxLikeExampleState(); } -class _SelectedDaysUpdateExampleState extends State { +class _CheckboxLikeExampleState extends State { final values = List.filled(7, false); @override @@ -325,12 +332,16 @@ class _SelectedDaysUpdateExampleState extends State { return Column( mainAxisSize: MainAxisSize.min, children: [ - ExampleTitle('Stateful widget with selected days'), + ExampleTitle('Checkbox-like weekday selector'), Text( - 'When the user taps on a day, toggle the state! You can use stateful widgets, or any other methods for managing your state.'), + 'When the user taps on a day, toggle the state! ' + 'You can use stateful widgets, or any other methods for managing your state.', + ), // Using v == true, as some values could be null! Text( - 'The days that are currently selected are: ${valuesToEnglishDays(values, true)}.'), + 'The days that are currently selected are: ' + '${valuesToEnglishDays(values, true)}.', + ), WeekdaySelector( selectedFillColor: Colors.indigo, onChanged: (v) { @@ -340,6 +351,44 @@ class _SelectedDaysUpdateExampleState extends State { }); }, values: values, + semanticsWrapper: WeekdayButton.checkboxSemanticsWrapper, + ), + ], + ); + } +} + +class RadioLikeExample extends StatefulWidget { + @override + _RadioLikeExampleState createState() => _RadioLikeExampleState(); +} + +class _RadioLikeExampleState extends State { + var values = List.filled(7, false); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ExampleTitle('Radio-like weekday selector'), + Text('When the user taps on a day, select that day.'), + // Using v == true, as some values could be null! + Text( + 'Currently selected day:' + '${valuesToEnglishDays(values, true)}.', + ), + WeekdaySelector( + selectedFillColor: Colors.blue, + onChanged: (v) { + printIntAsDay(v); + setState(() { + values = List.filled(7, false); + values[v % 7] = true; + }); + }, + values: values, + semanticsWrapper: WeekdayButton.radioSemanticsWrapper, ), ], ); diff --git a/lib/src/weekday_selector.dart b/lib/src/weekday_selector.dart index dc8b7b4..0f96e6f 100644 --- a/lib/src/weekday_selector.dart +++ b/lib/src/weekday_selector.dart @@ -88,6 +88,7 @@ class WeekdaySelector extends StatelessWidget { this.shape, this.selectedShape, this.disabledShape, + this.semanticsWrapper, }) : assert(values.length == 7), assert(shortWeekdays.length == 7), assert(weekdays.length == 7), @@ -244,6 +245,20 @@ class WeekdaySelector extends StatelessWidget { /// state management library) so that the parent gets rebuilt. final ValueChanged? onChanged; + /// Provides a wrapper around the weekday buttons for improving accessibility. + /// + /// For more info, check [WeekdayButton]'s `semanticsWrapper` field. + final WeekdayButtonSemanticsWrapper? semanticsWrapper; + + static Widget checkbox(Widget weekdayButton, bool? selected, String label) { + return Semantics( + checked: selected, + label: label, + tooltip: label, + child: weekdayButton, + ); + } + Widget buildButtonWith(int value) { // In the arrays, element at index 0 correspond to Sunday... final arrayIndex = value % 7; @@ -278,6 +293,7 @@ class WeekdaySelector extends StatelessWidget { shape: shape, selectedShape: selectedShape, disabledShape: disabledShape, + semanticsWrapper: semanticsWrapper, ); } @@ -288,7 +304,7 @@ class WeekdaySelector extends StatelessWidget { return Row( textDirection: textDirection, children: days - .where((d) => displayedIndices.contains(d)) + .where(displayedIndices.contains) .map((i) => i + firstDayOfWeek) .map(buildButtonWith) .toList(), @@ -296,6 +312,15 @@ class WeekdaySelector extends StatelessWidget { } } +/// Function signature for wrapping individual weekday buttons. +typedef WeekdayButtonSemanticsWrapper = Widget Function( + BuildContext context, + String label, + bool? selected, + VoidCallback? onPressed, + Widget child, +); + /// A single button that holds a weekday. /// /// This widget is used in the [WeekdaySelector] widget, @@ -331,6 +356,7 @@ class WeekdayButton extends StatelessWidget { this.shape, this.selectedShape, this.disabledShape, + this.semanticsWrapper, }) : assert(text.length != 0), assert(tooltip.length != 0), super(key: key); @@ -437,6 +463,75 @@ class WeekdayButton extends StatelessWidget { /// The shape of the disabled day button's [Material]. final ShapeBorder? disabledShape; + /// Provides a wrapper around the weekday buttons for improving accessibility. + /// + /// See [WeekdayButton.checkboxSemanticsWrapper], + /// [WeekdayButton.radioSemanticsWrapper] and + /// [WeekdayButton.tooltipWrapper] for examples. + /// + /// If these examples don't match your use case, you can create your own. + /// See [WeekdayButtonSemanticsWrapper] for more info. + /// + /// If omitted, defaults to [WeekdayButton.tooltipWrapper]. + final WeekdayButtonSemanticsWrapper? semanticsWrapper; + + static WeekdayButtonSemanticsWrapper checkboxSemanticsWrapper = ( + BuildContext context, + String label, + bool? selected, + VoidCallback? onPressed, + Widget child, + ) { + return Semantics( + label: label, + checked: selected, + enabled: onPressed != null, + onTap: onPressed, + inMutuallyExclusiveGroup: false, + child: Tooltip( + message: label, + child: ExcludeSemantics( + child: child, + ), + ), + ); + }; + + static WeekdayButtonSemanticsWrapper radioSemanticsWrapper = ( + BuildContext context, + String label, + bool? selected, + VoidCallback? onPressed, + Widget child, + ) { + return Semantics( + label: label, + checked: selected, + enabled: onPressed != null, + onTap: onPressed, + inMutuallyExclusiveGroup: true, + child: Tooltip( + message: label, + child: ExcludeSemantics( + child: child, + ), + ), + ); + }; + + static WeekdayButtonSemanticsWrapper tooltipWrapper = ( + BuildContext context, + String label, + bool? selected, + VoidCallback? onPressed, + Widget child, + ) { + return Tooltip( + message: label, + child: child, + ); + }; + @override Widget build(BuildContext context) { Color currentColor; @@ -512,10 +607,16 @@ class WeekdayButton extends StatelessWidget { theme.textTheme.bodyText2!.copyWith(color: currentColor); } + final semanticsWrapper = this.semanticsWrapper ?? tooltipWrapper; + final label = tooltip; + return Expanded( - child: Tooltip( - message: tooltip, - child: RawMaterialButton( + child: semanticsWrapper( + context, + label, + selected, + onPressed, + RawMaterialButton( textStyle: currentTextStyle, elevation: currentElevation ?? 0.0, disabledElevation: currentDisabledElevation ?? 0.0,