Adds custom control builder functionality to Stepper (#23310)
* Adds test and builder
diff --git a/packages/flutter/lib/src/material/stepper.dart b/packages/flutter/lib/src/material/stepper.dart
index d091855..78c54c3 100644
--- a/packages/flutter/lib/src/material/stepper.dart
+++ b/packages/flutter/lib/src/material/stepper.dart
@@ -140,6 +140,7 @@
this.onStepTapped,
this.onStepContinue,
this.onStepCancel,
+ this.controlsBuilder,
}) : assert(steps != null),
assert(type != null),
assert(currentStep != null),
@@ -174,6 +175,53 @@
/// If null, the 'cancel' button will be disabled.
final VoidCallback onStepCancel;
+ /// The callback for creating custom controls.
+ ///
+ /// 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.
+ ///
+ /// ## Sample Code:
+ /// Creates a stepper control with custom buttons.
+ ///
+ /// ```dart
+ /// Stepper(
+ /// controlsBuilder:
+ /// (BuildContext context, {VoidCallback onStepContinue, VoidCallback onStepCancel}) {
+ /// return Row(
+ /// children: <Widget>[
+ /// FlatButton(
+ /// onPressed: onStepContinue,
+ /// child: const Text('My Awesome Continue Message!'),
+ /// ),
+ /// FlatButton(
+ /// onPressed: onStepCancel,
+ /// child: const Text('My Awesome Cancel Message!'),
+ /// ),
+ /// ],
+ /// ),
+ /// },
+ /// 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,
+ /// ),
+ /// ),
+ /// ],
+ /// )
+ /// ```
+ final ControlsWidgetBuilder controlsBuilder;
+
@override
_StepperState createState() => _StepperState();
}
@@ -327,6 +375,9 @@
}
Widget _buildVerticalControls() {
+ if (widget.controlsBuilder != null)
+ return widget.controlsBuilder(context, onStepContinue: widget.onStepContinue, onStepCancel: widget.onStepCancel);
+
Color cancelColor;
switch (Theme.of(context).brightness) {
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index b5e2ae0..0cebc96 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -3632,6 +3632,11 @@
/// [MaterialApp.builder].
typedef TransitionBuilder = Widget Function(BuildContext context, Widget child);
+/// A Signiture for a function that creates a widget given [onStepContinue] and [onStepCancel].
+///
+/// Used by [Stepper.builder].
+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
diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart
index 6b1e912..24eeeea 100644
--- a/packages/flutter/test/material/stepper_test.dart
+++ b/packages/flutter/test/material/stepper_test.dart
@@ -368,6 +368,91 @@
expect(find.text('2'), findsOneWidget);
});
+ testWidgets('Stepper custom controls test', (WidgetTester tester) async {
+ bool continuePressed = false;
+ void setContinue() {
+ continuePressed = true;
+ }
+
+ bool canceledPressed = false;
+ void setCanceled() {
+ canceledPressed = true;
+ }
+
+ final ControlsWidgetBuilder builder =
+ (BuildContext context, {VoidCallback onStepContinue, VoidCallback onStepCancel}) {
+ return Container(
+ margin: const EdgeInsets.only(top: 16.0),
+ child: ConstrainedBox(
+ constraints: const BoxConstraints.tightFor(height: 48.0),
+ child: Row(
+ children: <Widget>[
+ FlatButton(
+ onPressed: onStepContinue,
+ color: Colors.blue,
+ textColor: Colors.white,
+ textTheme: ButtonTextTheme.normal,
+ child: const Text('Let us continue!'),
+ ),
+ Container(
+ margin: const EdgeInsetsDirectional.only(start: 8.0),
+ child: FlatButton(
+ onPressed: onStepCancel,
+ textColor: Colors.red,
+ textTheme: ButtonTextTheme.normal,
+ child: const Text('Cancel This!'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ };
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Center(
+ child: Material(
+ child: Stepper(
+ controlsBuilder: builder,
+ onStepCancel: setCanceled,
+ onStepContinue: setContinue,
+ steps: const <Step>[
+ Step(
+ title: Text('A'),
+ state: StepState.complete,
+ content: SizedBox(
+ width: 100.0,
+ height: 100.0,
+ ),
+ ),
+ Step(
+ title: Text('B'),
+ content: SizedBox(
+ width: 100.0,
+ height: 100.0,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ // 2 because stepper creates a set of controls for each step
+ expect(find.text('Let us continue!'), findsNWidgets(2));
+ expect(find.text('Cancel This!'), findsNWidgets(2));
+
+ await tester.tap(find.text('Cancel This!').first);
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Let us continue!').first);
+ await tester.pumpAndSettle();
+
+ expect(canceledPressed, isTrue);
+ expect(continuePressed, isTrue);
+ });
+
testWidgets('Stepper error test', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(