Skip to content

Commit a939d9d

Browse files
authored
[Stepper] Add Alternative Label on horizontal-type Stepper (#91496)
* Add label on Step * Add _buildLabelText, _labelStyle * Update _buildHorizontal with _buildLabelText * Fix Redundant Center widgets * Fix Unnecessary Column Widget * Fix Unnecessary import * Fix Unnecessary Container * Set label style to bodyText1 to match title style * Update test for Alternative Label * Fix code format * Update label docstring * Update to test textstyle of label * Update to Test `TextStyles` of Label * Fix typo in inline comments * Update for Stepper type to `horizontal` * Restore `_buildVerticalHeader` * Update docstring, `Only [StepperType.horizontal]` * Update for Readability * Add `_isLabel` method * Update `Horizontal height` with `_isLabel` * Update to make `_buildIcon` centered * Fix for `curly_braces_in_flow_control_structures`
1 parent fd294fa commit a939d9d

File tree

2 files changed

+138
-3
lines changed

2 files changed

+138
-3
lines changed

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

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ class Step {
138138
required this.content,
139139
this.state = StepState.indexed,
140140
this.isActive = false,
141+
this.label,
141142
}) : assert(title != null),
142143
assert(content != null),
143144
assert(state != null);
@@ -162,6 +163,10 @@ class Step {
162163

163164
/// Whether or not the step is active. The flag only influences styling.
164165
final bool isActive;
166+
167+
/// Only [StepperType.horizontal], Optional widget that appears under the [title].
168+
/// By default, uses the `bodyText1` theme.
169+
final Widget? label;
165170
}
166171

167172
/// A material stepper widget that displays progress through a sequence of
@@ -353,6 +358,15 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
353358
return Theme.of(context).brightness == Brightness.dark;
354359
}
355360

361+
bool _isLabel() {
362+
for (final Step step in widget.steps) {
363+
if (step.label != null) {
364+
return true;
365+
}
366+
}
367+
return false;
368+
}
369+
356370
Widget _buildLine(bool visible) {
357371
return Container(
358372
width: visible ? 1.0 : 0.0,
@@ -573,6 +587,27 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
573587
}
574588
}
575589

590+
TextStyle _labelStyle(int index) {
591+
final ThemeData themeData = Theme.of(context);
592+
final TextTheme textTheme = themeData.textTheme;
593+
594+
assert(widget.steps[index].state != null);
595+
switch (widget.steps[index].state) {
596+
case StepState.indexed:
597+
case StepState.editing:
598+
case StepState.complete:
599+
return textTheme.bodyText1!;
600+
case StepState.disabled:
601+
return textTheme.bodyText1!.copyWith(
602+
color: _isDark() ? _kDisabledDark : _kDisabledLight,
603+
);
604+
case StepState.error:
605+
return textTheme.bodyText1!.copyWith(
606+
color: _isDark() ? _kErrorDark : _kErrorLight,
607+
);
608+
}
609+
}
610+
576611
Widget _buildHeaderText(int index) {
577612
return Column(
578613
crossAxisAlignment: CrossAxisAlignment.start,
@@ -598,6 +633,17 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
598633
);
599634
}
600635

636+
Widget _buildLabelText(int index) {
637+
if (widget.steps[index].label != null) {
638+
return AnimatedDefaultTextStyle(
639+
style: _labelStyle(index),
640+
duration: kThemeAnimationDuration,
641+
child: widget.steps[index].label!,
642+
);
643+
}
644+
return const SizedBox();
645+
}
646+
601647
Widget _buildVerticalHeader(int index) {
602648
return Container(
603649
margin: const EdgeInsets.symmetric(horizontal: 24.0),
@@ -709,9 +755,14 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
709755
child: Row(
710756
children: <Widget>[
711757
SizedBox(
712-
height: 72.0,
713-
child: Center(
714-
child: _buildIcon(i),
758+
height: _isLabel() ? 104.0 : 72.0,
759+
child: Column(
760+
mainAxisAlignment: MainAxisAlignment.center,
761+
children: <Widget>[
762+
if (widget.steps[i].label != null) const SizedBox(height: 24.0,),
763+
Center(child: _buildIcon(i)),
764+
if (widget.steps[i].label != null) SizedBox(height : 24.0, child: _buildLabelText(i),),
765+
],
715766
),
716767
),
717768
Container(

packages/flutter/test/material/stepper_test.dart

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,90 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async
11441144

11451145
expect(material.margin, equals(margin));
11461146
});
1147+
1148+
testWidgets('Stepper with Alternative Label', (WidgetTester tester) async {
1149+
int index = 0;
1150+
late TextStyle bodyText1Style;
1151+
late TextStyle bodyText2Style;
1152+
late TextStyle captionStyle;
1153+
1154+
await tester.pumpWidget(
1155+
MaterialApp(
1156+
home: Material(
1157+
child: StatefulBuilder(
1158+
builder: (BuildContext context, StateSetter setState) {
1159+
bodyText1Style = Theme.of(context).textTheme.bodyText1!;
1160+
bodyText2Style = Theme.of(context).textTheme.bodyText2!;
1161+
captionStyle = Theme.of(context).textTheme.caption!;
1162+
return Stepper(
1163+
type: StepperType.horizontal,
1164+
currentStep: index,
1165+
onStepTapped: (int i) {
1166+
setState(() {
1167+
index = i;
1168+
});
1169+
},
1170+
steps: <Step>[
1171+
Step(
1172+
title: const Text('Title 1'),
1173+
content: const Text('Content 1'),
1174+
label: Text('Label 1', style: Theme.of(context).textTheme.caption),
1175+
),
1176+
Step(
1177+
title: const Text('Title 2'),
1178+
content: const Text('Content 2'),
1179+
label: Text('Label 2', style: Theme.of(context).textTheme.bodyText1),
1180+
),
1181+
Step(
1182+
title: const Text('Title 3'),
1183+
content: const Text('Content 3'),
1184+
label: Text('Label 3', style: Theme.of(context).textTheme.bodyText2),
1185+
),
1186+
],
1187+
);
1188+
}),
1189+
),
1190+
),
1191+
);
1192+
1193+
// Check Styles of Label Text Widgets before tapping steps
1194+
final Text label1TextWidget =
1195+
tester.widget<Text>(find.text('Label 1'));
1196+
final Text label3TextWidget =
1197+
tester.widget<Text>(find.text('Label 3'));
1198+
1199+
expect(captionStyle, label1TextWidget.style);
1200+
expect(bodyText2Style, label3TextWidget.style);
1201+
1202+
late Text selectedLabelTextWidget;
1203+
late Text nextLabelTextWidget;
1204+
1205+
// Tap to Step1 Label then, `index` become 0
1206+
await tester.tap(find.text('Label 1'));
1207+
expect(index, 0);
1208+
1209+
// Check Styles of Selected Label Text Widgets and Another Label Text Widget
1210+
selectedLabelTextWidget =
1211+
tester.widget<Text>(find.text('Label ${index + 1}'));
1212+
expect(captionStyle, selectedLabelTextWidget.style);
1213+
nextLabelTextWidget =
1214+
tester.widget<Text>(find.text('Label ${index + 2}'));
1215+
expect(bodyText1Style, nextLabelTextWidget.style);
1216+
1217+
1218+
// Tap to Step2 Label then, `index` become 1
1219+
await tester.tap(find.text('Label 2'));
1220+
expect(index, 1);
1221+
1222+
// Check Styles of Selected Label Text Widgets and Another Label Text Widget
1223+
selectedLabelTextWidget =
1224+
tester.widget<Text>(find.text('Label ${index + 1}'));
1225+
expect(bodyText1Style, selectedLabelTextWidget.style);
1226+
1227+
nextLabelTextWidget =
1228+
tester.widget<Text>(find.text('Label ${index + 2}'));
1229+
expect(bodyText2Style, nextLabelTextWidget.style);
1230+
});
11471231
}
11481232

11491233
class _TappableColorWidget extends StatefulWidget {

0 commit comments

Comments
 (0)