Unverified Commit 94f48c2c authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Make UserAccountsDrawerHeader accessible (#13851)

Fixes #13743 
Fixes #12379
Follow-up to #13745

Also adds an option to hide gestures introduced by `InkWell` and `InkResponse` from the semantics tree (see also `GestureDetector.excludeFromSemantics`).
parent f4040455
......@@ -100,19 +100,35 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
package: _kGalleryAssetsPackage,
),
),
otherAccountsPictures: const <Widget>[
const CircleAvatar(
otherAccountsPictures: <Widget>[
new GestureDetector(
onTap: () {
_onOtherAccountsTap(context);
},
child: new Semantics(
label: 'Switch to Account B',
child: const CircleAvatar(
backgroundImage: const AssetImage(
_kAsset1,
package: _kGalleryAssetsPackage,
),
),
const CircleAvatar(
),
),
new GestureDetector(
onTap: () {
_onOtherAccountsTap(context);
},
child: new Semantics(
label: 'Switch to Account C',
child: const CircleAvatar(
backgroundImage: const AssetImage(
_kAsset2,
package: _kGalleryAssetsPackage,
),
),
),
),
],
margin: EdgeInsets.zero,
onDetailsPressed: () {
......@@ -213,4 +229,21 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
),
);
}
void _onOtherAccountsTap(BuildContext context) {
showDialog<Null>(
context: context,
child: new AlertDialog(
title: const Text('Account switching not implemented.'),
actions: <Widget>[
new FlatButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
);
}
}
......@@ -96,6 +96,7 @@ class InkResponse extends StatefulWidget {
this.highlightColor,
this.splashColor,
this.enableFeedback: true,
this.excludeFromSemantics: false,
}) : assert(enableFeedback != null), super(key: key);
/// The widget below this widget in the tree.
......@@ -194,6 +195,15 @@ class InkResponse extends StatefulWidget {
/// * [Feedback] for providing platform-specific feedback to certain actions.
final bool enableFeedback;
/// Whether to exclude the gestures introduced by this widget from the
/// semantics tree.
///
/// For example, a long-press gesture for showing a tooltip is usually
/// excluded because the tooltip itself is included in the semantics
/// tree directly and so having a gesture to show it would result in
/// duplication of information.
final bool excludeFromSemantics;
/// The rectangle to use for the highlight effect and for clipping
/// the splash effects if [containedInkWell] is true.
///
......@@ -379,7 +389,8 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null,
behavior: HitTestBehavior.opaque,
child: widget.child
child: widget.child,
excludeFromSemantics: widget.excludeFromSemantics,
);
}
......@@ -427,6 +438,7 @@ class InkWell extends InkResponse {
Color splashColor,
BorderRadius borderRadius,
bool enableFeedback: true,
bool excludeFromSemantics: false,
}) : super(
key: key,
child: child,
......@@ -440,5 +452,6 @@ class InkWell extends InkResponse {
splashColor: splashColor,
borderRadius: borderRadius,
enableFeedback: enableFeedback,
excludeFromSemantics: excludeFromSemantics,
);
}
......@@ -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 {
......@@ -31,23 +32,29 @@ class _AccountPictures extends StatelessWidget {
end: 0.0,
child: new Row(
children: (otherAccountsPictures ?? <Widget>[]).take(3).map((Widget picture) {
return new Container(
return new Semantics(
explicitChildNodes: true,
child: new Container(
margin: const EdgeInsetsDirectional.only(start: 16.0),
width: 40.0,
height: 40.0,
child: picture
),
);
}).toList(),
),
),
new Positioned(
top: 0.0,
child: new Semantics(
explicitChildNodes: true,
child: new SizedBox(
width: 72.0,
height: 72.0,
child: currentAccountPicture
),
),
),
],
);
}
......@@ -67,66 +74,170 @@ 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) {
assert(debugCheckHasDirectionality(context));
final ThemeData theme = Theme.of(context);
Widget accountNameLine = accountName == null ? null : new DefaultTextStyle(
final List<Widget> children = <Widget>[];
if (accountName != null) {
final Widget accountNameLine = new LayoutId(
id: _AccountDetailsLayout.accountName,
child: new Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: new DefaultTextStyle(
style: theme.primaryTextTheme.body2,
overflow: TextOverflow.ellipsis,
child: accountName,
),
),
);
Widget accountEmailLine = accountEmail == null ? null : new DefaultTextStyle(
children.add(accountNameLine);
}
if (accountEmail != null) {
final Widget accountEmailLine = new LayoutId(
id: _AccountDetailsLayout.accountEmail,
child: new Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: new DefaultTextStyle(
style: theme.primaryTextTheme.body1,
overflow: TextOverflow.ellipsis,
child: accountEmail,
),
),
);
if (onTap != null) {
if (accountEmailLine != null)
accountEmailLine = addDropdownIcon(accountEmailLine);
else
accountNameLine = addDropdownIcon(accountNameLine);
children.add(accountEmailLine);
}
Widget accountDetails;
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]
if (onTap != null) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final Widget dropDownIcon = new LayoutId(
id: _AccountDetailsLayout.dropdownIcon,
child: new Semantics(
container: true,
button: true,
onTap: onTap,
child: new SizedBox(
height: _kAccountDetailsHeight,
width: _kAccountDetailsHeight,
child: new Center(
child: new Icon(
isOpen ? Icons.arrow_drop_up : Icons.arrow_drop_down,
color: Colors.white,
semanticLabel: isOpen
? localizations.hideAccountsLabel
: localizations.showAccountsLabel,
),
),
),
),
);
children.add(dropDownIcon);
}
if (onTap != null)
accountDetails = new InkWell(onTap: onTap, child: accountDetails);
Widget accountDetails = new CustomMultiChildLayout(
delegate: new _AccountDetailsLayout(
textDirection: Directionality.of(context),
),
children: children,
);
if (onTap != null) {
accountDetails = new InkWell(
onTap: onTap,
child: accountDetails,
excludeFromSemantics: true,
);
}
return new SizedBox(
height: 56.0,
height: _kAccountDetailsHeight,
child: accountDetails,
);
}
}
const double _kAccountDetailsHeight = 56.0;
class _AccountDetailsLayout extends MultiChildLayoutDelegate {
_AccountDetailsLayout({ @required this.textDirection });
static final String accountName = 'accountName';
static final String accountEmail = 'accountEmail';
static final String dropdownIcon = 'dropdownIcon';
final TextDirection textDirection;
@override
void performLayout(Size size) {
Size iconSize;
if (hasChild(dropdownIcon)) {
// place the dropdown icon in bottom right (LTR) or bottom left (RTL)
iconSize = layoutChild(dropdownIcon, new BoxConstraints.loose(size));
positionChild(dropdownIcon, _offsetForIcon(size, iconSize));
}
final String bottomLine = hasChild(accountEmail) ? accountEmail : (hasChild(accountName) ? accountName : null);
if (bottomLine != null) {
final Size constraintSize = iconSize == null ? size : size - new Offset(iconSize.width, 0.0);
iconSize ??= const Size(_kAccountDetailsHeight, _kAccountDetailsHeight);
// place bottom line center at same height as icon center
final Size bottomLineSize = layoutChild(bottomLine, new BoxConstraints.loose(constraintSize));
final Offset bottomLineOffset = _offsetForBottomLine(size, iconSize, bottomLineSize);
positionChild(bottomLine, bottomLineOffset);
// place account name above account email
if (bottomLine == accountEmail && hasChild(accountName)) {
final Size nameSize = layoutChild(accountName, new BoxConstraints.loose(constraintSize));
positionChild(accountName, _offsetForName(size, nameSize, bottomLineOffset));
}
}
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true;
Offset _offsetForIcon(Size size, Size iconSize) {
switch (textDirection) {
case TextDirection.ltr:
return new Offset(size.width - iconSize.width, size.height - iconSize.height);
case TextDirection.rtl:
return new Offset(0.0, size.height - iconSize.height);
}
assert(false, 'Unreachable');
return null;
}
Offset _offsetForBottomLine(Size size, Size iconSize, Size bottomLineSize) {
final double y = size.height - 0.5 * iconSize.height - 0.5 * bottomLineSize.height;
switch (textDirection) {
case TextDirection.ltr:
return new Offset(0.0, y);
case TextDirection.rtl:
return new Offset(size.width - bottomLineSize.width, y);
}
assert(false, 'Unreachable');
return null;
}
Offset _offsetForName(Size size, Size nameSize, Offset bottomLineOffset) {
final double y = bottomLineOffset.dy - nameSize.height;
switch (textDirection) {
case TextDirection.ltr:
return new Offset(0.0, y);
case TextDirection.rtl:
return new Offset(size.width - nameSize.width, y);
}
assert(false, 'Unreachable');
return null;
}
}
/// A material design [Drawer] header that identifies the app's user.
///
/// Requires one of its ancestors to be a [Material] widget.
......@@ -195,20 +306,27 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
return new DrawerHeader(
return new Semantics(
container: true,
label: MaterialLocalizations.of(context).signedInLabel,
child: new DrawerHeader(
decoration: widget.decoration ?? new BoxDecoration(
color: Theme.of(context).primaryColor,
),
margin: widget.margin,
padding: const EdgeInsetsDirectional.only(top: 16.0, start: 16.0),
child: new SafeArea(
bottom: false,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Expanded(
child: new Padding(
padding: const EdgeInsetsDirectional.only(end: 16.0),
child: new _AccountPictures(
currentAccountPicture: widget.currentAccountPicture,
otherAccountsPictures: widget.otherAccountsPictures,
),
)
),
new _AccountDetails(
......@@ -220,6 +338,7 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
],
),
),
),
);
}
}
......@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
void main() {
......@@ -156,4 +157,33 @@ void main() {
await runTest(true);
await runTest(false);
});
testWidgets('excludeFromSemantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new InkWell(
onTap: () { },
child: const Text('Button'),
),
),
));
expect(semantics, includesNodeWith(label: 'Button', actions: <SemanticsAction>[SemanticsAction.tap]));
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new InkWell(
onTap: () { },
child: const Text('Button'),
excludeFromSemantics: true,
),
),
));
expect(semantics, isNot(includesNodeWith(label: 'Button', actions: <SemanticsAction>[SemanticsAction.tap])));
semantics.dispose();
});
}
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