Add ability to clip Stepper step content (#152370)

fixes [Dismissible content overlays Stepper interface while dismissing it](https://github.com/flutter/flutter/issues/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)
This commit is contained in:
Taha Tesser 2024-07-31 22:58:04 +03:00 committed by GitHub
parent 91a3f69f11
commit 0d154e55d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 103 additions and 2 deletions

View File

@ -222,6 +222,7 @@ class Stepper extends StatefulWidget {
this.stepIconHeight, this.stepIconHeight,
this.stepIconWidth, this.stepIconWidth,
this.stepIconMargin, this.stepIconMargin,
this.clipBehavior = Clip.none,
}) : assert(0 <= currentStep && currentStep < steps.length), }) : assert(0 <= currentStep && currentStep < steps.length),
assert(stepIconHeight == null || (stepIconHeight >= _kStepSize && stepIconHeight <= _kMaxStepSize), assert(stepIconHeight == null || (stepIconHeight >= _kStepSize && stepIconHeight <= _kMaxStepSize),
'stepIconHeight must be greater than $_kStepSize and less or equal to $_kMaxStepSize'), 'stepIconHeight must be greater than $_kStepSize and less or equal to $_kMaxStepSize'),
@ -366,6 +367,15 @@ class Stepper extends StatefulWidget {
/// Overrides the default step icon margin. /// Overrides the default step icon margin.
final EdgeInsets? stepIconMargin; final EdgeInsets? stepIconMargin;
/// The [Step.content] will be clipped to this Clip type.
///
/// Defaults to [Clip.none].
///
/// See also:
///
/// * [Clip], which explains how to use this property.
final Clip clipBehavior;
@override @override
State<Stepper> createState() => _StepperState(); State<Stepper> createState() => _StepperState();
} }
@ -795,7 +805,10 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
), ),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
widget.steps[index].content, ClipRect(
clipBehavior: widget.clipBehavior,
child: widget.steps[index].content,
),
_buildVerticalControls(index), _buildVerticalControls(index),
], ],
), ),
@ -888,7 +901,10 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
Visibility( Visibility(
maintainState: true, maintainState: true,
visible: i == widget.currentStep, visible: i == widget.currentStep,
child: widget.steps[i].content, child: ClipRect(
clipBehavior: widget.clipBehavior,
child: widget.steps[i].content,
),
), ),
); );
} }

View File

@ -1720,6 +1720,91 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async
)); ));
expect(lastConnector.width, equals(0.0)); expect(lastConnector.width, equals(0.0));
}); });
// This is a regression test for https://github.com/flutter/flutter/issues/66007.
testWidgets('Default Stepper clipBehavior', (WidgetTester tester) async {
Widget buildStepper({ required StepperType type }) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Stepper(
type: type,
steps: const <Step>[
Step(
title: Text('step1'),
content: Text('step1 content'),
),
Step(
title: Text('step2'),
content: Text('step2 content'),
),
],
),
),
),
);
}
ClipRect getContentClipRect() {
return tester.widget<ClipRect>(find.ancestor(
of: find.text('step1 content'),
matching: find.byType(ClipRect),
).first);
}
// Test vertical stepper with default clipBehavior.
await tester.pumpWidget(buildStepper(type: StepperType.vertical));
expect(getContentClipRect().clipBehavior, equals(Clip.none));
// Test horizontal stepper with default clipBehavior.
await tester.pumpWidget(buildStepper(type: StepperType.horizontal));
expect(getContentClipRect().clipBehavior, equals(Clip.none));
});
// This is a regression test for https://github.com/flutter/flutter/issues/66007.
testWidgets('Stepper steps can be clipped', (WidgetTester tester) async {
Widget buildStepper({ required StepperType type, required Clip clipBehavior }) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Stepper(
clipBehavior: clipBehavior,
type: type,
steps: const <Step>[
Step(
title: Text('step1'),
content: Text('step1 content'),
),
Step(
title: Text('step2'),
content: Text('step2 content'),
),
],
),
),
),
);
}
ClipRect getContentClipRect() {
return tester.widget<ClipRect>(find.ancestor(
of: find.text('step1 content'),
matching: find.byType(ClipRect),
).first);
}
// Test vertical stepper with clipBehavior set to Clip.hardEdge.
await tester.pumpWidget(buildStepper(type: StepperType.vertical, clipBehavior: Clip.hardEdge));
expect(getContentClipRect().clipBehavior, equals(Clip.hardEdge));
// Test horizontal stepper with clipBehavior set to Clip.hardEdge.
await tester.pumpWidget(buildStepper(type: StepperType.horizontal, clipBehavior: Clip.hardEdge));
expect(getContentClipRect().clipBehavior, equals(Clip.hardEdge));
});
} }
class _TappableColorWidget extends StatefulWidget { class _TappableColorWidget extends StatefulWidget {