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

Make UserAccountsDrawerHeader accessible (#13711)

* other accounts a11y; show accounts button a11y

* layout with bigger tap areas; all semantics

* internationalize UserAccountsDrawerHeader a11y labels

* better Russian translation

* break import cycle

* address comments
parent dd796853
......@@ -239,6 +239,18 @@ abstract class MaterialLocalizations {
/// ```
int get firstDayOfWeekIndex;
/// The semantics label used to indicate which account is signed in in the
/// [UserAccountsDrawerHeader] widget.
String get signedInLabel;
/// The semantics label used for the button on [UserAccountsDrawerHeader] that
/// hides the list of accounts.
String get hideAccountsLabel;
/// The semantics label used for the button on [UserAccountsDrawerHeader] that
/// shows the list of accounts.
String get showAccountsLabel;
/// The `MaterialLocalizations` from the closest [Localizations] instance
/// that encloses the given context.
///
......@@ -570,6 +582,15 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
@override
TextTheme get localTextGeometry => MaterialTextGeometry.englishLike;
@override
String get signedInLabel => 'Signed in';
@override
String get hideAccountsLabel => 'Hide accounts';
@override
String get showAccountsLabel => 'Show accounts';
/// Creates an object that provides US English resource values for the material
/// library widgets.
///
......
......@@ -10,6 +10,7 @@ import 'debug.dart';
import 'drawer_header.dart';
import 'icons.dart';
import 'ink_well.dart';
import 'material_localizations.dart';
import 'theme.dart';
class _AccountPictures extends StatelessWidget {
......@@ -35,7 +36,10 @@ class _AccountPictures extends StatelessWidget {
margin: const EdgeInsetsDirectional.only(start: 16.0),
width: 40.0,
height: 40.0,
child: picture
child: new Semantics(
container: true,
child: picture,
),
);
}).toList(),
),
......@@ -45,7 +49,7 @@ class _AccountPictures extends StatelessWidget {
child: new SizedBox(
width: 72.0,
height: 72.0,
child: currentAccountPicture
child: currentAccountPicture,
),
),
],
......@@ -67,62 +71,72 @@ class _AccountDetails extends StatelessWidget {
final VoidCallback onTap;
final bool isOpen;
Widget addDropdownIcon(Widget line) {
final Widget icon = new Icon(
isOpen ? Icons.arrow_drop_up : Icons.arrow_drop_down,
color: Colors.white
);
return new Expanded(
child: new Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: line == null ? <Widget>[icon] : <Widget>[
new Expanded(child: line),
icon,
],
),
);
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
Widget accountNameLine = accountName == null ? null : new DefaultTextStyle(
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final Widget accountNameLine = accountName == null ? null : new DefaultTextStyle(
style: theme.primaryTextTheme.body2,
overflow: TextOverflow.ellipsis,
child: accountName,
);
Widget accountEmailLine = accountEmail == null ? null : new DefaultTextStyle(
final Widget accountEmailLine = accountEmail == null ? null : new DefaultTextStyle(
style: theme.primaryTextTheme.body1,
overflow: TextOverflow.ellipsis,
child: accountEmail,
);
if (onTap != null) {
if (accountEmailLine != null)
accountEmailLine = addDropdownIcon(accountEmailLine);
else
accountNameLine = addDropdownIcon(accountNameLine);
}
Widget accountDetails;
final List<Widget> rowChildren = <Widget>[];
if (accountEmailLine != null || accountNameLine != null) {
accountDetails = new Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: (accountEmailLine != null && accountNameLine != null)
? <Widget>[accountNameLine, accountEmailLine]
: <Widget>[accountNameLine ?? accountEmailLine]
rowChildren.add(
new Expanded(
flex: 1,
child: new Padding(
padding: const EdgeInsets.fromLTRB(16.0, 8.0, 0.0, 8.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: (accountEmailLine != null && accountNameLine != null)
? <Widget>[accountNameLine, accountEmailLine]
: <Widget>[accountNameLine ?? accountEmailLine],
),
),
),
);
}
if (onTap != null)
accountDetails = new InkWell(onTap: onTap, child: accountDetails);
const double kAccountDetailsHeight = 56.0;
if (onTap != null) {
rowChildren.add(
new InkWell(
onTap: onTap,
child: new Semantics(
button: true,
child: new SizedBox(
height: kAccountDetailsHeight,
width: kAccountDetailsHeight, // make it a square
child: new Center(
child: new Icon(
isOpen ? Icons.arrow_drop_up : Icons.arrow_drop_down,
color: Colors.white,
semanticLabel: isOpen
? localizations.hideAccountsLabel
: localizations.showAccountsLabel,
),
),
),
),
),
);
}
return new SizedBox(
height: 56.0,
child: accountDetails,
height: kAccountDetailsHeight,
child: new Row(
children: rowChildren,
),
);
}
}
......@@ -147,7 +161,7 @@ class UserAccountsDrawerHeader extends StatefulWidget {
this.otherAccountsPictures,
@required this.accountName,
@required this.accountEmail,
this.onDetailsPressed
this.onDetailsPressed,
}) : super(key: key);
/// The header's background. If decoration is null then a [BoxDecoration]
......@@ -200,24 +214,32 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
color: Theme.of(context).primaryColor,
),
margin: widget.margin,
padding: EdgeInsets.zero,
child: new SafeArea(
bottom: false,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Expanded(
child: new _AccountPictures(
currentAccountPicture: widget.currentAccountPicture,
otherAccountsPictures: widget.otherAccountsPictures,
)
),
new _AccountDetails(
accountName: widget.accountName,
accountEmail: widget.accountEmail,
isOpen: _isOpen,
onTap: widget.onDetailsPressed == null ? null : _handleDetailsPressed,
),
],
child: new Semantics(
container: true,
label: 'Signed in',
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Expanded(
child: new Padding(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
child: new _AccountPictures(
currentAccountPicture: widget.currentAccountPicture,
otherAccountsPictures: widget.otherAccountsPictures,
),
),
),
new _AccountDetails(
accountName: widget.accountName,
accountEmail: widget.accountEmail,
isOpen: _isOpen,
onTap: widget.onDetailsPressed == null ? null : _handleDetailsPressed,
),
],
),
),
),
);
......
......@@ -4826,7 +4826,7 @@ class Semantics extends SingleChildRenderObjectWidget {
/// more accessible.
final SemanticsProperties properties;
/// If 'container' is true, this widget will introduce a new
/// If [container] is true, this widget will introduce a new
/// node in the semantics tree. Otherwise, the semantics will be
/// merged with the semantics of any ancestors (if the ancestor allows that).
///
......
......@@ -2,58 +2,71 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test/flutter_test.dart' hide TypeMatcher;
void main() {
testWidgets('UserAccountsDrawerHeader test', (WidgetTester tester) async {
final Key avatarA = const Key('A');
final Key avatarC = const Key('C');
final Key avatarD = const Key('D');
await tester.pumpWidget(
new MaterialApp(
home: new MediaQuery(
data: const MediaQueryData(
padding: const EdgeInsets.only(
left: 10.0,
top: 20.0,
right: 30.0,
bottom: 40.0,
),
import '../widgets/semantics_tester.dart';
const Key avatarA = const Key('A');
const Key avatarC = const Key('C');
const Key avatarD = const Key('D');
Future<Null> pumpTestWidget(WidgetTester tester, {
bool withName: true,
bool withEmail: true,
bool withOnDetailsPressedHandler: true,
}) async {
await tester.pumpWidget(
new MaterialApp(
home: new MediaQuery(
data: const MediaQueryData(
padding: const EdgeInsets.only(
left: 10.0,
top: 20.0,
right: 30.0,
bottom: 40.0,
),
child: new Material(
child: new Center(
child: new UserAccountsDrawerHeader(
currentAccountPicture: new CircleAvatar(
key: avatarA,
child: const Text('A'),
),
otherAccountsPictures: <Widget>[
const CircleAvatar(
child: const Text('B'),
),
new CircleAvatar(
key: avatarC,
child: const Text('C'),
),
new CircleAvatar(
key: avatarD,
child: const Text('D'),
),
const CircleAvatar(
child: const Text('E'),
)
],
accountName: const Text('name'),
accountEmail: const Text('email'),
),
child: new Material(
child: new Center(
child: new UserAccountsDrawerHeader(
onDetailsPressed: withOnDetailsPressedHandler ? () {} : null,
currentAccountPicture: const CircleAvatar(
key: avatarA,
child: const Text('A'),
),
otherAccountsPictures: <Widget>[
const CircleAvatar(
child: const Text('B'),
),
const CircleAvatar(
key: avatarC,
child: const Text('C'),
),
const CircleAvatar(
key: avatarD,
child: const Text('D'),
),
const CircleAvatar(
child: const Text('E'),
)
],
accountName: withName ? const Text('name') : null,
accountEmail: withEmail ? const Text('email') : null,
),
),
),
),
);
),
);
}
void main() {
testWidgets('UserAccountsDrawerHeader layout', (WidgetTester tester) async {
await pumpTestWidget(tester);
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
......@@ -126,6 +139,8 @@ void main() {
));
expect(find.byType(Icon), findsOneWidget);
// When either email or account name (but not both!) are present, the icon
// is center aligned with the text displaying the email/name.
await tester.pumpWidget(buildFrame(
accountName: const Text('accountName'),
onDetailsPressed: () { },
......@@ -144,13 +159,16 @@ void main() {
tester.getCenter(find.byType(Icon)).dy
);
// When _both_ email and account name are present, the icon is placed in the
// center of the entire row. It's not aligned with text any more.
await tester.pumpWidget(buildFrame(
accountName: const Text('accountName'),
accountEmail: const Text('accountEmail'),
onDetailsPressed: () { },
));
final RenderFlex row = tester.element(find.text('accountEmail')).ancestorRenderObjectOfType(const TypeMatcher<RenderFlex>());
expect(
tester.getCenter(find.text('accountEmail')).dy,
row.localToGlobal(row.size.center(Offset.zero)).dy,
tester.getCenter(find.byType(Icon)).dy
);
expect(
......@@ -186,4 +204,83 @@ void main() {
greaterThan(tester.getBottomLeft(find.byKey(avatarA)).dy)
);
});
testWidgets('UserAccountsDrawerHeader provides semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await pumpTestWidget(tester);
expect(
semantics,
hasSemantics(
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
label: 'Signed in\nA\nname\nemail',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
new TestSemantics(
label: r'B',
textDirection: TextDirection.ltr,
),
new TestSemantics(
label: r'C',
textDirection: TextDirection.ltr,
),
new TestSemantics(
label: r'D',
textDirection: TextDirection.ltr,
),
new TestSemantics(
flags: <SemanticsFlags>[SemanticsFlags.isButton],
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'Show accounts',
textDirection: TextDirection.ltr,
),
],
),
],
),
ignoreId: true, ignoreTransform: true, ignoreRect: true,
),
);
semantics.dispose();
});
testWidgets('UserAccountsDrawerHeader provides semantics with missing properties', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await pumpTestWidget(
tester,
withEmail: false,
withName: false,
withOnDetailsPressedHandler: false,
);
expect(
semantics,
hasSemantics(
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
label: 'Signed in\nA',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
new TestSemantics(
label: r'B',
textDirection: TextDirection.ltr,
),
new TestSemantics(
label: r'C',
textDirection: TextDirection.ltr,
),
new TestSemantics(
label: r'D',
textDirection: TextDirection.ltr,
),
],
),
],
),
ignoreId: true, ignoreTransform: true, ignoreRect: true,
),
);
semantics.dispose();
});
}
......@@ -34,5 +34,8 @@
"postMeridiemAbbreviation": "م",
"timePickerHourModeAnnouncement": "اختيار الساعات",
"timePickerMinuteModeAnnouncement": "اختيار الدقائق",
"signedInLabel": "تم تسجيل الدخول",
"hideAccountsLabel": "إخفاء الحسابات",
"showAccountsLabel": "عرض الحسابات",
"modalBarrierDismissLabel": "تجاهل"
}
......@@ -31,5 +31,8 @@
"postMeridiemAbbreviation": "NACHM.",
"timePickerHourModeAnnouncement": "Stunden auswählen",
"timePickerMinuteModeAnnouncement": "Minuten auswählen",
"signedInLabel": "Angemeldet",
"hideAccountsLabel": "Konten ausblenden",
"showAccountsLabel": "Konten anzeigen",
"modalBarrierDismissLabel": "Schließen"
}
......@@ -158,5 +158,20 @@
"modalBarrierDismissLabel": "Dismiss",
"@modalBarrierDismissLabel": {
"description": "Label read out by accessibility tools (TalkBack or VocieOver) for a modal barrier to indicate that a tap dismisses the barrier. A modal barrier can for example be found behind a alert or popup to block user interaction with elements behind it."
},
"signedInLabel": "Signed in",
"@signedInLabel": {
"description": "The semantics label used to indicate which account is signed in in UserAccountsDrawerHeader when an accessibility user navigates the UI."
},
"hideAccountsLabel": "Hide accounts",
"@hideAccountsLabel": {
"description": "The semantics label used for the button on UserAccountsDrawerHeader that hides the list of accounts."
},
"showAccountsLabel": "Show accounts",
"@showAccountsLabel": {
"description": "The semantics label used for the button on UserAccountsDrawerHeader that shows the list of accounts."
}
}
......@@ -31,5 +31,8 @@
"postMeridiemAbbreviation": "P.M.",
"timePickerHourModeAnnouncement": "Seleccionar horas",
"timePickerMinuteModeAnnouncement": "Seleccionar minutos",
"signedInLabel": "Registrado",
"hideAccountsLabel": "Ocultar cuentas",
"showAccountsLabel": "Mostrar cuentas",
"modalBarrierDismissLabel": "Ignorar"
}
......@@ -30,5 +30,8 @@
"postMeridiemAbbreviation": "ب.ظ.",
"timePickerHourModeAnnouncement": "انتخاب ساعت",
"timePickerMinuteModeAnnouncement": "انتخاب دقیقه",
"signedInLabel": "وارد شدن",
"hideAccountsLabel": "پنهان کردن حساب ها",
"showAccountsLabel": "نمایش حساب ها",
"modalBarrierDismissLabel": "رد کردن"
}
......@@ -31,5 +31,8 @@
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "Sélectionner une heure",
"timePickerMinuteModeAnnouncement": "Sélectionner des minutes",
"signedInLabel": "Connecté",
"hideAccountsLabel": "Masquer les comptes",
"showAccountsLabel": "Afficher les comptes",
"modalBarrierDismissLabel": "Ignorer"
}
......@@ -32,5 +32,8 @@
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "בחירת שעות",
"timePickerMinuteModeAnnouncement": "בחירת דקות",
"signedInLabel": "מחובר",
"hideAccountsLabel": "הסתר חשבונות",
"showAccountsLabel": "הצג חשבונות",
"modalBarrierDismissLabel": "סגירה"
}
......@@ -30,5 +30,8 @@
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "Seleziona le ore",
"timePickerMinuteModeAnnouncement": "Seleziona i minuti",
"signedInLabel": "Registrato",
"hideAccountsLabel": "Nascondi account",
"showAccountsLabel": "Mostra account",
"modalBarrierDismissLabel": "Ignora"
}
......@@ -30,5 +30,8 @@
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "時間を選択",
"timePickerMinuteModeAnnouncement": "分を選択",
"signedInLabel": "ログイン中",
"hideAccountsLabel": "アカウントを隠す",
"showAccountsLabel": "アカウントを表示する",
"modalBarrierDismissLabel": "閉じる"
}
......@@ -30,5 +30,8 @@
"postMeridiemAbbreviation": "오후",
"timePickerHourModeAnnouncement": "시간 선택",
"timePickerMinuteModeAnnouncement": "분 선택",
"signedInLabel": "로그인 함",
"hideAccountsLabel": "계정 숨기기",
"showAccountsLabel": "계정 표시",
"modalBarrierDismissLabel": "버리다"
}
......@@ -30,5 +30,8 @@
"postMeridiemAbbreviation": "pm",
"timePickerHourModeAnnouncement": "Uren selecteren",
"timePickerMinuteModeAnnouncement": "Minuten selecteren",
"signedInLabel": "Ingelogd",
"hideAccountsLabel": "Verberg accounts",
"showAccountsLabel": "Toon accounts",
"modalBarrierDismissLabel": "ontslaan"
}
......@@ -32,5 +32,8 @@
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "Wybierz godziny",
"timePickerMinuteModeAnnouncement": "Wybierz minuty",
"signedInLabel": "Zapisany",
"hideAccountsLabel": "Ukryj konta",
"showAccountsLabel": "Pokaż konta",
"modalBarrierDismissLabel": "oddalić"
}
......@@ -29,5 +29,8 @@
"viewLicensesButtonLabel": "لیدلس وګورئ",
"timePickerHourModeAnnouncement": "وختونه وټاکئ",
"timePickerMinuteModeAnnouncement": "منې غوره کړئ",
"signedInLabel": "ننوتل",
"hideAccountsLabel": "حسابونه پټ کړئ",
"showAccountsLabel": "حسابونه ښکاره کړئ",
"modalBarrierDismissLabel": "رد کړه"
}
......@@ -32,5 +32,8 @@
"viewLicensesButtonLabel": "VER LICENÇAS",
"timePickerHourModeAnnouncement": "Selecione as horas",
"timePickerMinuteModeAnnouncement": "Selecione os minutos",
"signedInLabel": "Assinado",
"hideAccountsLabel": "Ocultar contas",
"showAccountsLabel": "Mostrar contas",
"modalBarrierDismissLabel": "Dispensar"
}
......@@ -33,5 +33,8 @@
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "Выберите часы",
"timePickerMinuteModeAnnouncement": "Выберите минуты",
"signedInLabel": "Залогинен",
"hideAccountsLabel": "Спрятать аккаунты",
"showAccountsLabel": "Показать аккаунты",
"modalBarrierDismissLabel": "Закрыть"
}
......@@ -30,5 +30,8 @@
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "เลือกชั่วโมง",
"timePickerMinuteModeAnnouncement": "เลือกนาที",
"signedInLabel": "ลงทะเบียนเข้า",
"hideAccountsLabel": "ซ่อนบัญชี",
"showAccountsLabel": "แสดงบัญชี",
"modalBarrierDismissLabel": "ยกเลิก"
}
......@@ -30,5 +30,8 @@
"postMeridiemAbbreviation": "ÖS",
"timePickerHourModeAnnouncement": "Saati seçin",
"timePickerMinuteModeAnnouncement": "Dakikayı seçin",
"signedInLabel": "Yetkili",
"hideAccountsLabel": "Hesapları gizle",
"showAccountsLabel": "Hesapları göster",
"modalBarrierDismissLabel": "Reddet"
}
......@@ -30,5 +30,8 @@
"postMeridiemAbbreviation": "PM",
"timePickerHourModeAnnouncement": "گھنٹے منتخب کریں",
"timePickerMinuteModeAnnouncement": "منٹ منتخب کریں",
"signedInLabel": "سائن ان",
"hideAccountsLabel": "اکاؤنٹس چھپائیں",
"showAccountsLabel": "اکاؤنٹس دکھائیں",
"modalBarrierDismissLabel": "برطرف"
}
......@@ -30,5 +30,8 @@
"postMeridiemAbbreviation": "下午",
"timePickerHourModeAnnouncement": "选择小时",
"timePickerMinuteModeAnnouncement": "选择分钟",
"signedInLabel": "登录",
"hideAccountsLabel": "隐藏帐户",
"showAccountsLabel": "显示帐户",
"modalBarrierDismissLabel": "关闭"
}
......@@ -334,6 +334,15 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
@override
String get modalBarrierDismissLabel => _translationBundle.modalBarrierDismissLabel;
@override
String get signedInLabel => _translationBundle.signedInLabel;
@override
String get hideAccountsLabel => _translationBundle.hideAccountsLabel;
@override
String get showAccountsLabel => _translationBundle.showAccountsLabel;
/// The [TimeOfDayFormat] corresponding to one of the following supported
/// patterns:
///
......
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