From f40a99ce7eb5d927b986b090bf832404b4e6fc05 Mon Sep 17 00:00:00 2001 From: Mairramer <50643541+Mairramer@users.noreply.github.com> Date: Fri, 12 Jan 2024 13:35:08 -0300 Subject: [PATCH] Adds support for StepStyle visual property bundle to the Step widget (#140825) Fixes #140770 and #103124 Adds the possibility of passing a height and width to icons. And also a margin for the distance of the lines between the icons. --- .../lib/material/stepper/step_style.0.dart | 105 +++++++ .../material/stepper/step_style.0_test.dart | 72 +++++ .../flutter/lib/src/material/stepper.dart | 276 ++++++++++++++++-- .../flutter/test/material/stepper_test.dart | 104 ++++++- 4 files changed, 534 insertions(+), 23 deletions(-) create mode 100644 examples/api/lib/material/stepper/step_style.0.dart create mode 100644 examples/api/test/material/stepper/step_style.0_test.dart diff --git a/examples/api/lib/material/stepper/step_style.0.dart b/examples/api/lib/material/stepper/step_style.0.dart new file mode 100644 index 00000000000..2841a0e8868 --- /dev/null +++ b/examples/api/lib/material/stepper/step_style.0.dart @@ -0,0 +1,105 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [StepStyle]. + +void main() => runApp(const StepStyleExampleApp()); + +class StepStyleExampleApp extends StatelessWidget { + + const StepStyleExampleApp({ super.key }); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Step Style Example')), + body: const Center( + child: StepStyleExample(), + ), + ), + ); + } +} + +class StepStyleExample extends StatefulWidget { + const StepStyleExample({ super.key }); + + @override + State createState() => _StepStyleExampleState(); +} + +class _StepStyleExampleState extends State { + final StepStyle _stepStyle = StepStyle( + connectorThickness: 10, + color: Colors.white, + connectorColor: Colors.red, + indexStyle: const TextStyle( + color: Colors.black, + fontSize: 20, + ), + border: Border.all( + width: 2, + ), + ); + + @override + Widget build(BuildContext context) { + return Stepper( + type: StepperType.horizontal, + stepIconHeight: 48, + stepIconWidth: 48, + stepIconMargin: EdgeInsets.zero, + steps: [ + Step( + title: const SizedBox.shrink(), + content: const SizedBox.shrink(), + isActive: true, + stepStyle: _stepStyle, + ), + Step( + title: const SizedBox.shrink(), + content: const SizedBox.shrink(), + isActive: true, + stepStyle: _stepStyle.copyWith( + connectorColor: Colors.orange, + gradient: const LinearGradient( + colors: [ + Colors.white, + Colors.black, + ], + ), + ), + ), + Step( + title: const SizedBox.shrink(), + content: const SizedBox.shrink(), + isActive: true, + stepStyle: _stepStyle.copyWith( + connectorColor: Colors.blue, + ), + ), + Step( + title: const SizedBox.shrink(), + content: const SizedBox.shrink(), + isActive: true, + stepStyle: _stepStyle.merge( + StepStyle( + color: Colors.white, + indexStyle: const TextStyle( + color: Colors.black, + fontSize: 20, + ), + border: Border.all( + width: 2, + ), + ), + ), + ), + ], + ); + } +} diff --git a/examples/api/test/material/stepper/step_style.0_test.dart b/examples/api/test/material/stepper/step_style.0_test.dart new file mode 100644 index 00000000000..db15bc72c14 --- /dev/null +++ b/examples/api/test/material/stepper/step_style.0_test.dart @@ -0,0 +1,72 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/stepper/step_style.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('StepStyle Smoke Test', (WidgetTester tester) async { + await tester.pumpWidget( + const example.StepStyleExampleApp(), + ); + + expect(find.widgetWithText(AppBar, 'Step Style Example'), findsOneWidget); + + final Stepper stepper = tester.widget(find.byType(Stepper)); + // Check that the stepper has the correct properties. + expect(stepper.type, StepperType.horizontal); + expect(stepper.stepIconHeight, 48); + expect(stepper.stepIconWidth, 48); + expect(stepper.stepIconMargin, EdgeInsets.zero); + + // Check that the first step has the correct properties. + final Step firstStep = stepper.steps[0]; + expect(firstStep.title, isA()); + expect(firstStep.content, isA()); + expect(firstStep.isActive, true); + expect(firstStep.stepStyle?.connectorThickness, 10); + expect(firstStep.stepStyle?.color, Colors.white); + expect(firstStep.stepStyle?.connectorColor, Colors.red); + expect(firstStep.stepStyle?.indexStyle?.color, Colors.black); + expect(firstStep.stepStyle?.indexStyle?.fontSize, 20); + expect(firstStep.stepStyle?.border, Border.all(width: 2)); + + // Check that the second step has the correct properties. + final Step secondStep = stepper.steps[1]; + expect(secondStep.title, isA()); + expect(secondStep.content, isA()); + expect(secondStep.isActive, true); + expect(secondStep.stepStyle?.connectorThickness, 10); + expect(secondStep.stepStyle?.connectorColor, Colors.orange); + expect(secondStep.stepStyle?.gradient, const LinearGradient( + colors: [ + Colors.white, + Colors.black, + ], + )); + + // Check that the third step has the correct properties. + final Step thirdStep = stepper.steps[2]; + expect(thirdStep.title, isA()); + expect(thirdStep.content, isA()); + expect(thirdStep.isActive, true); + expect(thirdStep.stepStyle?.connectorThickness, 10); + expect(thirdStep.stepStyle?.color, Colors.white); + expect(thirdStep.stepStyle?.connectorColor, Colors.blue); + expect(thirdStep.stepStyle?.indexStyle?.color, Colors.black); + expect(thirdStep.stepStyle?.indexStyle?.fontSize, 20); + expect(thirdStep.stepStyle?.border, Border.all(width: 2)); + + // Check that the fourth step has the correct properties. + final Step fourthStep = stepper.steps[3]; + expect(fourthStep.title, isA()); + expect(fourthStep.content, isA()); + expect(fourthStep.isActive, true); + expect(fourthStep.stepStyle?.color, Colors.white); + expect(fourthStep.stepStyle?.indexStyle?.color, Colors.black); + expect(fourthStep.stepStyle?.indexStyle?.fontSize, 20); + expect(fourthStep.stepStyle?.border, Border.all(width: 2)); + }); +} diff --git a/packages/flutter/lib/src/material/stepper.dart b/packages/flutter/lib/src/material/stepper.dart index 470fe0975f9..2e6a5659511 100644 --- a/packages/flutter/lib/src/material/stepper.dart +++ b/packages/flutter/lib/src/material/stepper.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'button_style.dart'; @@ -68,6 +69,7 @@ class ControlsDetails { 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. @@ -121,7 +123,9 @@ const Color _kCircleActiveDark = Colors.black87; const Color _kDisabledLight = Colors.black38; const Color _kDisabledDark = Colors.white38; const double _kStepSize = 24.0; -const double _kTriangleHeight = _kStepSize * 0.866025; // Triangle height. sqrt(3.0) / 2.0 +const double _kTriangleSqrt = 0.866025; // sqrt(3.0) / 2.0 +const double _kTriangleHeight = _kStepSize * _kTriangleSqrt; +const double _kMaxStepSize = 80.0; /// A material step used in [Stepper]. The step can have a title and subtitle, /// an icon within its circle, some content and a state that governs its @@ -141,6 +145,7 @@ class Step { this.state = StepState.indexed, this.isActive = false, this.label, + this.stepStyle, }); /// The title of the step that typically describes it. @@ -167,6 +172,9 @@ class Step { /// Only [StepperType.horizontal], Optional widget that appears under the [title]. /// By default, uses the `bodyLarge` theme. final Widget? label; + + /// Optional overrides for the step's default visual configuration. + final StepStyle? stepStyle; } /// A material stepper widget that displays progress through a sequence of @@ -211,7 +219,18 @@ class Stepper extends StatefulWidget { this.connectorColor, this.connectorThickness, this.stepIconBuilder, - }) : assert(0 <= currentStep && currentStep < steps.length); + this.stepIconHeight, + this.stepIconWidth, + this.stepIconMargin, + }) : assert(0 <= currentStep && currentStep < steps.length), + assert(stepIconHeight == null || (stepIconHeight >= _kStepSize && stepIconHeight <= _kMaxStepSize), + 'stepIconHeight must be greater than $_kStepSize and less or equal to $_kMaxStepSize'), + assert(stepIconWidth == null || (stepIconWidth >= _kStepSize && stepIconWidth <= _kMaxStepSize), + 'stepIconWidth must be greater than $_kStepSize and less or equal to $_kMaxStepSize'), + assert( + stepIconHeight == null || stepIconWidth == null || stepIconHeight == stepIconWidth, + 'If either stepIconHeight or stepIconWidth is specified, both must be specified and ' + 'the values must be equal.'); /// The steps of the stepper whose titles, subtitles, icons always get shown. /// @@ -338,6 +357,15 @@ class Stepper extends StatefulWidget { /// If null, the default icons will be used for respective [StepState]. final StepIconBuilder? stepIconBuilder; + /// Overrides the default step icon size height. + final double? stepIconHeight; + + /// Overrides the default step icon size width. + final double? stepIconWidth; + + /// Overrides the default step icon margin. + final EdgeInsets? stepIconMargin; + @override State createState() => _StepperState(); } @@ -369,6 +397,16 @@ class _StepperState extends State with TickerProviderStateMixin { } } + EdgeInsetsGeometry? get _stepIconMargin => widget.stepIconMargin; + + double? get _stepIconHeight => widget.stepIconHeight; + + double? get _stepIconWidth => widget.stepIconWidth; + + double get _heightFactor { + return (_isLabel() && _stepIconHeight != null) ? 2.5 : 2.0; + } + bool _isFirst(int index) { return index == 0; } @@ -394,6 +432,10 @@ class _StepperState extends State with TickerProviderStateMixin { return false; } + StepStyle? _stepStyle(int index) { + return widget.steps[index].stepStyle; + } + Color _connectorColor(bool isActive) { final ColorScheme colorScheme = Theme.of(context).colorScheme; final Set states = { @@ -421,12 +463,15 @@ class _StepperState extends State with TickerProviderStateMixin { if (icon != null) { return icon; } + TextStyle? textStyle = _stepStyle(index)?.indexStyle; + textStyle ??= isDarkActive ? _kStepStyle.copyWith(color: Colors.black87) : _kStepStyle; + switch (state) { case StepState.indexed: case StepState.disabled: return Text( '${index + 1}', - style: isDarkActive ? _kStepStyle.copyWith(color: Colors.black87) : _kStepStyle, + style: textStyle, ); case StepState.editing: return Icon( @@ -441,7 +486,7 @@ class _StepperState extends State with TickerProviderStateMixin { size: 18.0, ); case StepState.error: - return const Text('!', style: _kStepStyle); + return const Center(child: Text('!', style: _kStepStyle)); } } @@ -464,15 +509,18 @@ class _StepperState extends State with TickerProviderStateMixin { Widget _buildCircle(int index, bool oldState) { return Container( - margin: const EdgeInsets.symmetric(vertical: 8.0), - width: _kStepSize, - height: _kStepSize, + margin:_stepIconMargin ?? const EdgeInsets.symmetric(vertical: 8.0), + width: _stepIconWidth ?? _kStepSize, + height: _stepIconHeight ?? _kStepSize, child: AnimatedContainer( curve: Curves.fastOutSlowIn, duration: kThemeAnimationDuration, decoration: BoxDecoration( - color: _circleColor(index), + color: _stepStyle(index)?.color ?? _circleColor(index), shape: BoxShape.circle, + border: _stepStyle(index)?.border, + boxShadow: _stepStyle(index)?.boxShadow != null ? [_stepStyle(index)!.boxShadow!] : null, + gradient: _stepStyle(index)?.gradient, ), child: Center( child: _buildCircleChild(index, oldState && widget.steps[index].state == StepState.error), @@ -482,17 +530,20 @@ class _StepperState extends State with TickerProviderStateMixin { } Widget _buildTriangle(int index, bool oldState) { + Color? color = _stepStyle(index)?.errorColor; + color ??= _isDark() ? _kErrorDark : _kErrorLight; + return Container( - margin: const EdgeInsets.symmetric(vertical: 8.0), - width: _kStepSize, - height: _kStepSize, + margin: _stepIconMargin ?? const EdgeInsets.symmetric(vertical: 8.0), + width: _stepIconWidth ?? _kStepSize, + height: _stepIconHeight ?? _kStepSize, child: Center( child: SizedBox( - width: _kStepSize, - height: _kTriangleHeight, // Height of 24dp-long-sided equilateral triangle. + width: _stepIconWidth ?? _kStepSize, + height: _stepIconHeight != null ? _stepIconHeight! * _kTriangleSqrt : _kTriangleHeight, child: CustomPaint( painter: _TrianglePainter( - color: _isDark() ? _kErrorDark : _kErrorLight, + color: color, ), child: Align( alignment: const Alignment(0.0, 0.8), // 0.8 looks better than the geometrical 0.33. @@ -724,14 +775,25 @@ class _StepperState extends State with TickerProviderStateMixin { } Widget _buildVerticalBody(int index) { + final double? marginLeft = _stepIconMargin?.resolve(TextDirection.ltr).left; + final double? marginRight = _stepIconMargin?.resolve(TextDirection.ltr).right; + final double? additionalMarginLeft = marginLeft != null ? marginLeft / 2.0 : null; + final double? additionalMarginRight = marginRight != null ? marginRight / 2.0 : null; + return Stack( children: [ PositionedDirectional( - start: 24.0, + // When use margin affects the left or right side of the child, we + // need to add half of the margin to the start or end of the child + // respectively to get the correct positioning. + start: 24.0 + (additionalMarginLeft ?? 0.0) + (additionalMarginRight ?? 0.0), top: 0.0, bottom: 0.0, child: SizedBox( - width: 24.0, + // The line is drawn from the center of the circle vertically until + // it reaches the bottom and then horizontally to the edge of the + // stepper. + width: _stepIconWidth ?? _kStepSize, child: Center( child: SizedBox( width: widget.connectorThickness ?? 1.0, @@ -745,8 +807,10 @@ class _StepperState extends State with TickerProviderStateMixin { AnimatedCrossFade( firstChild: Container(height: 0.0), secondChild: Container( - margin: widget.margin ?? const EdgeInsetsDirectional.only( - start: 60.0, + margin: EdgeInsetsDirectional.only( + // Adjust [controlsBuilder] padding so that the content is + // centered vertically. + start: 60.0 + (marginLeft ?? 0.0), end: 24.0, bottom: 24.0, ), @@ -821,7 +885,7 @@ class _StepperState extends State with TickerProviderStateMixin { ), ), Container( - margin: const EdgeInsetsDirectional.only(start: 12.0), + margin: _stepIconMargin ?? const EdgeInsetsDirectional.only(start: 12.0), child: _buildHeaderText(i), ), ], @@ -831,9 +895,9 @@ class _StepperState extends State with TickerProviderStateMixin { Expanded( child: Container( key: Key('line$i'), - margin: const EdgeInsets.symmetric(horizontal: 8.0), - height: widget.connectorThickness ?? 1.0, - color: _connectorColor(widget.steps[i+1].isActive), + margin: _stepIconMargin ?? const EdgeInsets.symmetric(horizontal: 8.0), + height: widget.steps[i].stepStyle?.connectorThickness ?? widget.connectorThickness ?? 1.0, + color: widget.steps[i].stepStyle?.connectorColor ?? _connectorColor(widget.steps[i].isActive), ), ), ], @@ -856,6 +920,7 @@ class _StepperState extends State with TickerProviderStateMixin { elevation: widget.elevation ?? 2, child: Container( margin: const EdgeInsets.symmetric(horizontal: 24.0), + height: _stepIconHeight != null ? _stepIconHeight! * _heightFactor : null, child: Row( children: children, ), @@ -938,3 +1003,170 @@ class _TrianglePainter extends CustomPainter { ); } } + +/// This class is used to override the default visual properties of [Step] widgets within a [Stepper]. +/// +/// To customize the appearance of a [Step] create an instance of this class with non-null parameters +/// for the step properties whose default value you want to override. +/// +/// Example usage: +/// ```dart +/// Step( +/// title: const Text('Step 1'), +/// content: const Text('Content for Step 1'), +/// stepStyle: StepStyle( +/// color: Colors.blue, +/// errorColor: Colors.red, +/// border: Border.all(color: Colors.grey), +/// boxShadow: const BoxShadow(blurRadius: 3.0, color: Colors.black26), +/// gradient: const LinearGradient(colors: [Colors.red, Colors.blue]), +/// indexStyle: const TextStyle(color: Colors.white), +/// ), +/// ) +/// ``` +/// +/// {@tool dartpad} +/// An example that uses [StepStyle] to customize the appearance of each [Step] in a [Stepper]. +/// +/// ** See code in examples/api/lib/material/stepper/step_style.0.dart ** +/// {@end-tool} + +@immutable +class StepStyle with Diagnosticable { + /// Constructs a [StepStyle]. + const StepStyle({ + this.color, + this.errorColor, + this.connectorColor, + this.connectorThickness, + this.border, + this.boxShadow, + this.gradient, + this.indexStyle, + }); + + /// Overrides the default color of the circle in the step. + final Color? color; + + /// Overrides the default color of the error indicator in the step. + final Color? errorColor; + + /// Overrides the default color of the connector line between two steps. + /// + /// This property only applies when [Stepper.type] is [StepperType.horizontal]. + final Color? connectorColor; + + /// Overrides the default thickness of the connector line between two steps. + /// + /// This property only applies when [Stepper.type] is [StepperType.horizontal]. + final double? connectorThickness; + + /// Add a border around the step. + /// + /// Will be applied to the circle in the step. + final BoxBorder? border; + + /// Add a shadow around the step. + final BoxShadow? boxShadow; + + /// Add a gradient around the step. + /// + /// If [gradient] is specified, [color] will be ignored. + final Gradient? gradient; + + /// Overrides the default style of the index in the step. + final TextStyle? indexStyle; + + /// Returns a copy of this ButtonStyle with the given fields replaced with + /// the new values. + StepStyle copyWith({ + Color? color, + Color? errorColor, + Color? connectorColor, + double? connectorThickness, + BoxBorder? border, + BoxShadow? boxShadow, + Gradient? gradient, + TextStyle? indexStyle, + }) { + return StepStyle( + color: color ?? this.color, + errorColor: errorColor ?? this.errorColor, + connectorColor: connectorColor ?? this.connectorColor, + connectorThickness: connectorThickness ?? this.connectorThickness, + border: border ?? this.border, + boxShadow: boxShadow ?? this.boxShadow, + gradient: gradient ?? this.gradient, + indexStyle: indexStyle ?? this.indexStyle, + ); + } + + /// Returns a copy of this StepStyle where the non-null fields in [stepStyle] + /// have replaced the corresponding null fields in this StepStyle. + /// + /// In other words, [stepStyle] is used to fill in unspecified (null) fields + /// this StepStyle. + StepStyle merge(StepStyle? stepStyle) { + if (stepStyle == null) { + return this; + } + return copyWith( + color: stepStyle.color, + errorColor: stepStyle.errorColor, + connectorColor: stepStyle.connectorColor, + connectorThickness: stepStyle.connectorThickness, + border: stepStyle.border, + boxShadow: stepStyle.boxShadow, + gradient: stepStyle.gradient, + indexStyle: stepStyle.indexStyle, + ); + } + + @override + int get hashCode { + return Object.hash( + color, + errorColor, + connectorColor, + connectorThickness, + border, + boxShadow, + gradient, + indexStyle, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is StepStyle && + other.color == color && + other.errorColor == errorColor && + other.connectorColor == connectorColor && + other.connectorThickness == connectorThickness && + other.border == border && + other.boxShadow == boxShadow && + other.gradient == gradient && + other.indexStyle == indexStyle; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + final ThemeData theme = ThemeData.fallback(); + final TextTheme defaultTextTheme = theme.textTheme; + properties.add(ColorProperty('color', color, defaultValue: null)); + properties.add(ColorProperty('errorColor', errorColor, defaultValue: null)); + properties.add(ColorProperty('connectorColor', connectorColor, defaultValue: null)); + properties.add(DoubleProperty('connectorThickness', connectorThickness, defaultValue: null)); + properties.add(DiagnosticsProperty('border', border, defaultValue: null)); + properties.add(DiagnosticsProperty('boxShadow', boxShadow, defaultValue: null)); + properties.add(DiagnosticsProperty('gradient', gradient, defaultValue: null)); + properties.add(DiagnosticsProperty('indexStyle', indexStyle, defaultValue: defaultTextTheme.bodyLarge)); + } +} diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart index 4e80da4bd1e..e21bc6fb4cd 100644 --- a/packages/flutter/test/material/stepper_test.dart +++ b/packages/flutter/test/material/stepper_test.dart @@ -1530,7 +1530,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(circleColor('1'), selectedColor); expect(circleColor('2'), disabledColor); // in two steps case there will be single line - expect(lineColor('line0'), disabledColor); + expect(lineColor('line0'), selectedColor); // now hitting step two await tester.tap(find.text('step2')); @@ -1587,6 +1587,108 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(find.text('!'), findsOneWidget); }); + testWidgets('StepperProperties test', (WidgetTester tester) async { + const Widget widget = SizedBox.shrink(); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Stepper( + stepIconHeight: 24, + stepIconWidth: 24, + stepIconMargin: const EdgeInsets.all(8), + steps: List.generate(3, (int index) { + return Step( + title: Text('Step $index'), + content: widget, + ); + }), + ), + ), + ), + ); + + final Finder stepperFinder = find.byType(Stepper); + final Stepper stepper = tester.widget(stepperFinder); + + expect(stepper.stepIconHeight, 24); + expect(stepper.stepIconWidth, 24); + expect(stepper.stepIconMargin, const EdgeInsets.all(8)); + }); + + testWidgets('StepStyle test', (WidgetTester tester) async { + final StepStyle stepStyle = StepStyle( + color: Colors.white, + errorColor: Colors.orange, + connectorColor: Colors.red, + connectorThickness: 2, + border: Border.all(), + gradient: const LinearGradient( + colors: [Colors.red, Colors.blue], + ), + indexStyle: const TextStyle(color: Colors.black), + ); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Stepper( + steps: [ + Step( + title: const Text('Regular title'), + content: const Text('Text content'), + stepStyle: stepStyle, + ), + ], + ), + ), + ), + ); + + final Finder stepperFinder = find.byType(Stepper); + final Stepper stepper = tester.widget(stepperFinder); + final StepStyle? style = stepper.steps.first.stepStyle; + + expect(style?.color, stepStyle.color); + expect(style?.errorColor, stepStyle.errorColor); + expect(style?.connectorColor, stepStyle.connectorColor); + expect(style?.connectorThickness, stepStyle.connectorThickness); + expect(style?.border, stepStyle.border); + expect(style?.gradient, stepStyle.gradient); + expect(style?.indexStyle, stepStyle.indexStyle); + + //copyWith + final StepStyle newStyle = stepStyle.copyWith( + color: Colors.black, + errorColor: Colors.red, + connectorColor: Colors.blue, + connectorThickness: 3, + border: Border.all(), + gradient: const LinearGradient( + colors: [Colors.red, Colors.blue], + ), + indexStyle: const TextStyle(color: Colors.black), + ); + + expect(newStyle.color, Colors.black); + expect(newStyle.errorColor, Colors.red); + expect(newStyle.connectorColor, Colors.blue); + expect(newStyle.connectorThickness, 3); + expect(newStyle.border, stepStyle.border); + expect(newStyle.gradient, stepStyle.gradient); + expect(newStyle.indexStyle, stepStyle.indexStyle); + + //merge + final StepStyle mergedStyle = stepStyle.merge(newStyle); + + expect(mergedStyle.color, Colors.black); + expect(mergedStyle.errorColor, Colors.red); + expect(mergedStyle.connectorColor, Colors.blue); + expect(mergedStyle.connectorThickness, 3); + expect(mergedStyle.border, stepStyle.border); + expect(mergedStyle.gradient, stepStyle.gradient); + expect(mergedStyle.indexStyle, stepStyle.indexStyle); + }); } class _TappableColorWidget extends StatefulWidget {