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.
This commit is contained in:
parent
30a9f99bc8
commit
d0664bcd79
@ -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 {
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,47 +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: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(
|
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 {
|
|||||||
),
|
),
|
||||||
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 {
|
|||||||
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> {
|
|||||||
),
|
),
|
||||||
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 {
|
|||||||
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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,44 +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: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,
|
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 @@
|
|||||||
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 {
|
|||||||
|
|
||||||
final List<UseCase> useCases = <UseCase>[
|
final List<UseCase> useCases = <UseCase>[
|
||||||
CheckBoxListTile(),
|
CheckBoxListTile(),
|
||||||
CheckBoxListTileDisabled(),
|
|
||||||
DialogUseCase(),
|
DialogUseCase(),
|
||||||
SliderUseCase(),
|
SliderUseCase(),
|
||||||
TextFieldUseCase(),
|
TextFieldUseCase(),
|
||||||
TextFieldDisabledUseCase(),
|
|
||||||
TextFieldPasswordUseCase(),
|
TextFieldPasswordUseCase(),
|
||||||
DatePickerUseCase(),
|
DatePickerUseCase(),
|
||||||
];
|
];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
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'
|
||||||
|
@ -1,23 +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() {
|
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';
|
|||||||
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 @@
|
|||||||
"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": [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user