From 9d38db421c20db1571ebdffe4abc580ff2488016 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Tue, 18 Jun 2019 15:24:34 -0700 Subject: [PATCH] fix ExpansionPanelList merge the header semantics when it is not necessart (#33808) --- .../lib/src/material/expansion_panel.dart | 51 +++-- .../test/material/expansion_panel_test.dart | 181 +++++++++++++++++- 2 files changed, 206 insertions(+), 26 deletions(-) diff --git a/packages/flutter/lib/src/material/expansion_panel.dart b/packages/flutter/lib/src/material/expansion_panel.dart index 37f3150181c..8e2fdeb9929 100644 --- a/packages/flutter/lib/src/material/expansion_panel.dart +++ b/packages/flutter/lib/src/material/expansion_panel.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'expand_icon.dart'; import 'ink_well.dart'; +import 'material_localizations.dart'; import 'mergeable_material.dart'; import 'theme.dart'; @@ -446,7 +447,26 @@ class _ExpansionPanelListState extends State { context, _isChildExpanded(index), ); - final Row header = Row( + + Widget expandIconContainer = Container( + margin: const EdgeInsetsDirectional.only(end: 8.0), + child: ExpandIcon( + isExpanded: _isChildExpanded(index), + padding: const EdgeInsets.all(16.0), + onPressed: !child.canTapOnHeader + ? (bool isExpanded) => _handlePressed(isExpanded, index) + : null, + ), + ); + if (!child.canTapOnHeader) { + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + expandIconContainer = Semantics( + label: _isChildExpanded(index)? localizations.expandedIconTapHint : localizations.collapsedIconTapHint, + container: true, + child: expandIconContainer + ); + } + Widget header = Row( children: [ Expanded( child: AnimatedContainer( @@ -459,32 +479,23 @@ class _ExpansionPanelListState extends State { ), ), ), - Container( - margin: const EdgeInsetsDirectional.only(end: 8.0), - child: ExpandIcon( - isExpanded: _isChildExpanded(index), - padding: const EdgeInsets.all(16.0), - onPressed: !child.canTapOnHeader - ? (bool isExpanded) => _handlePressed(isExpanded, index) - : null, - ), - ), + expandIconContainer, ], ); - + if (child.canTapOnHeader) { + header = MergeSemantics( + child: InkWell( + onTap: () => _handlePressed(_isChildExpanded(index), index), + child: header, + ) + ); + } items.add( MaterialSlice( key: _SaltedKey(context, index * 2), child: Column( children: [ - MergeSemantics( - child: child.canTapOnHeader - ? InkWell( - onTap: () => _handlePressed(_isChildExpanded(index), index), - child: header, - ) - : header, - ), + header, AnimatedCrossFade( firstChild: Container(height: 0.0), secondChild: child.body, diff --git a/packages/flutter/test/material/expansion_panel_test.dart b/packages/flutter/test/material/expansion_panel_test.dart index c7154ff7356..10efb550736 100644 --- a/packages/flutter/test/material/expansion_panel_test.dart +++ b/packages/flutter/test/material/expansion_panel_test.dart @@ -54,6 +54,49 @@ class _SimpleExpansionPanelListTestWidgetState extends State ExpansionPanelListSemanticsTestState(); +} + +class ExpansionPanelListSemanticsTestState extends State { + bool headerTapped = false; + @override + Widget build(BuildContext context) { + return ListView( + children: [ + ExpansionPanelList( + children: [ + ExpansionPanel( + canTapOnHeader: false, + headerBuilder: (BuildContext context, bool isExpanded) { + return MergeSemantics( + key: widget.headerKey, + child: GestureDetector( + onTap: () => headerTapped = true, + child: const Text.rich( + TextSpan( + text:'head1', + ), + ), + ), + ); + }, + body: Container( + child: const Placeholder(), + ), + ), + ], + ), + ], + ); + } +} + void main() { testWidgets('ExpansionPanelList test', (WidgetTester tester) async { int index; @@ -91,7 +134,7 @@ void main() { box = tester.renderObject(find.byType(ExpansionPanelList)); expect(box.size.height, equals(oldHeight)); - // now expand the child panel + // Now, expand the child panel. await tester.pumpWidget( MaterialApp( home: SingleChildScrollView( @@ -121,6 +164,52 @@ void main() { expect(box.size.height - oldHeight, greaterThanOrEqualTo(100.0)); // 100 + some margin }); + testWidgets('ExpansionPanelList does not merge header when canTapOnHeader is false', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + final Key headerKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: ExpansionPanelListSemanticsTest(headerKey: headerKey), + ), + ); + + // Make sure custom gesture detector widget is clickable. + await tester.tap(find.text('head1')); + await tester.pump(); + + final ExpansionPanelListSemanticsTestState state = + tester.state(find.byType(ExpansionPanelListSemanticsTest)); + expect(state.headerTapped, true); + + // Check the expansion icon semantics does not merged with header widget. + final Finder expansionIcon = find.descendant( + of: find.ancestor( + of: find.byKey(headerKey), + matching: find.byType(Row), + ), + matching: find.byType(ExpandIcon), + ); + expect(tester.getSemantics(expansionIcon), matchesSemantics( + label: 'Expand', + isButton: true, + hasEnabledState: true, + isEnabled: true, + hasTapAction: true, + )); + + // Check custom header widget semantics is preserved. + final Finder headerWidget = find.descendant( + of: find.byKey(headerKey), + matching: find.byType(RichText), + ); + expect(tester.getSemantics(headerWidget), matchesSemantics( + label: 'head1', + hasTapAction: true, + )); + + handle.dispose(); + }); + testWidgets('Multiple Panel List test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -798,7 +887,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Panel header has semantics', (WidgetTester tester) async { + testWidgets('Panel header has semantics, canTapOnHeader = false ', (WidgetTester tester) async { const Key expandedKey = Key('expanded'); const Key collapsedKey = Key('collapsed'); const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); @@ -832,8 +921,16 @@ void main() { ), ); - expect(tester.getSemantics(find.byKey(expandedKey)), matchesSemantics( - label: 'Expanded', + // Check the semantics of [ExpanIcon] for expanded panel. + final Finder expandedIcon = find.descendant( + of: find.ancestor( + of: find.byKey(expandedKey), + matching: find.byType(Row), + ), + matching: find.byType(ExpandIcon), + ); + expect(tester.getSemantics(expandedIcon), matchesSemantics( + label: 'Collapse', isButton: true, hasEnabledState: true, isEnabled: true, @@ -841,8 +938,22 @@ void main() { onTapHint: localizations.expandedIconTapHint, )); - expect(tester.getSemantics(find.byKey(collapsedKey)), matchesSemantics( - label: 'Collapsed', + // Check the semantics of the header widget for expanded panel. + final Finder expandedHeader = find.byKey(expandedKey); + expect(tester.getSemantics(expandedHeader), matchesSemantics( + label: 'Expanded', + )); + + // Check the semantics of [ExpanIcon] for collapsed panel. + final Finder collapsedIcon = find.descendant( + of: find.ancestor( + of: find.byKey(collapsedKey), + matching: find.byType(Row), + ), + matching: find.byType(ExpandIcon), + ); + expect(tester.getSemantics(collapsedIcon), matchesSemantics( + label: 'Expand', isButton: true, hasEnabledState: true, isEnabled: true, @@ -850,6 +961,64 @@ void main() { onTapHint: localizations.collapsedIconTapHint, )); + // Check the semantics of the header widget for expanded panel. + final Finder collapsedHeader = find.byKey(collapsedKey); + expect(tester.getSemantics(collapsedHeader), matchesSemantics( + label: 'Collapsed', + )); + + handle.dispose(); + }); + + testWidgets('Panel header has semantics, canTapOnHeader = true', (WidgetTester tester) async { + const Key expandedKey = Key('expanded'); + const Key collapsedKey = Key('collapsed'); + final SemanticsHandle handle = tester.ensureSemantics(); + final List _demoItems = [ + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return const Text('Expanded', key: expandedKey); + }, + canTapOnHeader: true, + body: const SizedBox(height: 100.0), + isExpanded: true, + ), + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return const Text('Collapsed', key: collapsedKey); + }, + canTapOnHeader: true, + body: const SizedBox(height: 100.0), + isExpanded: false, + ), + ]; + + final ExpansionPanelList _expansionList = ExpansionPanelList( + children: _demoItems, + ); + + await tester.pumpWidget( + MaterialApp( + home: SingleChildScrollView( + child: _expansionList, + ), + ), + ); + + expect(tester.getSemantics(find.byKey(expandedKey)), matchesSemantics( + label: 'Expanded', + isButton: true, + hasEnabledState: true, + hasTapAction: true, + )); + + expect(tester.getSemantics(find.byKey(collapsedKey)), matchesSemantics( + label: 'Collapsed', + isButton: true, + hasEnabledState: true, + hasTapAction: true, + )); + handle.dispose(); });