Unverified Commit d0664bcd authored by Yegor's avatar Yegor Committed by GitHub

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.
parent 30a9f99b
...@@ -2,12 +2,23 @@ ...@@ -2,12 +2,23 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'use_cases/use_cases.dart'; import 'use_cases/use_cases.dart';
// TODO(yjbanov): https://github.com/flutter/flutter/issues/83809
// Currently this app (as most Flutter Web apps) relies on the
// `autofocus` property to guide the a11y focus when navigating
// across routes (screen transitions, dialogs, etc). We may want
// to revisit this after we figure out a long-term story for a11y
// focus. See also https://github.com/flutter/flutter/issues/97747
void main() { void main() {
runApp(const App()); runApp(const App());
if (kIsWeb) {
SemanticsBinding.instance.ensureSemantics();
}
} }
class App extends StatelessWidget { class App extends StatelessWidget {
...@@ -36,12 +47,13 @@ class App extends StatelessWidget { ...@@ -36,12 +47,13 @@ class App extends StatelessWidget {
class HomePage extends StatelessWidget { class HomePage extends StatelessWidget {
const HomePage({super.key}); const HomePage({super.key});
Widget _buildUseCaseItem(UseCase useCase) { Widget _buildUseCaseItem(int index, UseCase useCase) {
return Padding( return Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: Builder( child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return TextButton( return TextButton(
autofocus: index == 0,
key: Key(useCase.name), key: Key(useCase.name),
onPressed: () => Navigator.of(context).pushNamed(useCase.route), onPressed: () => Navigator.of(context).pushNamed(useCase.route),
child: Text(useCase.name), child: Text(useCase.name),
...@@ -57,7 +69,10 @@ class HomePage extends StatelessWidget { ...@@ -57,7 +69,10 @@ class HomePage extends StatelessWidget {
appBar: AppBar(title: const Text('Accessibility Assessments')), appBar: AppBar(title: const Text('Accessibility Assessments')),
body: Center( body: Center(
child: ListView( child: ListView(
children: useCases.map<Widget>(_buildUseCaseItem).toList(), children: List<Widget>.generate(
useCases.length,
(int index) => _buildUseCaseItem(index, useCases[index]),
),
), ),
), ),
); );
......
...@@ -30,16 +30,29 @@ class _MainWidgetState extends State<_MainWidget> { ...@@ -30,16 +30,29 @@ class _MainWidgetState extends State<_MainWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('CheckBoxListTile')), appBar: AppBar(title: const Text('CheckBoxListTile')),
body: Center( body: ListView(
child: CheckboxListTile( children: <Widget>[
value: _checked, CheckboxListTile(
onChanged: (bool? value) { autofocus: true,
setState(() { value: _checked,
_checked = value!; onChanged: (bool? value) {
}); setState(() {
}, _checked = value!;
title: const Text('a check box list title'), });
), },
title: const Text('a check box list title'),
),
CheckboxListTile(
value: _checked,
onChanged: (bool? value) {
setState(() {
_checked = value!;
});
},
title: const Text('a disabled check box list title'),
enabled: false,
),
],
), ),
); );
} }
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'use_cases.dart';
class CheckBoxListTileDisabled extends UseCase {
@override
String get name => 'CheckBoxListTile Disabled';
@override
String get route => '/check-box-list-tile-disabled';
@override
Widget build(BuildContext context) => _MainWidget();
}
class _MainWidget extends StatefulWidget {
@override
State<_MainWidget> createState() => _MainWidgetState();
}
class _MainWidgetState extends State<_MainWidget> {
bool _checked = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('CheckBoxListTile Disabled')),
body: Center(
child: CheckboxListTile(
value: _checked,
onChanged: (bool? value) {
setState(() {
_checked = value!;
});
},
title: const Text('a disabled check box list title'),
enabled: false,
),
),
);
}
}
...@@ -36,6 +36,7 @@ class _MainWidgetState extends State<_MainWidget> { ...@@ -36,6 +36,7 @@ class _MainWidgetState extends State<_MainWidget> {
), ),
body: Center( body: Center(
child: TextButton( child: TextButton(
autofocus: true,
onPressed: () => showDatePicker( onPressed: () => showDatePicker(
context: context, context: context,
initialEntryMode: DatePickerEntryMode.calendarOnly, initialEntryMode: DatePickerEntryMode.calendarOnly,
......
...@@ -29,6 +29,7 @@ class _MainWidget extends StatelessWidget { ...@@ -29,6 +29,7 @@ class _MainWidget extends StatelessWidget {
), ),
body: Center( body: Center(
child: TextButton( child: TextButton(
autofocus: true,
onPressed: () => showDialog<String>( onPressed: () => showDialog<String>(
context: context, context: context,
builder: (BuildContext context) => Dialog( builder: (BuildContext context) => Dialog(
...@@ -41,6 +42,7 @@ class _MainWidget extends StatelessWidget { ...@@ -41,6 +42,7 @@ class _MainWidget extends StatelessWidget {
const Text('This is a typical dialog.'), const Text('This is a typical dialog.'),
const SizedBox(height: 15), const SizedBox(height: 15),
TextButton( TextButton(
autofocus: true,
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
}, },
......
...@@ -37,6 +37,7 @@ class MainWidgetState extends State<MainWidget> { ...@@ -37,6 +37,7 @@ class MainWidgetState extends State<MainWidget> {
), ),
body: Center( body: Center(
child: Slider( child: Slider(
autofocus: true,
value: currentSliderValue, value: currentSliderValue,
max: 100, max: 100,
divisions: 5, divisions: 5,
......
...@@ -28,14 +28,28 @@ class _MainWidget extends StatelessWidget { ...@@ -28,14 +28,28 @@ class _MainWidget extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.inversePrimary, backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('TextField'), title: const Text('TextField'),
), ),
body: const Center( body: ListView(
child: TextField( children: <Widget>[
decoration: InputDecoration( const TextField(
labelText: 'Email', key: Key('enabled text field'),
suffixText: '@gmail.com', autofocus: true,
hintText: 'Enter your email', decoration: InputDecoration(
labelText: 'Email',
suffixText: '@gmail.com',
hintText: 'Enter your email',
),
), ),
) TextField(
key: const Key('disabled text field'),
decoration: const InputDecoration(
labelText: 'Email',
suffixText: '@gmail.com',
hintText: 'Enter your email',
),
enabled: false,
controller: TextEditingController(text: 'xyz'),
),
],
), ),
); );
} }
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'use_cases.dart';
class TextFieldDisabledUseCase extends UseCase {
@override
String get name => 'TextField disabled';
@override
String get route => '/text-field-disabled';
@override
Widget build(BuildContext context) => const _MainWidget();
}
class _MainWidget extends StatelessWidget {
const _MainWidget();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('TextField disabled'),
),
body: Center(
child: TextField(
decoration: const InputDecoration(
labelText: 'Email',
suffixText: '@gmail.com',
hintText: 'Enter your email',
enabled: false,
),
controller: TextEditingController(text: 'abc'),
)
),
);
}
}
...@@ -28,14 +28,27 @@ class _MainWidget extends StatelessWidget { ...@@ -28,14 +28,27 @@ class _MainWidget extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.inversePrimary, backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('TextField password'), title: const Text('TextField password'),
), ),
body: const Center( body: ListView(
child: TextField( children: const <Widget>[
decoration: InputDecoration( TextField(
labelText: 'Password', key: Key('enabled password'),
hintText: 'Enter your password', autofocus: true,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
),
obscureText: true,
), ),
obscureText: true, TextField(
) key: Key('disabled password'),
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
),
enabled: false,
obscureText: true,
),
],
), ),
); );
} }
......
...@@ -5,12 +5,10 @@ ...@@ -5,12 +5,10 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'check_box_list_tile.dart'; import 'check_box_list_tile.dart';
import 'check_box_list_tile_disabled.dart';
import 'date_picker.dart'; import 'date_picker.dart';
import 'dialog.dart'; import 'dialog.dart';
import 'slider.dart'; import 'slider.dart';
import 'text_field.dart'; import 'text_field.dart';
import 'text_field_disabled.dart';
import 'text_field_password.dart'; import 'text_field_password.dart';
abstract class UseCase { abstract class UseCase {
...@@ -21,11 +19,9 @@ abstract class UseCase { ...@@ -21,11 +19,9 @@ abstract class UseCase {
final List<UseCase> useCases = <UseCase>[ final List<UseCase> useCases = <UseCase>[
CheckBoxListTile(), CheckBoxListTile(),
CheckBoxListTileDisabled(),
DialogUseCase(), DialogUseCase(),
SliderUseCase(), SliderUseCase(),
TextFieldUseCase(), TextFieldUseCase(),
TextFieldDisabledUseCase(),
TextFieldPasswordUseCase(), TextFieldPasswordUseCase(),
DatePickerUseCase(), DatePickerUseCase(),
]; ];
name: a11y_assessments name: a11y_assessments
description: "A new Flutter project." description: A new Flutter project
environment: environment:
sdk: '>=3.2.0-22.0.dev <4.0.0' sdk: '>=3.2.0-22.0.dev <4.0.0'
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:a11y_assessments/use_cases/text_field_disabled.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'test_utils.dart';
void main() {
testWidgets('text field disabled can run', (WidgetTester tester) async {
await pumpsUseCase(tester, TextFieldDisabledUseCase());
expect(find.byType(TextField), findsOneWidget);
expect(find.text('abc'), findsOneWidget);
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'bde');
await tester.pumpAndSettle();
expect(find.text('abc'), findsOneWidget);
});
}
...@@ -11,12 +11,23 @@ import 'test_utils.dart'; ...@@ -11,12 +11,23 @@ import 'test_utils.dart';
void main() { void main() {
testWidgets('text field password can run', (WidgetTester tester) async { testWidgets('text field password can run', (WidgetTester tester) async {
await pumpsUseCase(tester, TextFieldPasswordUseCase()); await pumpsUseCase(tester, TextFieldPasswordUseCase());
expect(find.byType(TextField), findsOneWidget); expect(find.byType(TextField), findsExactly(2));
await tester.tap(find.byType(TextField)); // Test the enabled password
await tester.pumpAndSettle(); {
await tester.enterText(find.byType(TextField), 'abc'); final Finder finder = find.byKey(const Key('enabled password'));
await tester.pumpAndSettle(); await tester.tap(finder);
expect(find.text('abc'), findsOneWidget); await tester.pumpAndSettle();
await tester.enterText(finder, 'abc');
await tester.pumpAndSettle();
expect(find.text('abc'), findsOneWidget);
}
// Test the disabled password
{
final Finder finder = find.byKey(const Key('disabled password'));
final TextField passwordField = tester.widget<TextField>(finder);
expect(passwordField.enabled, isFalse);
}
}); });
} }
...@@ -11,12 +11,23 @@ import 'test_utils.dart'; ...@@ -11,12 +11,23 @@ import 'test_utils.dart';
void main() { void main() {
testWidgets('text field can run', (WidgetTester tester) async { testWidgets('text field can run', (WidgetTester tester) async {
await pumpsUseCase(tester, TextFieldUseCase()); await pumpsUseCase(tester, TextFieldUseCase());
expect(find.byType(TextField), findsOneWidget); expect(find.byType(TextField), findsExactly(2));
await tester.tap(find.byType(TextField)); // Test the enabled text field
await tester.pumpAndSettle(); {
await tester.enterText(find.byType(TextField), 'abc'); final Finder finder = find.byKey(const Key('enabled text field'));
await tester.pumpAndSettle(); await tester.tap(finder);
expect(find.text('abc'), findsOneWidget); await tester.pumpAndSettle();
await tester.enterText(finder, 'abc');
await tester.pumpAndSettle();
expect(find.text('abc'), findsOneWidget);
}
// Test the disabled text field
{
final Finder finder = find.byKey(const Key('disabled text field'));
final TextField textField = tester.widget<TextField>(finder);
expect(textField.enabled, isFalse);
}
}); });
} }
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
"display": "standalone", "display": "standalone",
"background_color": "#0175C2", "background_color": "#0175C2",
"theme_color": "#0175C2", "theme_color": "#0175C2",
"description": ""A new Flutter project."", "description": "A new Flutter project.",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"prefer_related_applications": false, "prefer_related_applications": false,
"icons": [ "icons": [
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment