Commit 2e6edaf4 authored by Efthymis Sarmpanis's avatar Efthymis Sarmpanis Committed by Shi-Hao Hong

Adds Tap Header Feature to ExpansionPanelList (#29390)

parent beffb248
......@@ -6,6 +6,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'expand_icon.dart';
import 'ink_well.dart';
import 'mergeable_material.dart';
import 'theme.dart';
......@@ -71,9 +72,11 @@ class ExpansionPanel {
@required this.headerBuilder,
@required this.body,
this.isExpanded = false,
this.canTapOnHeader = false,
}) : assert(headerBuilder != null),
assert(body != null),
assert(isExpanded != null);
assert(isExpanded != null),
assert(canTapOnHeader != null);
/// The widget builder that builds the expansion panels' header.
final ExpansionPanelHeaderBuilder headerBuilder;
......@@ -88,6 +91,11 @@ class ExpansionPanel {
/// Defaults to false.
final bool isExpanded;
/// Whether tapping on the panel's header will expand/collapse it.
///
/// Defaults to false.
final bool canTapOnHeader;
}
/// An expansion panel that allows for radio-like functionality.
......@@ -109,8 +117,13 @@ class ExpansionPanelRadio extends ExpansionPanel {
@required this.value,
@required ExpansionPanelHeaderBuilder headerBuilder,
@required Widget body,
bool canTapOnHeader = false,
}) : assert(value != null),
super(body: body, headerBuilder: headerBuilder);
super(
body: body,
headerBuilder: headerBuilder,
canTapOnHeader: canTapOnHeader,
);
/// The value that uniquely identifies a radio panel so that the currently
/// selected radio panel can be identified.
......@@ -406,6 +419,10 @@ class _ExpansionPanelListState extends State<ExpansionPanelList> {
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));
final ExpansionPanel child = widget.children[index];
final Widget headerWidget = child.headerBuilder(
context,
_isChildExpanded(index),
);
final Row header = Row(
children: <Widget>[
Expanded(
......@@ -415,10 +432,7 @@ class _ExpansionPanelListState extends State<ExpansionPanelList> {
margin: _isChildExpanded(index) ? kExpandedEdgeInsets : EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: _kPanelHeaderCollapsedHeight),
child: child.headerBuilder(
context,
_isChildExpanded(index),
),
child: headerWidget,
),
),
),
......@@ -427,7 +441,9 @@ class _ExpansionPanelListState extends State<ExpansionPanelList> {
child: ExpandIcon(
isExpanded: _isChildExpanded(index),
padding: const EdgeInsets.all(16.0),
onPressed: (bool isExpanded) => _handlePressed(isExpanded, index),
onPressed: !child.canTapOnHeader
? (bool isExpanded) => _handlePressed(isExpanded, index)
: null,
),
),
],
......@@ -438,7 +454,14 @@ class _ExpansionPanelListState extends State<ExpansionPanelList> {
key: _SaltedKey<BuildContext, int>(context, index * 2),
child: Column(
children: <Widget>[
MergeSemantics(child: header),
MergeSemantics(
child: child.canTapOnHeader
? InkWell(
onTap: () => _handlePressed(_isChildExpanded(index), index),
child: header,
)
: header,
),
AnimatedCrossFade(
firstChild: Container(height: 0.0),
secondChild: child.body,
......
......@@ -5,6 +5,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class SimpleExpansionPanelListTestWidget extends StatefulWidget {
const SimpleExpansionPanelListTestWidget({
Key key,
this.firstPanelKey,
this.secondPanelKey,
this.canTapOnHeader = false
}) : super(key: key);
final Key firstPanelKey;
final Key secondPanelKey;
final bool canTapOnHeader;
@override
_SimpleExpansionPanelListTestWidgetState createState() => _SimpleExpansionPanelListTestWidgetState();
}
class _SimpleExpansionPanelListTestWidgetState extends State<SimpleExpansionPanelListTestWidget> {
List<bool> extendedState = <bool>[false, false];
@override
Widget build(BuildContext context) {
return ExpansionPanelList(
expansionCallback: (int _index, bool _isExpanded) {
setState(() {
extendedState[_index] = !extendedState[_index];
});
},
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A', key: widget.firstPanelKey);
},
body: const SizedBox(height: 100.0),
canTapOnHeader: widget.canTapOnHeader,
isExpanded: extendedState[0],
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C', key: widget.secondPanelKey);
},
body: const SizedBox(height: 100.0),
canTapOnHeader: widget.canTapOnHeader,
isExpanded: extendedState[1],
),
],
);
}
}
void main() {
testWidgets('ExpansionPanelList test', (WidgetTester tester) async {
int index;
......@@ -402,4 +451,230 @@ void main() {
handle.dispose();
});
testWidgets('Ensure canTapOnHeader is false by default', (WidgetTester tester) async {
final ExpansionPanel _expansionPanel = ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) => const Text('Demo'),
body: const SizedBox(height: 100.0),
);
expect(_expansionPanel.canTapOnHeader, isFalse);
});
testWidgets('Toggle ExpansionPanelRadio when tapping header and canTapOnHeader is true', (WidgetTester tester) async {
const Key firstPanelKey = Key('firstPanelKey');
const Key secondPanelKey = Key('secondPanelKey');
final List<ExpansionPanel> _demoItemsRadio = <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A', key: firstPanelKey);
},
body: const SizedBox(height: 100.0),
value: 0,
canTapOnHeader: true,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C', key: secondPanelKey);
},
body: const SizedBox(height: 100.0),
value: 1,
canTapOnHeader: true,
),
];
final ExpansionPanelList _expansionListRadio = ExpansionPanelList.radio(
children: _demoItemsRadio,
);
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: _expansionListRadio,
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// Now the first panel is open
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(secondPanelKey));
await tester.pumpAndSettle();
// Now the second panel is open
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsNothing);
expect(find.text('D'), findsOneWidget);
});
testWidgets('Toggle ExpansionPanel when tapping header and canTapOnHeader is true', (WidgetTester tester) async {
const Key firstPanelKey = Key('firstPanelKey');
const Key secondPanelKey = Key('secondPanelKey');
await tester.pumpWidget(
const MaterialApp(
home: SingleChildScrollView(
child: SimpleExpansionPanelListTestWidget(
firstPanelKey: firstPanelKey,
secondPanelKey: secondPanelKey,
canTapOnHeader: true,
),
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// The first panel is open
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// The first panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(secondPanelKey));
await tester.pumpAndSettle();
// The second panel is open
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsNothing);
expect(find.text('D'), findsOneWidget);
await tester.tap(find.byKey(secondPanelKey));
await tester.pumpAndSettle();
// The second panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
});
testWidgets('Do not toggle ExpansionPanel when tapping header and canTapOnHeader is false', (WidgetTester tester) async {
const Key firstPanelKey = Key('firstPanelKey');
const Key secondPanelKey = Key('secondPanelKey');
await tester.pumpWidget(
const MaterialApp(
home: SingleChildScrollView(
child: SimpleExpansionPanelListTestWidget(
firstPanelKey: firstPanelKey,
secondPanelKey: secondPanelKey,
),
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// The first panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(secondPanelKey));
await tester.pumpAndSettle();
// The second panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
});
testWidgets('Do not toggle ExpansionPanelRadio when tapping header and canTapOnHeader is false', (WidgetTester tester) async {
const Key firstPanelKey = Key('firstPanelKey');
const Key secondPanelKey = Key('secondPanelKey');
final List<ExpansionPanel> _demoItemsRadio = <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A', key: firstPanelKey);
},
body: const SizedBox(height: 100.0),
value: 0,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C', key: secondPanelKey);
},
body: const SizedBox(height: 100.0),
value: 1,
),
];
final ExpansionPanelList _expansionListRadio = ExpansionPanelList.radio(
children: _demoItemsRadio,
);
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: _expansionListRadio,
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// The first panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(secondPanelKey));
await tester.pumpAndSettle();
// The second panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
});
}
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