Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// #region body
import 'package:material_ui/material_ui.dart';

/// Flutter code sample for [ExpansionPanel.trailingIconVisibility].

void main() => runApp(const ExpansionPanelIconVisibilityExampleApp());

class ExpansionPanelIconVisibilityExampleApp extends StatelessWidget {
const ExpansionPanelIconVisibilityExampleApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ExpansionPanel Icon Visibility')),
body: const ExpansionPanelIconVisibilityExample(),
),
);
}
}

class ExpansionPanelIconVisibilityExample extends StatefulWidget {
const ExpansionPanelIconVisibilityExample({super.key});

@override
State<ExpansionPanelIconVisibilityExample> createState() =>
_ExpansionPanelIconVisibilityExampleState();
}

class _ExpansionPanelIconVisibilityExampleState
extends State<ExpansionPanelIconVisibilityExample> {
ExpansionPanelIconVisibility _visibility = .visible;
Comment thread
benji-farquhar marked this conversation as resolved.
bool _canTapOnHeader = false;
final List<bool> _isExpanded = <bool>[false, false, false];

@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: <Widget>[
SegmentedButton<ExpansionPanelIconVisibility>(
segments: const <ButtonSegment<ExpansionPanelIconVisibility>>[
ButtonSegment<ExpansionPanelIconVisibility>(
value: .visible,
label: Text('Visible'),
),
ButtonSegment<ExpansionPanelIconVisibility>(
value: .hidden,
label: Text('Hidden'),
),
ButtonSegment<ExpansionPanelIconVisibility>(
value: .gone,
label: Text('Gone'),
),
],
Comment thread
benji-farquhar marked this conversation as resolved.
selected: <ExpansionPanelIconVisibility>{_visibility},
onSelectionChanged:
(Set<ExpansionPanelIconVisibility> selected) {
setState(() {
_visibility = selected.first;
});
},
),
const SizedBox(height: 12),
SwitchListTile(
title: const Text('canTapOnHeader'),
value: _canTapOnHeader,
onChanged: (bool value) {
setState(() {
_canTapOnHeader = value;
});
},
),
],
),
),
const SizedBox(height: 24),
ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_isExpanded[index] = isExpanded;
});
},
children: <ExpansionPanel>[
for (int i = 0; i < 3; i++)
ExpansionPanel(
trailingIconVisibility: _visibility,
canTapOnHeader: _canTapOnHeader,
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text('Panel ${i + 1}'),
trailing: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
),
);
},
body: ListTile(
title: Text('Panel ${i + 1} content'),
subtitle: const Text(
'When the icon visibility is hidden, the space is preserved. '
'When gone, the space is removed.',
),
),
isExpanded: _isExpanded[i],
),
],
),
const SizedBox(height: 24),
],
),
);
}
}
// #endregion body
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter_test/flutter_test.dart';
import 'package:material_ui/material_ui.dart';
import 'package:material_ui_examples/expansion_panel/expansion_panel_list.1.dart'
as example;

void main() {
testWidgets('ExpansionPanel icon visibility can be toggled', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const example.ExpansionPanelIconVisibilityExampleApp(),
);

expect(find.byType(ExpandIcon), findsNWidgets(3));

final Finder visibilityFinder = find
.ancestor(
of: find.byType(ExpandIcon).first,
matching: find.byType(Visibility),
)
.first;

Visibility visibility = tester.widget(visibilityFinder);
expect(visibility.visible, isTrue);

await tester.tap(find.text('Hidden'));
await tester.pumpAndSettle();

visibility = tester.widget(visibilityFinder);
expect(visibility.visible, isFalse);
expect(visibility.maintainSize, isTrue);

await tester.tap(find.text('Gone'));
await tester.pumpAndSettle();

expect(find.byType(ExpandIcon), findsNothing);
});
}
70 changes: 58 additions & 12 deletions packages/material_ui/lib/src/expansion_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ typedef ExpansionPanelCallback = void Function(int panelIndex, bool isExpanded);
/// [ExpansionPanel] needs to rebuild.
typedef ExpansionPanelHeaderBuilder = Widget Function(BuildContext context, bool isExpanded);

/// Controls the visibility of the trailing expand/collapse icon in an
/// [ExpansionPanel].
enum ExpansionPanelIconVisibility {
/// The icon is visible and interactive.
visible,

/// The icon is hidden, but its layout space is preserved to maintain
/// alignment with other panels.
hidden,

/// The icon is hidden and its layout space is removed.
gone,
}

/// A material expansion panel. It has a header and a body and can be either
/// expanded or collapsed. The body of the panel is only visible when it is
/// expanded.
Expand All @@ -69,6 +83,17 @@ typedef ExpansionPanelHeaderBuilder = Widget Function(BuildContext context, bool
///
/// See [ExpansionPanelList] for a sample implementation.
///
/// <callout-box>
///
/// This example demonstrates how to use [trailingIconVisibility] to control
/// the visibility of the expand/collapse icon.
///
/// {@macro material_ui.dartpad_guide}
///
/// {@example /example/lib/expansion_panel/expansion_panel_list.1.dart#body}
///
/// </callout-box>
///
/// See also:
///
/// * [ExpansionPanelList]
Expand All @@ -84,6 +109,7 @@ class ExpansionPanel {
this.backgroundColor,
this.splashColor,
this.highlightColor,
this.trailingIconVisibility = ExpansionPanelIconVisibility.visible,
});

/// The widget builder that builds the expansion panels' header.
Expand Down Expand Up @@ -130,6 +156,16 @@ class ExpansionPanel {
///
/// Defaults to [ThemeData.cardColor].
final Color? backgroundColor;

/// Controls the visibility of the trailing expand/collapse icon.
///
/// When set to [ExpansionPanelIconVisibility.hidden], the icon is not
/// rendered but its layout space is preserved for alignment. When set to
/// [ExpansionPanelIconVisibility.gone], both the icon and its space are
/// removed.
///
/// Defaults to [ExpansionPanelIconVisibility.visible].
final ExpansionPanelIconVisibility trailingIconVisibility;
}

/// An expansion panel that allows for radio-like functionality.
Expand All @@ -153,6 +189,7 @@ class ExpansionPanelRadio extends ExpansionPanel {
super.backgroundColor,
super.splashColor,
super.highlightColor,
super.trailingIconVisibility,
});

/// The value that uniquely identifies a radio panel so that the currently
Expand Down Expand Up @@ -406,22 +443,31 @@ class _ExpansionPanelListState extends State<ExpansionPanelList> {
final ExpansionPanel child = widget.children[index];
final Widget headerWidget = child.headerBuilder(context, _isChildExpanded(index));

Widget expandIconPadded = Padding(
padding: const EdgeInsetsDirectional.only(end: 8.0),
child: IgnorePointer(
ignoring: child.canTapOnHeader,
child: ExpandIcon(
color: widget.expandIconColor,
isExpanded: _isChildExpanded(index),
padding: _kExpandIconPadding,
splashColor: child.splashColor,
highlightColor: child.highlightColor,
onPressed: (bool isExpanded) => _handlePressed(isExpanded, index),
final iconVisible = child.trailingIconVisibility == .visible;
final iconMaintainSize = child.trailingIconVisibility == .hidden;
Comment thread
benji-farquhar marked this conversation as resolved.

Widget expandIconPadded = Visibility(
visible: iconVisible,
maintainSize: iconMaintainSize,
maintainAnimation: iconMaintainSize,
maintainState: iconMaintainSize,
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 8.0),
child: IgnorePointer(
ignoring: child.canTapOnHeader,
child: ExpandIcon(
color: widget.expandIconColor,
isExpanded: _isChildExpanded(index),
padding: _kExpandIconPadding,
splashColor: child.splashColor,
highlightColor: child.highlightColor,
onPressed: (bool isExpanded) => _handlePressed(isExpanded, index),
),
),
),
);

if (!child.canTapOnHeader) {
if (!child.canTapOnHeader && iconVisible) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
expandIconPadded = Semantics(
label: _isChildExpanded(index)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changelog: |
- Adds `ExpansionPanel.trailingIconVisibility` to control expand/collapse icon visibility.
version: minor
Loading