Unverified Commit 3a1b0495 authored by Craig Labenz's avatar Craig Labenz Committed by GitHub

fix: refactor Stepper.controlsBuilder to use ControlsDetails (#88538)

* changed controlsBuilder signature

combined all parameters into ControlsDetails class

* sample fixes

* updates to docstrings

* switched to positional argument for stepper.controlsbuilder

* Merge branch 'master' into stepper-builder-fix
parent ca077d43
......@@ -49,16 +49,15 @@ class MyStatelessWidget extends StatelessWidget {
Widget build(BuildContext context) {
return Stepper(
controlsBuilder: (BuildContext context,
{VoidCallback? onStepContinue, VoidCallback? onStepCancel}) {
controlsBuilder: (BuildContext context, ControlsDetails details) {
return Row(
children: <Widget>[
TextButton(
onPressed: onStepContinue,
onPressed: details.onStepContinue,
child: const Text('NEXT'),
),
TextButton(
onPressed: onStepCancel,
onPressed: details.onStepCancel,
child: const Text('CANCEL'),
),
],
......
......@@ -55,6 +55,57 @@ enum StepperType {
horizontal,
}
/// Container for all the information necessary to build a Stepper widget's
/// foward and backward controls for any given step.
///
/// Used by [Stepper.controlsBuilder].
@immutable
class ControlsDetails {
/// Creates a set of details describing the Stepper.
const ControlsDetails({
required this.currentStep,
required this.stepIndex,
this.onStepCancel,
this.onStepContinue,
});
/// Index that is active for the surrounding [Stepper] widget. This may be
/// different from [stepIndex] if the user has just changed steps and we are
/// currently animating toward that step.
final int currentStep;
/// Index of the step for which these controls are being built. This is
/// not necessarily the active index, if the user has just changed steps and
/// this step is animating away. To determine whether a given builder is building
/// the active step or the step being navigated away from, see [isActive].
final int stepIndex;
/// The callback called when the 'continue' button is tapped.
///
/// If null, the 'continue' button will be disabled.
final VoidCallback? onStepContinue;
/// The callback called when the 'cancel' button is tapped.
///
/// If null, the 'cancel' button will be disabled.
final VoidCallback? onStepCancel;
/// True if the indicated step is also the current active step. If the user has
/// just activated the transition to a new step, some [Stepper.type] values will
/// lead to both steps being rendered for the duration of the animation shifting
/// between steps.
bool get isActive => currentStep == stepIndex;
}
/// A builder that creates a widget given the two callbacks `onStepContinue` and
/// `onStepCancel`.
///
/// Used by [Stepper.controlsBuilder].
///
/// See also:
///
/// * [WidgetBuilder], which is similar but only takes a [BuildContext].
typedef ControlsWidgetBuilder = Widget Function(BuildContext context, ControlsDetails details);
const TextStyle _kStepStyle = TextStyle(
fontSize: 12.0,
color: Colors.white,
......@@ -199,14 +250,52 @@ class Stepper extends StatefulWidget {
///
/// If null, the default controls from the current theme will be used.
///
/// This callback which takes in a context and two functions: [onStepContinue]
/// and [onStepCancel]. These can be used to control the stepper.
/// For example, keeping track of the [currentStep] within the callback can
/// change the text of the continue or cancel button depending on which step users are at.
/// This callback which takes in a context and a [ControlsDetails] object, which
/// contains step information and two functions: [onStepContinue] and [onStepCancel].
/// These can be used to control the stepper. For example, reading the
/// [ControlsDetails.currentStep] value within the callback can change the text
/// of the continue or cancel button depending on which step users are at.
///
/// {@tool dartpad --template=stateless_widget_scaffold}
/// Creates a stepper control with custom buttons.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Stepper(
/// controlsBuilder:
/// (BuildContext context, ControlsDetails details) {
/// return Row(
/// children: <Widget>[
/// TextButton(
/// onPressed: details.onStepContinue,
/// child: Text('Continue to Step ${details.stepIndex + 1}'),
/// ),
/// TextButton(
/// onPressed: details.onStepCancel,
/// child: Text('Back to Step ${details.stepIndex - 1}'),
/// ),
/// ],
/// );
/// },
/// steps: const <Step>[
/// Step(
/// title: Text('A'),
/// content: SizedBox(
/// width: 100.0,
/// height: 100.0,
/// ),
/// ),
/// Step(
/// title: Text('B'),
/// content: SizedBox(
/// width: 100.0,
/// height: 100.0,
/// ),
/// ),
/// ],
/// );
/// }
/// ```
/// ** See code in examples/api/lib/material/stepper/stepper.controls_builder.0.dart **
/// {@end-tool}
final ControlsWidgetBuilder? controlsBuilder;
......@@ -368,9 +457,17 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
}
}
Widget _buildVerticalControls() {
Widget _buildVerticalControls(int stepIndex) {
if (widget.controlsBuilder != null)
return widget.controlsBuilder!(context, onStepContinue: widget.onStepContinue, onStepCancel: widget.onStepCancel);
return widget.controlsBuilder!(
context,
ControlsDetails(
currentStep: widget.currentStep,
onStepContinue: widget.onStepContinue,
onStepCancel: widget.onStepCancel,
stepIndex: stepIndex,
),
);
final Color cancelColor;
switch (Theme.of(context).brightness) {
......@@ -552,7 +649,7 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
child: Column(
children: <Widget>[
widget.steps[index].content,
_buildVerticalControls(),
_buildVerticalControls(index),
],
),
),
......@@ -652,7 +749,7 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
duration: kThemeAnimationDuration,
child: widget.steps[widget.currentStep].content,
),
_buildVerticalControls(),
_buildVerticalControls(widget.currentStep),
],
),
),
......
......@@ -4532,16 +4532,6 @@ typedef NullableIndexedWidgetBuilder = Widget? Function(BuildContext context, in
/// * [ValueWidgetBuilder], which is similar but takes a value and a child.
typedef TransitionBuilder = Widget Function(BuildContext context, Widget? child);
/// A builder that creates a widget given the two callbacks `onStepContinue` and
/// `onStepCancel`.
///
/// Used by [Stepper.controlsBuilder].
///
/// See also:
///
/// * [WidgetBuilder], which is similar but only takes a [BuildContext].
typedef ControlsWidgetBuilder = Widget Function(BuildContext context, { VoidCallback? onStepContinue, VoidCallback? onStepCancel });
/// An [Element] that composes other [Element]s.
///
/// Rather than creating a [RenderObject] directly, a [ComponentElement] creates
......
......@@ -380,7 +380,7 @@ void main() {
canceledPressed = true;
}
Widget builder(BuildContext context, { VoidCallback? onStepContinue, VoidCallback? onStepCancel }) {
Widget builder(BuildContext context, ControlsDetails details) {
return Container(
margin: const EdgeInsets.only(top: 16.0),
child: ConstrainedBox(
......@@ -388,13 +388,13 @@ void main() {
child: Row(
children: <Widget>[
TextButton(
onPressed: onStepContinue,
onPressed: details.onStepContinue,
child: const Text('Let us continue!'),
),
Container(
margin: const EdgeInsetsDirectional.only(start: 8.0),
child: TextButton(
onPressed: onStepCancel,
onPressed: details.onStepCancel,
child: const Text('Cancel This!'),
),
),
......@@ -448,6 +448,100 @@ void main() {
expect(continuePressed, isTrue);
});
testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async {
int currentStep = 0;
void setContinue() {
currentStep += 1;
}
void setCanceled() {
currentStep -= 1;
}
Widget builder(BuildContext context, ControlsDetails details) {
// For the purposes of testing, only render something for the active
// step.
if (!details.isActive)
return Container();
return Container(
margin: const EdgeInsets.only(top: 16.0),
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(height: 48.0),
child: Row(
children: <Widget>[
TextButton(
onPressed: details.onStepContinue,
child: Text('Continue to ${details.stepIndex + 1}'),
),
Container(
margin: const EdgeInsetsDirectional.only(start: 8.0),
child: TextButton(
onPressed: details.onStepCancel,
child: Text('Return to ${details.stepIndex - 1}'),
),
),
],
),
),
);
}
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Stepper(
currentStep: currentStep,
controlsBuilder: builder,
onStepCancel: () => setState(setCanceled),
onStepContinue: () => setState(setContinue),
steps: const <Step>[
Step(
title: Text('A'),
state: StepState.complete,
content: SizedBox(
width: 100.0,
height: 100.0,
),
),
Step(
title: Text('C'),
content: SizedBox(
width: 100.0,
height: 100.0,
),
),
],
);
},
),
),
),
),
);
// Never mind that there is no Step -1 or Step 2 -- actual build method
// implementations would make those checks.
expect(find.text('Return to -1'), findsNWidgets(1));
expect(find.text('Continue to 1'), findsNWidgets(1));
expect(find.text('Return to 0'), findsNWidgets(0));
expect(find.text('Continue to 2'), findsNWidgets(0));
await tester.tap(find.text('Continue to 1').first);
await tester.pumpAndSettle();
// Never mind that there is no Step -1 or Step 2 -- actual build method
// implementations would make those checks.
expect(find.text('Return to -1'), findsNWidgets(0));
expect(find.text('Continue to 1'), findsNWidgets(0));
expect(find.text('Return to 0'), findsNWidgets(1));
expect(find.text('Continue to 2'), findsNWidgets(1));
});
testWidgets('Stepper error test', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
......
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