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 { ...@@ -100,19 +100,35 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
package: _kGalleryAssetsPackage, package: _kGalleryAssetsPackage,
), ),
), ),
otherAccountsPictures: const <Widget>[ otherAccountsPictures: <Widget>[
const CircleAvatar( new GestureDetector(
onTap: () {
_onOtherAccountsTap(context);
},
child: new Semantics(
label: 'Switch to Account B',
child: const CircleAvatar(
backgroundImage: const AssetImage( backgroundImage: const AssetImage(
_kAsset1, _kAsset1,
package: _kGalleryAssetsPackage, package: _kGalleryAssetsPackage,
), ),
), ),
const CircleAvatar( ),
),
new GestureDetector(
onTap: () {
_onOtherAccountsTap(context);
},
child: new Semantics(
label: 'Switch to Account C',
child: const CircleAvatar(
backgroundImage: const AssetImage( backgroundImage: const AssetImage(
_kAsset2, _kAsset2,
package: _kGalleryAssetsPackage, package: _kGalleryAssetsPackage,
), ),
), ),
),
),
], ],
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
onDetailsPressed: () { onDetailsPressed: () {
...@@ -213,4 +229,21 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin { ...@@ -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 { ...@@ -96,6 +96,7 @@ class InkResponse extends StatefulWidget {
this.highlightColor, this.highlightColor,
this.splashColor, this.splashColor,
this.enableFeedback: true, this.enableFeedback: true,
this.excludeFromSemantics: false,
}) : assert(enableFeedback != null), super(key: key); }) : assert(enableFeedback != null), super(key: key);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
...@@ -194,6 +195,15 @@ class InkResponse extends StatefulWidget { ...@@ -194,6 +195,15 @@ class InkResponse extends StatefulWidget {
/// * [Feedback] for providing platform-specific feedback to certain actions. /// * [Feedback] for providing platform-specific feedback to certain actions.
final bool enableFeedback; 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 rectangle to use for the highlight effect and for clipping
/// the splash effects if [containedInkWell] is true. /// the splash effects if [containedInkWell] is true.
/// ///
...@@ -379,7 +389,8 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe ...@@ -379,7 +389,8 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null, onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null, onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: widget.child child: widget.child,
excludeFromSemantics: widget.excludeFromSemantics,
); );
} }
...@@ -427,6 +438,7 @@ class InkWell extends InkResponse { ...@@ -427,6 +438,7 @@ class InkWell extends InkResponse {
Color splashColor, Color splashColor,
BorderRadius borderRadius, BorderRadius borderRadius,
bool enableFeedback: true, bool enableFeedback: true,
bool excludeFromSemantics: false,
}) : super( }) : super(
key: key, key: key,
child: child, child: child,
...@@ -440,5 +452,6 @@ class InkWell extends InkResponse { ...@@ -440,5 +452,6 @@ class InkWell extends InkResponse {
splashColor: splashColor, splashColor: splashColor,
borderRadius: borderRadius, borderRadius: borderRadius,
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
excludeFromSemantics: excludeFromSemantics,
); );
} }
...@@ -10,6 +10,7 @@ import 'debug.dart'; ...@@ -10,6 +10,7 @@ import 'debug.dart';
import 'drawer_header.dart'; import 'drawer_header.dart';
import 'icons.dart'; import 'icons.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'material_localizations.dart';
import 'theme.dart'; import 'theme.dart';
class _AccountPictures extends StatelessWidget { class _AccountPictures extends StatelessWidget {
...@@ -31,23 +32,29 @@ class _AccountPictures extends StatelessWidget { ...@@ -31,23 +32,29 @@ class _AccountPictures extends StatelessWidget {
end: 0.0, end: 0.0,
child: new Row( child: new Row(
children: (otherAccountsPictures ?? <Widget>[]).take(3).map((Widget picture) { 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), margin: const EdgeInsetsDirectional.only(start: 16.0),
width: 40.0, width: 40.0,
height: 40.0, height: 40.0,
child: picture child: picture
),
); );
}).toList(), }).toList(),
), ),
), ),
new Positioned( new Positioned(
top: 0.0, top: 0.0,
child: new Semantics(
explicitChildNodes: true,
child: new SizedBox( child: new SizedBox(
width: 72.0, width: 72.0,
height: 72.0, height: 72.0,
child: currentAccountPicture child: currentAccountPicture
), ),
), ),
),
], ],
); );
} }
...@@ -67,66 +74,170 @@ class _AccountDetails extends StatelessWidget { ...@@ -67,66 +74,170 @@ class _AccountDetails extends StatelessWidget {
final VoidCallback onTap; final VoidCallback onTap;
final bool isOpen; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final ThemeData theme = Theme.of(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, style: theme.primaryTextTheme.body2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
child: accountName, 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, style: theme.primaryTextTheme.body1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
child: accountEmail, child: accountEmail,
),
),
); );
if (onTap != null) { children.add(accountEmailLine);
if (accountEmailLine != null)
accountEmailLine = addDropdownIcon(accountEmailLine);
else
accountNameLine = addDropdownIcon(accountNameLine);
} }
Widget accountDetails; if (onTap != null) {
if (accountEmailLine != null || accountNameLine != null) { final MaterialLocalizations localizations = MaterialLocalizations.of(context);
accountDetails = new Padding( final Widget dropDownIcon = new LayoutId(
padding: const EdgeInsets.symmetric(vertical: 8.0), id: _AccountDetailsLayout.dropdownIcon,
child: new Column( child: new Semantics(
crossAxisAlignment: CrossAxisAlignment.start, container: true,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, button: true,
children: (accountEmailLine != null && accountNameLine != null) onTap: onTap,
? <Widget>[accountNameLine, accountEmailLine] child: new SizedBox(
: <Widget>[accountNameLine ?? accountEmailLine] 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) Widget accountDetails = new CustomMultiChildLayout(
accountDetails = new InkWell(onTap: onTap, child: accountDetails); delegate: new _AccountDetailsLayout(
textDirection: Directionality.of(context),
),
children: children,
);
if (onTap != null) {
accountDetails = new InkWell(
onTap: onTap,
child: accountDetails,
excludeFromSemantics: true,
);
}
return new SizedBox( return new SizedBox(
height: 56.0, height: _kAccountDetailsHeight,
child: accountDetails, 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. /// A material design [Drawer] header that identifies the app's user.
/// ///
/// Requires one of its ancestors to be a [Material] widget. /// Requires one of its ancestors to be a [Material] widget.
...@@ -195,20 +306,27 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> { ...@@ -195,20 +306,27 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(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( decoration: widget.decoration ?? new BoxDecoration(
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
), ),
margin: widget.margin, margin: widget.margin,
padding: const EdgeInsetsDirectional.only(top: 16.0, start: 16.0),
child: new SafeArea( child: new SafeArea(
bottom: false, bottom: false,
child: new Column( child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Expanded( new Expanded(
child: new Padding(
padding: const EdgeInsetsDirectional.only(end: 16.0),
child: new _AccountPictures( child: new _AccountPictures(
currentAccountPicture: widget.currentAccountPicture, currentAccountPicture: widget.currentAccountPicture,
otherAccountsPictures: widget.otherAccountsPictures, otherAccountsPictures: widget.otherAccountsPictures,
),
) )
), ),
new _AccountDetails( new _AccountDetails(
...@@ -220,6 +338,7 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> { ...@@ -220,6 +338,7 @@ class _UserAccountsDrawerHeaderState extends State<UserAccountsDrawerHeader> {
], ],
), ),
), ),
),
); );
} }
} }
...@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
void main() { void main() {
...@@ -156,4 +157,33 @@ void main() { ...@@ -156,4 +157,33 @@ void main() {
await runTest(true); await runTest(true);
await runTest(false); 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