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
|
||||
// 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,16 +30,29 @@ class _MainWidgetState extends State<_MainWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('CheckBoxListTile')),
|
||||
body: Center(
|
||||
child: CheckboxListTile(
|
||||
value: _checked,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
_checked = value!;
|
||||
});
|
||||
},
|
||||
title: const Text('a check box list title'),
|
||||
),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
CheckboxListTile(
|
||||
autofocus: true,
|
||||
value: _checked,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
_checked = value!;
|
||||
});
|
||||
},
|
||||
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(
|
||||
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(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
suffixText: '@gmail.com',
|
||||
hintText: 'Enter your email',
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
title: const Text('TextField password'),
|
||||
),
|
||||
body: const Center(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
hintText: 'Enter your password',
|
||||
body: ListView(
|
||||
children: const <Widget>[
|
||||
TextField(
|
||||
key: Key('enabled 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 '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(),
|
||||
];
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: a11y_assessments
|
||||
description: "A new Flutter project."
|
||||
description: A new Flutter project
|
||||
|
||||
environment:
|
||||
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() {
|
||||
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));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), 'abc');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('abc'), findsOneWidget);
|
||||
// Test the enabled password
|
||||
{
|
||||
final Finder finder = find.byKey(const Key('enabled password'));
|
||||
await tester.tap(finder);
|
||||
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() {
|
||||
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));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), 'abc');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('abc'), findsOneWidget);
|
||||
// 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(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": [
|
||||
|
Loading…
x
Reference in New Issue
Block a user