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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.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() {
runApp(const App());
if (kIsWeb) {
SemanticsBinding.instance.ensureSemantics();
}
}
class App extends StatelessWidget {
......@@ -36,12 +47,13 @@ class App extends StatelessWidget {
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget _buildUseCaseItem(UseCase useCase) {
Widget _buildUseCaseItem(int index, UseCase useCase) {
return Padding(
padding: const EdgeInsets.all(10),
child: Builder(
builder: (BuildContext context) {
return TextButton(
autofocus: index == 0,
key: Key(useCase.name),
onPressed: () => Navigator.of(context).pushNamed(useCase.route),
child: Text(useCase.name),
......@@ -57,7 +69,10 @@ class HomePage extends StatelessWidget {
appBar: AppBar(title: const Text('Accessibility Assessments')),
body: Center(
child: ListView(
children: useCases.map<Widget>(_buildUseCaseItem).toList(),
children: List<Widget>.generate(
useCases.length,
(int index) => _buildUseCaseItem(index, useCases[index]),
),
),
),
);
......
......@@ -30,8 +30,10 @@ class _MainWidgetState extends State<_MainWidget> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('CheckBoxListTile')),
body: Center(
child: CheckboxListTile(
body: ListView(
children: <Widget>[
CheckboxListTile(
autofocus: true,
value: _checked,
onChanged: (bool? value) {
setState(() {
......@@ -40,6 +42,17 @@ class _MainWidgetState extends State<_MainWidget> {
},
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> {
),
body: Center(
child: TextButton(
autofocus: true,
onPressed: () => showDatePicker(
context: context,
initialEntryMode: DatePickerEntryMode.calendarOnly,
......
......@@ -29,6 +29,7 @@ class _MainWidget extends StatelessWidget {
),
body: Center(
child: TextButton(
autofocus: true,
onPressed: () => showDialog<String>(
context: context,
builder: (BuildContext context) => Dialog(
......@@ -41,6 +42,7 @@ class _MainWidget extends StatelessWidget {
const Text('This is a typical dialog.'),
const SizedBox(height: 15),
TextButton(
autofocus: true,
onPressed: () {
Navigator.pop(context);
},
......
......@@ -37,6 +37,7 @@ class MainWidgetState extends State<MainWidget> {
),
body: Center(
child: Slider(
autofocus: true,
value: currentSliderValue,
max: 100,
divisions: 5,
......
......@@ -28,14 +28,28 @@ class _MainWidget extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('TextField'),
),
body: const Center(
child: TextField(
body: ListView(
children: <Widget>[
const TextField(
key: Key('enabled text field'),
autofocus: true,
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 {
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('TextField password'),
),
body: const Center(
child: TextField(
body: ListView(
children: const <Widget>[
TextField(
key: Key('enabled password'),
autofocus: true,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
),
obscureText: true,
)
),
TextField(
key: Key('disabled password'),
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
),
enabled: false,
obscureText: true,
),
],
),
);
}
......
......@@ -5,12 +5,10 @@
import 'package:flutter/widgets.dart';
import 'check_box_list_tile.dart';
import 'check_box_list_tile_disabled.dart';
import 'date_picker.dart';
import 'dialog.dart';
import 'slider.dart';
import 'text_field.dart';
import 'text_field_disabled.dart';
import 'text_field_password.dart';
abstract class UseCase {
......@@ -21,11 +19,9 @@ abstract class UseCase {
final List<UseCase> useCases = <UseCase>[
CheckBoxListTile(),
CheckBoxListTileDisabled(),
DialogUseCase(),
SliderUseCase(),
TextFieldUseCase(),
TextFieldDisabledUseCase(),
TextFieldPasswordUseCase(),
DatePickerUseCase(),
];
name: a11y_assessments
description: "A new Flutter project."
description: A new Flutter project
environment:
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';
void main() {
testWidgets('text field password can run', (WidgetTester tester) async {
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
{
final Finder finder = find.byKey(const Key('enabled password'));
await tester.tap(finder);
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'abc');
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';
void main() {
testWidgets('text field can run', (WidgetTester tester) async {
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
{
final Finder finder = find.byKey(const Key('enabled text field'));
await tester.tap(finder);
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'abc');
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 @@
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": ""A new Flutter project."",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"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