Skip to content

Commit 0d154e5

Browse files
authored
Add ability to clip Stepper step content (#152370)
fixes [Dismissible content overlays Stepper interface while dismissing it](flutter/flutter#66007) ### Code sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { const MyApp({super.key}); @OverRide State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { final List<String> items = List<String>.generate(20, (int i) => 'Item ${i + 1}'); @OverRide Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Padding( padding: const EdgeInsets.all(20), child: DecoratedBox( decoration: BoxDecoration( border: Border.all(color: Colors.amber, width: 2), ), child: Padding( padding: const EdgeInsets.all(2.0), child: Column( children: <Widget>[ const SizedBox(height: 8.0), Text( 'Dismissible Widget - Vertical Stepper Widget', style: Theme.of(context).textTheme.titleLarge, ), Expanded( child: Stepper( clipBehavior: Clip.hardEdge, steps: <Step>[ Step( isActive: true, title: const Text('Step 1'), content: ColoredBox( color: Colors.black12, child: ListView.builder( itemCount: items.length, shrinkWrap: true, itemBuilder: (BuildContext context, int index) { final String item = items[index]; return Dismissible( key: Key(item), onDismissed: (DismissDirection direction) { setState(() { items.removeAt(index); }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$item dismissed'))); }, background: Container(color: Colors.red), child: ListTile(title: Text(item)), ); }, ), ), ), const Step( title: Text('Step 2'), content: Text('content'), ), ], ), ), const Divider(height: 1), const SizedBox(height: 8.0), Text( 'Dismissible Widget - Horizontal Stepper Widget', style: Theme.of(context).textTheme.titleLarge, ), Expanded( child: Stepper( clipBehavior: Clip.hardEdge, type: StepperType.horizontal, elevation: 0.0, steps: <Step>[ Step( isActive: true, title: const Text('Step 1'), content: ColoredBox( color: Colors.black12, child: ListView.builder( itemCount: items.length, shrinkWrap: true, itemBuilder: (BuildContext context, int index) { final String item = items[index]; return Dismissible( key: Key(item), onDismissed: (DismissDirection direction) { setState(() { items.removeAt(index); }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$item dismissed'))); }, background: Container(color: Colors.red), child: ListTile(title: Text(item)), ); }, ), ), ), const Step( title: Text('Step 2'), content: Text('content'), ), ], ), ), ], ), ), ), ), ), ); } } ``` </details> ### Without `Stepper` step content clipping ![Group 1](https://github.com/user-attachments/assets/1814ad90-8d43-4e03-9f68-7da47e08c718) ### With `Stepper` step content clipping ![Group 2](https://github.com/user-attachments/assets/652ff597-7e9a-4d35-abc2-80d60cee03f4)
1 parent 91a3f69 commit 0d154e5

File tree

2 files changed

+103
-2
lines changed

2 files changed

+103
-2
lines changed

packages/flutter/lib/src/material/stepper.dart

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ class Stepper extends StatefulWidget {
222222
this.stepIconHeight,
223223
this.stepIconWidth,
224224
this.stepIconMargin,
225+
this.clipBehavior = Clip.none,
225226
}) : assert(0 <= currentStep && currentStep < steps.length),
226227
assert(stepIconHeight == null || (stepIconHeight >= _kStepSize && stepIconHeight <= _kMaxStepSize),
227228
'stepIconHeight must be greater than $_kStepSize and less or equal to $_kMaxStepSize'),
@@ -366,6 +367,15 @@ class Stepper extends StatefulWidget {
366367
/// Overrides the default step icon margin.
367368
final EdgeInsets? stepIconMargin;
368369

370+
/// The [Step.content] will be clipped to this Clip type.
371+
///
372+
/// Defaults to [Clip.none].
373+
///
374+
/// See also:
375+
///
376+
/// * [Clip], which explains how to use this property.
377+
final Clip clipBehavior;
378+
369379
@override
370380
State<Stepper> createState() => _StepperState();
371381
}
@@ -795,7 +805,10 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
795805
),
796806
child: Column(
797807
children: <Widget>[
798-
widget.steps[index].content,
808+
ClipRect(
809+
clipBehavior: widget.clipBehavior,
810+
child: widget.steps[index].content,
811+
),
799812
_buildVerticalControls(index),
800813
],
801814
),
@@ -888,7 +901,10 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
888901
Visibility(
889902
maintainState: true,
890903
visible: i == widget.currentStep,
891-
child: widget.steps[i].content,
904+
child: ClipRect(
905+
clipBehavior: widget.clipBehavior,
906+
child: widget.steps[i].content,
907+
),
892908
),
893909
);
894910
}

packages/flutter/test/material/stepper_test.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,6 +1720,91 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async
17201720
));
17211721
expect(lastConnector.width, equals(0.0));
17221722
});
1723+
1724+
// This is a regression test for https://github.com/flutter/flutter/issues/66007.
1725+
testWidgets('Default Stepper clipBehavior', (WidgetTester tester) async {
1726+
Widget buildStepper({ required StepperType type }) {
1727+
return MaterialApp(
1728+
home: Scaffold(
1729+
body: Center(
1730+
child: Stepper(
1731+
type: type,
1732+
steps: const <Step>[
1733+
Step(
1734+
title: Text('step1'),
1735+
content: Text('step1 content'),
1736+
),
1737+
Step(
1738+
title: Text('step2'),
1739+
content: Text('step2 content'),
1740+
),
1741+
],
1742+
),
1743+
),
1744+
),
1745+
);
1746+
}
1747+
1748+
ClipRect getContentClipRect() {
1749+
return tester.widget<ClipRect>(find.ancestor(
1750+
of: find.text('step1 content'),
1751+
matching: find.byType(ClipRect),
1752+
).first);
1753+
}
1754+
1755+
// Test vertical stepper with default clipBehavior.
1756+
await tester.pumpWidget(buildStepper(type: StepperType.vertical));
1757+
1758+
expect(getContentClipRect().clipBehavior, equals(Clip.none));
1759+
1760+
// Test horizontal stepper with default clipBehavior.
1761+
await tester.pumpWidget(buildStepper(type: StepperType.horizontal));
1762+
1763+
expect(getContentClipRect().clipBehavior, equals(Clip.none));
1764+
});
1765+
1766+
// This is a regression test for https://github.com/flutter/flutter/issues/66007.
1767+
testWidgets('Stepper steps can be clipped', (WidgetTester tester) async {
1768+
Widget buildStepper({ required StepperType type, required Clip clipBehavior }) {
1769+
return MaterialApp(
1770+
home: Scaffold(
1771+
body: Center(
1772+
child: Stepper(
1773+
clipBehavior: clipBehavior,
1774+
type: type,
1775+
steps: const <Step>[
1776+
Step(
1777+
title: Text('step1'),
1778+
content: Text('step1 content'),
1779+
),
1780+
Step(
1781+
title: Text('step2'),
1782+
content: Text('step2 content'),
1783+
),
1784+
],
1785+
),
1786+
),
1787+
),
1788+
);
1789+
}
1790+
1791+
ClipRect getContentClipRect() {
1792+
return tester.widget<ClipRect>(find.ancestor(
1793+
of: find.text('step1 content'),
1794+
matching: find.byType(ClipRect),
1795+
).first);
1796+
}
1797+
1798+
// Test vertical stepper with clipBehavior set to Clip.hardEdge.
1799+
await tester.pumpWidget(buildStepper(type: StepperType.vertical, clipBehavior: Clip.hardEdge));
1800+
1801+
expect(getContentClipRect().clipBehavior, equals(Clip.hardEdge));
1802+
1803+
// Test horizontal stepper with clipBehavior set to Clip.hardEdge.
1804+
await tester.pumpWidget(buildStepper(type: StepperType.horizontal, clipBehavior: Clip.hardEdge));
1805+
1806+
expect(getContentClipRect().clipBehavior, equals(Clip.hardEdge));
1807+
});
17231808
}
17241809

17251810
class _TappableColorWidget extends StatefulWidget {

0 commit comments

Comments
 (0)