Skip to content

Commit d0664bc

Browse files
authored
a few web tweaks for a11y assessment app (#134479)
Mostly tweaks for better focus management, namely: * Use `autofocus` throughout so the a11y focus is transferred to a logical place when overlaid content pops up (screen transitions, dialogs). * Consolidate "enabled" and "disabled" widgets into the same screen. Otherwise, when only a disabled widget is shown, there's nothing to focus on and the screen reader is lost.
1 parent 30a9f99 commit d0664bc

15 files changed

+121
-158
lines changed

dev/a11y_assessments/lib/main.dart

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,23 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/foundation.dart';
56
import 'package:flutter/material.dart';
7+
import 'package:flutter/rendering.dart';
68

79
import 'use_cases/use_cases.dart';
810

11+
// TODO(yjbanov): https://github.com/flutter/flutter/issues/83809
12+
// Currently this app (as most Flutter Web apps) relies on the
13+
// `autofocus` property to guide the a11y focus when navigating
14+
// across routes (screen transitions, dialogs, etc). We may want
15+
// to revisit this after we figure out a long-term story for a11y
16+
// focus. See also https://github.com/flutter/flutter/issues/97747
917
void main() {
1018
runApp(const App());
19+
if (kIsWeb) {
20+
SemanticsBinding.instance.ensureSemantics();
21+
}
1122
}
1223

1324
class App extends StatelessWidget {
@@ -36,12 +47,13 @@ class App extends StatelessWidget {
3647
class HomePage extends StatelessWidget {
3748
const HomePage({super.key});
3849

39-
Widget _buildUseCaseItem(UseCase useCase) {
50+
Widget _buildUseCaseItem(int index, UseCase useCase) {
4051
return Padding(
4152
padding: const EdgeInsets.all(10),
4253
child: Builder(
4354
builder: (BuildContext context) {
4455
return TextButton(
56+
autofocus: index == 0,
4557
key: Key(useCase.name),
4658
onPressed: () => Navigator.of(context).pushNamed(useCase.route),
4759
child: Text(useCase.name),
@@ -57,7 +69,10 @@ class HomePage extends StatelessWidget {
5769
appBar: AppBar(title: const Text('Accessibility Assessments')),
5870
body: Center(
5971
child: ListView(
60-
children: useCases.map<Widget>(_buildUseCaseItem).toList(),
72+
children: List<Widget>.generate(
73+
useCases.length,
74+
(int index) => _buildUseCaseItem(index, useCases[index]),
75+
),
6176
),
6277
),
6378
);

dev/a11y_assessments/lib/use_cases/check_box_list_tile.dart

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,29 @@ class _MainWidgetState extends State<_MainWidget> {
3030
Widget build(BuildContext context) {
3131
return Scaffold(
3232
appBar: AppBar(title: const Text('CheckBoxListTile')),
33-
body: Center(
34-
child: CheckboxListTile(
35-
value: _checked,
36-
onChanged: (bool? value) {
37-
setState(() {
38-
_checked = value!;
39-
});
40-
},
41-
title: const Text('a check box list title'),
42-
),
33+
body: ListView(
34+
children: <Widget>[
35+
CheckboxListTile(
36+
autofocus: true,
37+
value: _checked,
38+
onChanged: (bool? value) {
39+
setState(() {
40+
_checked = value!;
41+
});
42+
},
43+
title: const Text('a check box list title'),
44+
),
45+
CheckboxListTile(
46+
value: _checked,
47+
onChanged: (bool? value) {
48+
setState(() {
49+
_checked = value!;
50+
});
51+
},
52+
title: const Text('a disabled check box list title'),
53+
enabled: false,
54+
),
55+
],
4356
),
4457
);
4558
}

dev/a11y_assessments/lib/use_cases/check_box_list_tile_disabled.dart

Lines changed: 0 additions & 47 deletions
This file was deleted.

dev/a11y_assessments/lib/use_cases/date_picker.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class _MainWidgetState extends State<_MainWidget> {
3636
),
3737
body: Center(
3838
child: TextButton(
39+
autofocus: true,
3940
onPressed: () => showDatePicker(
4041
context: context,
4142
initialEntryMode: DatePickerEntryMode.calendarOnly,

dev/a11y_assessments/lib/use_cases/dialog.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class _MainWidget extends StatelessWidget {
2929
),
3030
body: Center(
3131
child: TextButton(
32+
autofocus: true,
3233
onPressed: () => showDialog<String>(
3334
context: context,
3435
builder: (BuildContext context) => Dialog(
@@ -41,6 +42,7 @@ class _MainWidget extends StatelessWidget {
4142
const Text('This is a typical dialog.'),
4243
const SizedBox(height: 15),
4344
TextButton(
45+
autofocus: true,
4446
onPressed: () {
4547
Navigator.pop(context);
4648
},

dev/a11y_assessments/lib/use_cases/slider.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class MainWidgetState extends State<MainWidget> {
3737
),
3838
body: Center(
3939
child: Slider(
40+
autofocus: true,
4041
value: currentSliderValue,
4142
max: 100,
4243
divisions: 5,

dev/a11y_assessments/lib/use_cases/text_field.dart

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,28 @@ class _MainWidget extends StatelessWidget {
2828
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
2929
title: const Text('TextField'),
3030
),
31-
body: const Center(
32-
child: TextField(
33-
decoration: InputDecoration(
34-
labelText: 'Email',
35-
suffixText: '@gmail.com',
36-
hintText: 'Enter your email',
31+
body: ListView(
32+
children: <Widget>[
33+
const TextField(
34+
key: Key('enabled text field'),
35+
autofocus: true,
36+
decoration: InputDecoration(
37+
labelText: 'Email',
38+
suffixText: '@gmail.com',
39+
hintText: 'Enter your email',
40+
),
3741
),
38-
)
42+
TextField(
43+
key: const Key('disabled text field'),
44+
decoration: const InputDecoration(
45+
labelText: 'Email',
46+
suffixText: '@gmail.com',
47+
hintText: 'Enter your email',
48+
),
49+
enabled: false,
50+
controller: TextEditingController(text: 'xyz'),
51+
),
52+
],
3953
),
4054
);
4155
}

dev/a11y_assessments/lib/use_cases/text_field_disabled.dart

Lines changed: 0 additions & 44 deletions
This file was deleted.

dev/a11y_assessments/lib/use_cases/text_field_password.dart

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,27 @@ class _MainWidget extends StatelessWidget {
2828
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
2929
title: const Text('TextField password'),
3030
),
31-
body: const Center(
32-
child: TextField(
33-
decoration: InputDecoration(
34-
labelText: 'Password',
35-
hintText: 'Enter your password',
31+
body: ListView(
32+
children: const <Widget>[
33+
TextField(
34+
key: Key('enabled password'),
35+
autofocus: true,
36+
decoration: InputDecoration(
37+
labelText: 'Password',
38+
hintText: 'Enter your password',
39+
),
40+
obscureText: true,
3641
),
37-
obscureText: true,
38-
)
42+
TextField(
43+
key: Key('disabled password'),
44+
decoration: InputDecoration(
45+
labelText: 'Password',
46+
hintText: 'Enter your password',
47+
),
48+
enabled: false,
49+
obscureText: true,
50+
),
51+
],
3952
),
4053
);
4154
}

dev/a11y_assessments/lib/use_cases/use_cases.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
import 'package:flutter/widgets.dart';
66

77
import 'check_box_list_tile.dart';
8-
import 'check_box_list_tile_disabled.dart';
98
import 'date_picker.dart';
109
import 'dialog.dart';
1110
import 'slider.dart';
1211
import 'text_field.dart';
13-
import 'text_field_disabled.dart';
1412
import 'text_field_password.dart';
1513

1614
abstract class UseCase {
@@ -21,11 +19,9 @@ abstract class UseCase {
2119

2220
final List<UseCase> useCases = <UseCase>[
2321
CheckBoxListTile(),
24-
CheckBoxListTileDisabled(),
2522
DialogUseCase(),
2623
SliderUseCase(),
2724
TextFieldUseCase(),
28-
TextFieldDisabledUseCase(),
2925
TextFieldPasswordUseCase(),
3026
DatePickerUseCase(),
3127
];

dev/a11y_assessments/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: a11y_assessments
2-
description: "A new Flutter project."
2+
description: A new Flutter project
33

44
environment:
55
sdk: '>=3.2.0-22.0.dev <4.0.0'

dev/a11y_assessments/test/text_field_disabled_test.dart

Lines changed: 0 additions & 23 deletions
This file was deleted.

dev/a11y_assessments/test/text_field_password_test.dart

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,23 @@ import 'test_utils.dart';
1111
void main() {
1212
testWidgets('text field password can run', (WidgetTester tester) async {
1313
await pumpsUseCase(tester, TextFieldPasswordUseCase());
14-
expect(find.byType(TextField), findsOneWidget);
14+
expect(find.byType(TextField), findsExactly(2));
1515

16-
await tester.tap(find.byType(TextField));
17-
await tester.pumpAndSettle();
18-
await tester.enterText(find.byType(TextField), 'abc');
19-
await tester.pumpAndSettle();
20-
expect(find.text('abc'), findsOneWidget);
16+
// Test the enabled password
17+
{
18+
final Finder finder = find.byKey(const Key('enabled password'));
19+
await tester.tap(finder);
20+
await tester.pumpAndSettle();
21+
await tester.enterText(finder, 'abc');
22+
await tester.pumpAndSettle();
23+
expect(find.text('abc'), findsOneWidget);
24+
}
25+
26+
// Test the disabled password
27+
{
28+
final Finder finder = find.byKey(const Key('disabled password'));
29+
final TextField passwordField = tester.widget<TextField>(finder);
30+
expect(passwordField.enabled, isFalse);
31+
}
2132
});
2233
}

0 commit comments

Comments
 (0)