Added single open panel functionality#19624
Conversation
| /// The duration of the expansion animation. | ||
| final Duration animationDuration; | ||
|
|
||
| //Whether multiple panels can be open simultaneously |
There was a problem hiding this comment.
Nit: space between // and text
| if (!widget._allowMultiplePanelsOpen) { | ||
| for (int i = 0; i < widget.children.length; i += 1) { | ||
| assert(widget.children[i] is ExpansionPanelRadio, | ||
| 'All children of ExpansionPanel.radio need to be of type ExpansionPanelRadio'); |
There was a problem hiding this comment.
indent with 2 spaces
| } | ||
|
|
||
| assert(_debugCheckExpandLimit(), | ||
| 'This expansion panel widget initialized with more than one panel open'); |
| @override | ||
| void initState() { | ||
| super.initState(); | ||
| if (!widget._allowMultiplePanelsOpen) { |
There was a problem hiding this comment.
Since you are always using the negation of this field, consider changing it to _preventMultiplePanelsOpen or something similar
| } | ||
| _openPanels[pressedChild.value] = !isExpanded; | ||
| } | ||
| setState((){}); |
There was a problem hiding this comment.
You only want to setState if you aren't allowing multiple panels
|
|
||
| void _handlePressed(bool isExpanded, int index) { | ||
| if (widget.expansionCallback != null) | ||
| widget.expansionCallback(index, isExpanded); |
There was a problem hiding this comment.
Does this callback get invoked twice in radio mode?
There was a problem hiding this comment.
You mean by the rest of the code below? I don't think so, only possibly one other panel if it was open before.
| context, | ||
| children[index].isExpanded, | ||
| widget._allowMultiplePanelsOpen ? widget.children[index].isExpanded : | ||
| _openPanels[_widgetChild.value], |
|
|
||
| testWidgets('Single Panel Open Test', (WidgetTester tester) async { | ||
|
|
||
| List<ExpansionPanel> _demoItems; |
There was a problem hiding this comment.
Initialize variables where they are declared
| }) : assert(headerBuilder != null), | ||
| assert(body != null), | ||
| assert(isExpanded != null); | ||
| assert(body != null), |
There was a problem hiding this comment.
Nit: keep asserts lined up
| bool panelExpanded = false; | ||
| for (int i = 0; i < widget.children.length; i += 1) { | ||
| assert(widget.children[i] is ExpansionPanelRadio, | ||
| 'All children of ExpansionPanel.radio need to be of type ExpansionPanelRadio'); |
| final int childKey = child.value; | ||
| final bool panelWillBeExpanded = child.initializesExpanded || (childKey == _currentOpenPanel?.value); | ||
| assert(!panelExpanded && panelWillBeExpanded || panelExpanded && !panelWillBeExpanded, | ||
| 'Trying to initialize radio panel list with more than two open panels!'); |
| void initState() { | ||
| super.initState(); | ||
| if (widget._allowOnlyOnePanelOpen) { | ||
| for (int i = 0; i < widget.children.length; i += 1) { |
There was a problem hiding this comment.
Since you don't need the index i for anything, use for (ExpansionPanelRadio child in widget.children), here and elsewhere. I don't think you need to assert in initState unless you are dealing with the radio -> non radio or reverse
| @override | ||
| void didUpdateWidget(ExpansionPanelList oldWidget) { | ||
| super.didUpdateWidget(oldWidget); | ||
|
|
There was a problem hiding this comment.
What happens if I change from radio to non-radio or likewise?
| expect(tester.getRect(find.byType(AnimatedSize).at(2)), new Rect.fromLTWH(0.0, 56.0 + 1.0 + 56.0 + 16.0 + 16.0 + 48.0 + 16.0, 800.0, 100.0)); | ||
| }); | ||
|
|
||
| testWidgets('Single Panel Open Test', (WidgetTester tester) async { |
There was a problem hiding this comment.
Make sure you test adding/removing panels in radio mode (especially the open panel), and switch from radio to non radio
| }) : assert(headerBuilder != null), | ||
| assert(body != null), | ||
| assert(isExpanded != null); | ||
| }) : assert(headerBuilder != null), |
There was a problem hiding this comment.
Remove the extra spaces, here and below in the initializer list.
|
|
||
| /// An expansion panel that allows for radio-like functionality. | ||
| /// | ||
| /// This type of panel will automatically handle closing and opening itself, |
There was a problem hiding this comment.
Try and keep these doc comments to 80 columns
| /// An expansion panel that allows for radio-like functionality. | ||
| /// | ||
| /// This type of panel will automatically handle closing and opening itself, | ||
| /// set the [initializesExpanded] value to true on a panel if you want it to begin |
There was a problem hiding this comment.
I'm not sure about placing initializesExpanded on the RadioPanel. Try adding another named argument to the ExpansionPanelList.radio constructor which takes a panel or value of a panel to open "only the first time it is created". Then don't both updating in the future, it doesn't make sense to try and keep all of the initializesExpanded and state changed by user interaction in sync.
| @required this.value, | ||
| @required ExpansionPanelHeaderBuilder headerBuilder, | ||
| @required Widget body, | ||
| }) : assert(initializesExpanded != null), |
There was a problem hiding this comment.
Same as above, 1 space here.
| /// Whether this panel initializes expanded or not. | ||
| final bool initializesExpanded; | ||
|
|
||
| /// Identifier that corresponds to this specific object. |
There was a problem hiding this comment.
"The value that uniquely identifies a radio panel"
There was a problem hiding this comment.
Also this should be Object like I said, otherwise I can't use a String key
| _currentOpenPanel = child; | ||
| } | ||
| } | ||
| else if(oldWidget._allowOnlyOnePanelOpen) { |
There was a problem hiding this comment.
Move this to the previous line so that it directly follows the previous closing }.
| } | ||
| else if(oldWidget._allowOnlyOnePanelOpen) { | ||
| _currentOpenPanel = null; | ||
| setState((){}); |
There was a problem hiding this comment.
You don't/shouldn't setState in didUpdateWidget, because your state is already dirty. Might have been a miscommunication - this is only required in _handlePressed
| if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1)) | ||
| items.add(new MaterialGap(key: new _SaltedKey<BuildContext, int>(context, index * 2 - 1))); | ||
|
|
||
| final ExpansionPanelRadio _widgetChild = widget._allowOnlyOnePanelOpen ? widget.children[index] : null; |
There was a problem hiding this comment.
Don't make locals private, rename to "radioPanel"
| child: widget.children[index].headerBuilder( | ||
| context, | ||
| children[index].isExpanded, | ||
| widget._allowOnlyOnePanelOpen ? _widgetChild.value == _currentOpenPanel?.value: |
There was a problem hiding this comment.
use the _isChildExpanded callback
| /// [ExpansionPanelRadio]. | ||
| const ExpansionPanelList.radio({ | ||
| Key key, | ||
| this.children = const <ExpansionPanelRadio>[], |
There was a problem hiding this comment.
Something I missed earlier - this isn't a type annotation for children, it is just the type for the default value. You need to use an explicitly typed param with an initializer.
({
List<ExpansionPanelRadio> children = ..,
}) : children = children,
| /// A unique identifier [value] must be assigned to each panel. | ||
| class ExpansionPanelRadio extends ExpansionPanel { | ||
|
|
||
| /// A subclass of expansion panel that allows for radio functionality. |
There was a problem hiding this comment.
Just say "An expansion panel" like you had above. Don't put the "subclass of" part in because if that changes, it's easy for the docs to get missed and be out of date.
|
|
||
| /// A subclass of expansion panel that allows for radio functionality. | ||
| /// | ||
| /// A unique identifier [value] must be passed into the constructor. The |
There was a problem hiding this comment.
Take out the word "identifier": it reads better as "A unique [value]".
Also, explain why it must be unique: say something like "The [value] is used to indicate which panel is currently selected."
| }) : assert(value != null), | ||
| super(body: body, headerBuilder: headerBuilder); | ||
|
|
||
| /// The value that uniquely identifies a radio panel. |
There was a problem hiding this comment.
Add why it exists: "so that the currently selected radio panel may be identified."
| final bool _allowOnlyOnePanelOpen; | ||
|
|
||
| /// The value of the panel that initially begins open. (This value is | ||
| /// only used when initializing with the radio constructor.) |
There was a problem hiding this comment.
radio -> [ExpansionPanelList.radio]
| child: new ConstrainedBox( | ||
| constraints: const BoxConstraints(minHeight: _kPanelHeaderCollapsedHeight), | ||
| child: children[index].headerBuilder( | ||
| child: widget.children[index].headerBuilder( |
There was a problem hiding this comment.
Since it's used more than once, you can add:
final ExpansionPanelRadio child = widget.children[index];above, and use child.headerBuilder( here, and below.
| /// This widget allows for at most one panel in the list to be open. | ||
| /// The expansion panel callback is triggered when an expansion panel | ||
| /// expand/collapse button is pushed. The [children] and [animationDuration] | ||
| /// arguments must not be null. The [children] objects also must of type |
There was a problem hiding this comment.
"objects also must of type" --> "objects must be instances of"

fixes #19240