From 7905388e1aeffbc5e59bdfca4a7c5f53a18cd6cd Mon Sep 17 00:00:00 2001 From: Luke Walton Date: Tue, 23 Jul 2024 16:18:49 +0100 Subject: [PATCH] fix(UX-1129): Add didUpdateStates to StatefulWidgets (#138) fix(UX-1129): Accordion, Chip, StepperInput didUpdateState fix: FAB expanded state fix: Make Navigation rail and list item stateless fix: Add min/max values in slider chore: Refactor existing didUpdateWidgets to best practices - remove setStates and put super call first test: Accordion, Chip, StepperInput didUpdateState, test: Improve existing accordion tests test: FAB expanded state test: Min/max values in slider --- .github/workflows/pull-request.yml | 2 +- .../lib/pages/components/button_example.dart | 14 +- .../components/stepper_input_example.dart | 2 +- .../pages/components/button_widgetbook.dart | 2 +- .../components/stepper_input_widgetbook.dart | 2 +- lib/src/components/accordion/accordion.dart | 14 +- .../components/breadcrumbs/breadcrumbs.dart | 8 +- .../components/button_group/button_group.dart | 8 - lib/src/components/chips/chip.dart | 8 + lib/src/components/components.dart | 2 +- lib/src/components/dropdown/dropdown.dart | 2 +- lib/src/components/fabs/fab.dart | 18 ++- .../list_item/dropdown_list_item.dart | 2 +- .../list_item/notification_list_item.dart | 30 ++-- .../navigation_rail/navigation_rail.dart | 28 ++-- lib/src/components/pagination/pagination.dart | 4 +- .../components/select_input/select_input.dart | 4 +- lib/src/components/slider/slider.dart | 18 ++- .../stepper_input/stepper_input.dart | 69 +++++--- lib/src/components/text_input/text_input.dart | 6 +- .../top_app_bar/search_top_app_bar.dart | 3 +- .../components/accordion/accordion_test.dart | 153 ++++++++++++++++++ test/src/components/chips/chip_test.dart | 101 ++++++++++++ test/src/components/fabs/fab_test.dart | 44 ++++- test/src/components/slider/slider_test.dart | 35 ++++ .../stepper input/stepper_input_test.dart | 62 +++++++ 26 files changed, 528 insertions(+), 113 deletions(-) create mode 100644 test/src/components/accordion/accordion_test.dart create mode 100644 test/src/components/chips/chip_test.dart create mode 100644 test/src/components/slider/slider_test.dart create mode 100644 test/src/components/stepper input/stepper_input_test.dart diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a6133df8..4a6298fc 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -21,7 +21,7 @@ jobs: with: cache: true - run: dart run build_runner build --delete-conflicting-outputs - - uses: ZebraDevs/flutter-code-quality@v1.0.6 + - uses: ZebraDevs/flutter-code-quality@v1.0.7 with: token: ${{secrets.GITHUB_TOKEN}} diff --git a/example/lib/pages/components/button_example.dart b/example/lib/pages/components/button_example.dart index 6630dc9f..517d710c 100644 --- a/example/lib/pages/components/button_example.dart +++ b/example/lib/pages/components/button_example.dart @@ -38,13 +38,13 @@ class _ButtonExampleState extends State { scrollController: _scrollController, label: 'Small Circle Primary', size: ZetaFabSize.small, - initiallyExpanded: false, + expanded: false, shape: ZetaWidgetBorder.full, type: ZetaFabType.primary, ), ZetaFAB( scrollController: _scrollController, - initiallyExpanded: false, + expanded: false, label: 'Small Rounded Primary', size: ZetaFabSize.small, shape: ZetaWidgetBorder.rounded, @@ -56,7 +56,7 @@ class _ButtonExampleState extends State { label: 'Small Sharp Primary', size: ZetaFabSize.small, shape: ZetaWidgetBorder.sharp, - initiallyExpanded: false, + expanded: false, type: ZetaFabType.inverse, onPressed: () => setFab(2), ), @@ -66,7 +66,7 @@ class _ButtonExampleState extends State { size: ZetaFabSize.large, shape: ZetaWidgetBorder.full, type: ZetaFabType.secondary, - initiallyExpanded: false, + expanded: false, onPressed: () => setFab(3), ), ZetaFAB( @@ -74,7 +74,7 @@ class _ButtonExampleState extends State { label: 'Large Rounded Secondary', size: ZetaFabSize.large, shape: ZetaWidgetBorder.rounded, - initiallyExpanded: false, + expanded: false, type: ZetaFabType.inverse, onPressed: () => setFab(4), ), @@ -84,7 +84,7 @@ class _ButtonExampleState extends State { size: ZetaFabSize.large, shape: ZetaWidgetBorder.sharp, type: ZetaFabType.primary, - initiallyExpanded: false, + expanded: false, onPressed: () => setFab(5), ), ]; @@ -93,7 +93,7 @@ class _ButtonExampleState extends State { return ExampleScaffold( name: 'Button', floatingActionButton: ZetaFAB( - initiallyExpanded: true, + expanded: true, icon: theFab.icon, label: theFab.label, scrollController: _scrollController, diff --git a/example/lib/pages/components/stepper_input_example.dart b/example/lib/pages/components/stepper_input_example.dart index e1e94b3b..d0d5cf27 100644 --- a/example/lib/pages/components/stepper_input_example.dart +++ b/example/lib/pages/components/stepper_input_example.dart @@ -23,7 +23,7 @@ class _StepperInputExampleState extends State { ZetaStepperInput( min: 0, max: 10, - initialValue: 5, + value: 5, onChange: (_) {}, ), ZetaStepperInput(), diff --git a/example/widgetbook/pages/components/button_widgetbook.dart b/example/widgetbook/pages/components/button_widgetbook.dart index dd008263..fdd7e383 100644 --- a/example/widgetbook/pages/components/button_widgetbook.dart +++ b/example/widgetbook/pages/components/button_widgetbook.dart @@ -139,7 +139,7 @@ class _FabWidgetState extends State { itemBuilder: (context, index) => Text("$index"), ), floatingActionButton: ZetaFAB( - initiallyExpanded: true, + expanded: true, scrollController: _scrollController, label: widget.c.knobs.string(label: 'Label', initialValue: 'Floating Action Button'), onPressed: widget.c.knobs.boolean(label: 'Disabled') ? null : () {}, diff --git a/example/widgetbook/pages/components/stepper_input_widgetbook.dart b/example/widgetbook/pages/components/stepper_input_widgetbook.dart index 162d71cd..70369084 100644 --- a/example/widgetbook/pages/components/stepper_input_widgetbook.dart +++ b/example/widgetbook/pages/components/stepper_input_widgetbook.dart @@ -8,7 +8,7 @@ import '../../utils/utils.dart'; Widget stepperInputUseCase(BuildContext context) { return WidgetbookScaffold( builder: (context, _) => ZetaStepperInput( - initialValue: context.knobs.int.input(label: 'Initial value'), + value: context.knobs.int.input(label: 'Value'), min: context.knobs.int.input(label: 'Minimum value', initialValue: 0), max: context.knobs.int.input(label: 'Maximum value', initialValue: 10), size: context.knobs.list( diff --git a/lib/src/components/accordion/accordion.dart b/lib/src/components/accordion/accordion.dart index 639c5c20..858d9d6b 100644 --- a/lib/src/components/accordion/accordion.dart +++ b/lib/src/components/accordion/accordion.dart @@ -67,18 +67,24 @@ class _ZetaAccordionState extends State with TickerProviderStateM parent: _controller, curve: Curves.fastOutSlowIn, ); - init(); + setInitialOpen(); + _disabled = widget.child == null; } @override void didUpdateWidget(ZetaAccordion oldWidget) { - init(); super.didUpdateWidget(oldWidget); + if (oldWidget.isOpen != widget.isOpen) { + setInitialOpen(); + } + if (oldWidget.child != widget.child) { + _disabled = widget.child == null; + } } - void init() { + void setInitialOpen() { _isOpen = widget.isOpen; - _disabled = widget.child == null; + _controller.value = _isOpen ? 1 : 0; } @override diff --git a/lib/src/components/breadcrumbs/breadcrumbs.dart b/lib/src/components/breadcrumbs/breadcrumbs.dart index b7c0fdda..ada9d7d9 100644 --- a/lib/src/components/breadcrumbs/breadcrumbs.dart +++ b/lib/src/components/breadcrumbs/breadcrumbs.dart @@ -56,13 +56,11 @@ class _ZetaBreadCrumbsState extends State { @override void didUpdateWidget(ZetaBreadCrumbs oldWidget) { + super.didUpdateWidget(oldWidget); if (widget.children.length != _children.length) { - setState(() { - _selectedIndex = widget.children.length - 1; - _children = [...widget.children]; - }); + _selectedIndex = widget.children.length - 1; + _children = [...widget.children]; } - super.didUpdateWidget(oldWidget); } @override diff --git a/lib/src/components/button_group/button_group.dart b/lib/src/components/button_group/button_group.dart index 51a545fe..995c46b2 100644 --- a/lib/src/components/button_group/button_group.dart +++ b/lib/src/components/button_group/button_group.dart @@ -231,14 +231,6 @@ class _ZetaGroupButtonState extends State { _controller.dispose(); } - @override - void didUpdateWidget(ZetaGroupButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.onPressed != widget.onPressed) { - setState(() {}); - } - } - double get _padding => widget.isLarge ? ZetaSpacing.large : ZetaSpacing.medium; BorderSide _getBorderSide( diff --git a/lib/src/components/chips/chip.dart b/lib/src/components/chips/chip.dart index c96ca0a3..ec47e929 100644 --- a/lib/src/components/chips/chip.dart +++ b/lib/src/components/chips/chip.dart @@ -95,6 +95,14 @@ class _ZetaChipState extends State { selected = widget.selected ?? false; } + @override + void didUpdateWidget(covariant ZetaChip oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.selected != widget.selected) { + selected = widget.selected ?? false; + } + } + Widget _renderLeading(Color foregroundColor) { if (widget.leading.runtimeType == Icon) { return IconTheme(data: IconThemeData(color: foregroundColor, size: ZetaSpacing.xl_1), child: widget.leading!); diff --git a/lib/src/components/components.dart b/lib/src/components/components.dart index d8f562f5..4fbd61ab 100644 --- a/lib/src/components/components.dart +++ b/lib/src/components/components.dart @@ -45,7 +45,7 @@ export 'select_input/select_input.dart'; export 'slider/slider.dart'; export 'snack_bar/snack_bar.dart'; export 'stepper/stepper.dart'; -export 'stepper_input/stepper_input.dart'; +export 'stepper_input/stepper_input.dart' hide ZetaStepperInputState; export 'switch/zeta_switch.dart'; export 'tabs/tab.dart'; export 'tabs/tab_bar.dart'; diff --git a/lib/src/components/dropdown/dropdown.dart b/lib/src/components/dropdown/dropdown.dart index e62baf50..d415e375 100644 --- a/lib/src/components/dropdown/dropdown.dart +++ b/lib/src/components/dropdown/dropdown.dart @@ -202,7 +202,7 @@ class ZetaDropDownState extends State> { void didUpdateWidget(ZetaDropdown oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.value != widget.value) { - setState(_setSelectedItem); + _setSelectedItem(); } if (oldWidget.size != widget.size) { WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/src/components/fabs/fab.dart b/lib/src/components/fabs/fab.dart index bc3cd16e..a9bf8d70 100644 --- a/lib/src/components/fabs/fab.dart +++ b/lib/src/components/fabs/fab.dart @@ -37,10 +37,11 @@ class ZetaFAB extends StatefulWidget { this.size = ZetaFabSize.small, this.shape = ZetaWidgetBorder.full, this.icon = ZetaIcons.add, - this.initiallyExpanded, + bool? expanded, + @Deprecated('Please use expanded instead. ' 'Deprecated in 0.15.0') bool? initiallyExpanded, this.focusNode, super.key, - }); + }) : expanded = expanded ?? initiallyExpanded ?? label != null; /// Defines the color of the button. /// @@ -80,7 +81,9 @@ class ZetaFAB extends StatefulWidget { /// Whether the FAB starts as expanded. /// /// If [scrollController] or [label] are null, this is the permanent state of the FAB. - final bool? initiallyExpanded; + /// + /// If the [label] is not null, the FAB will initialize as expanded. + final bool expanded; /// {@macro flutter.widgets.Focus.focusNode} final FocusNode? focusNode; @@ -99,7 +102,7 @@ class ZetaFAB extends StatefulWidget { ..add(DiagnosticsProperty('scrollController', scrollController)) ..add(StringProperty('label', label)) ..add(DiagnosticsProperty('icon', icon)) - ..add(DiagnosticsProperty('initiallyExpanded', initiallyExpanded)) + ..add(DiagnosticsProperty('initiallyExpanded', expanded)) ..add(DiagnosticsProperty('focusNode', focusNode)); } } @@ -107,7 +110,6 @@ class ZetaFAB extends StatefulWidget { class _ZetaFABState extends State { @override Widget build(BuildContext context) { - final bool isExpanded = (widget.initiallyExpanded != null ? widget.initiallyExpanded! : widget.label != null); final colors = widget.type.colors(context); final backgroundColor = widget.type == ZetaFabType.inverse ? colors.shade80 : colors.shade60; @@ -116,7 +118,7 @@ class _ZetaFABState extends State { focusNode: widget.focusNode, style: ButtonStyle( padding: const WidgetStatePropertyAll(EdgeInsets.zero), - shape: WidgetStatePropertyAll(widget.shape.buttonShape(isExpanded: isExpanded, size: widget.size)), + shape: WidgetStatePropertyAll(widget.shape.buttonShape(isExpanded: widget.expanded, size: widget.size)), backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.disabled)) { return Zeta.of(context).colors.surfaceDisabled; @@ -142,14 +144,14 @@ class _ZetaFABState extends State { child: AnimatedContainer( duration: ZetaAnimationLength.normal, child: Padding( - padding: isExpanded + padding: widget.expanded ? const EdgeInsets.symmetric(horizontal: ZetaSpacingBase.x3_5, vertical: ZetaSpacing.medium) : EdgeInsets.all(widget.size.padding), child: Row( mainAxisSize: MainAxisSize.min, children: [ ZetaIcon(widget.icon, size: widget.size.iconSize), - if (isExpanded && widget.label != null) + if (widget.expanded && widget.label != null) Row( mainAxisSize: MainAxisSize.min, children: [Text(widget.label!, style: ZetaTextStyles.labelLarge)], diff --git a/lib/src/components/list_item/dropdown_list_item.dart b/lib/src/components/list_item/dropdown_list_item.dart index 66c831fd..8e1f1576 100644 --- a/lib/src/components/list_item/dropdown_list_item.dart +++ b/lib/src/components/list_item/dropdown_list_item.dart @@ -86,10 +86,10 @@ class _ZetaDropdownListItemState extends State with Single @override void didUpdateWidget(covariant ZetaDropdownListItem oldWidget) { + super.didUpdateWidget(oldWidget); if (oldWidget.expanded != widget.expanded) { _setExpanded(widget.expanded); } - super.didUpdateWidget(oldWidget); } @override diff --git a/lib/src/components/list_item/notification_list_item.dart b/lib/src/components/list_item/notification_list_item.dart index 9a44db5f..e65c8225 100644 --- a/lib/src/components/list_item/notification_list_item.dart +++ b/lib/src/components/list_item/notification_list_item.dart @@ -5,7 +5,7 @@ import '../../../zeta_flutter.dart'; /// Notification list items are used in notification lists. /// {@category Components} -class ZetaNotificationListItem extends ZetaStatefulWidget { +class ZetaNotificationListItem extends ZetaStatelessWidget { /// Constructor for [ZetaNotificationListItem] const ZetaNotificationListItem({ super.key, @@ -46,9 +46,6 @@ class ZetaNotificationListItem extends ZetaStatefulWidget { /// {@macro zeta-widget-semantic-label} final String? semanticLabel; - @override - State createState() => _ZetaNotificationListItemState(); - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -59,9 +56,7 @@ class ZetaNotificationListItem extends ZetaStatefulWidget { ..add(DiagnosticsProperty('showDivider', showDivider)) ..add(StringProperty('semanticLabel', semanticLabel)); } -} -class _ZetaNotificationListItemState extends State { @override Widget build(BuildContext context) { final colors = Zeta.of(context).colors; @@ -69,7 +64,7 @@ class _ZetaNotificationListItemState extends State { rounded: context.rounded, child: Semantics( explicitChildNodes: true, - label: widget.semanticLabel, + label: semanticLabel, button: true, child: DecoratedBox( decoration: _getStyle(colors), @@ -81,7 +76,7 @@ class _ZetaNotificationListItemState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - widget.leading, + leading, Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -92,13 +87,13 @@ class _ZetaNotificationListItemState extends State { MergeSemantics( child: Row( children: [ - if (!widget.notificationRead) + if (!notificationRead) ZetaIndicator( color: colors.blue, size: ZetaWidgetSize.small, ), Text( - widget.title, + title, style: ZetaTextStyles.labelLarge, ), ], @@ -106,9 +101,9 @@ class _ZetaNotificationListItemState extends State { ), Row( children: [ - if (widget.notificationTime != null) + if (notificationTime != null) Text( - widget.notificationTime!, + notificationTime!, style: ZetaTextStyles.bodySmall.apply(color: colors.textDisabled), ), Container( @@ -125,13 +120,13 @@ class _ZetaNotificationListItemState extends State { ), ], ), - widget.body, + body, ].gap(ZetaSpacing.minimum), ), ), ].gap(ZetaSpacing.small), ), - Container(alignment: Alignment.centerRight, child: widget.action), + Container(alignment: Alignment.centerRight, child: action), ], ).paddingAll(ZetaSpacing.small), ), @@ -141,11 +136,10 @@ class _ZetaNotificationListItemState extends State { BoxDecoration _getStyle(ZetaColors colors) { return BoxDecoration( - color: widget.notificationRead ? colors.surfacePrimary : colors.surfaceSelected, + color: notificationRead ? colors.surfacePrimary : colors.surfaceSelected, borderRadius: ZetaRadius.rounded, - border: (widget.showDivider ?? false) - ? Border(bottom: BorderSide(width: ZetaSpacing.minimum, color: colors.blue)) - : null, + border: + (showDivider ?? false) ? Border(bottom: BorderSide(width: ZetaSpacing.minimum, color: colors.blue)) : null, ); } } diff --git a/lib/src/components/navigation_rail/navigation_rail.dart b/lib/src/components/navigation_rail/navigation_rail.dart index 14b659e5..bacdd85c 100644 --- a/lib/src/components/navigation_rail/navigation_rail.dart +++ b/lib/src/components/navigation_rail/navigation_rail.dart @@ -9,7 +9,7 @@ import '../../../zeta_flutter.dart'; /// navigation item. /// Should be used with [ZetaNavigationRailItem]. /// {@category Components} -class ZetaNavigationRail extends ZetaStatefulWidget { +class ZetaNavigationRail extends ZetaStatelessWidget { /// Constructor for [ZetaNavigationRail]. const ZetaNavigationRail({ super.key, @@ -66,8 +66,6 @@ class ZetaNavigationRail extends ZetaStatefulWidget { /// {@macro zeta-widget-semantic-label} final String? semanticLabel; - @override - State createState() => _ZetaNavigationRailState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -82,34 +80,32 @@ class ZetaNavigationRail extends ZetaStatefulWidget { ..add(DiagnosticsProperty('wordWrap', wordWrap)) ..add(StringProperty('semanticLabel', semanticLabel)); } -} -class _ZetaNavigationRailState extends State { @override Widget build(BuildContext context) { return ZetaRoundedScope( rounded: context.rounded, child: Semantics( - label: widget.semanticLabel, + label: semanticLabel, child: Padding( - padding: widget.margin, + padding: margin, child: IntrinsicWidth( child: Column( children: [ - for (int i = 0; i < widget.items.length; i++) + for (int i = 0; i < items.length; i++) Row( children: [ Expanded( child: Padding( - padding: widget.itemSpacing, + padding: itemSpacing, child: _ZetaNavigationRailItemContent( - label: widget.items[i].label, - icon: widget.items[i].icon, - selected: widget.selectedIndex == i, - disabled: widget.items[i].disabled, - onTap: () => widget.onSelect?.call(i), - padding: widget.itemPadding, - wordWrap: widget.wordWrap, + label: items[i].label, + icon: items[i].icon, + selected: selectedIndex == i, + disabled: items[i].disabled, + onTap: () => onSelect?.call(i), + padding: itemPadding, + wordWrap: wordWrap, ), ), ), diff --git a/lib/src/components/pagination/pagination.dart b/lib/src/components/pagination/pagination.dart index 469b2d77..26cd7b8a 100644 --- a/lib/src/components/pagination/pagination.dart +++ b/lib/src/components/pagination/pagination.dart @@ -122,9 +122,7 @@ class _ZetaPaginationState extends State { void didUpdateWidget(covariant ZetaPagination oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.currentPage != widget.currentPage) { - setState(() { - _currentPage = widget.currentPage; - }); + _currentPage = widget.currentPage; } } diff --git a/lib/src/components/select_input/select_input.dart b/lib/src/components/select_input/select_input.dart index d5e1706b..8f6b93aa 100644 --- a/lib/src/components/select_input/select_input.dart +++ b/lib/src/components/select_input/select_input.dart @@ -103,10 +103,10 @@ class _ZetaSelectInputState extends State> { @override void didUpdateWidget(covariant ZetaSelectInput oldWidget) { + super.didUpdateWidget(oldWidget); if (oldWidget.initialValue != widget.initialValue) { - setState(_setInitialItem); + _setInitialItem(); } - super.didUpdateWidget(oldWidget); } void _setInitialItem() { diff --git a/lib/src/components/slider/slider.dart b/lib/src/components/slider/slider.dart index 66a85346..55ef8306 100644 --- a/lib/src/components/slider/slider.dart +++ b/lib/src/components/slider/slider.dart @@ -14,9 +14,13 @@ class ZetaSlider extends ZetaStatefulWidget { this.onChange, this.divisions, this.semanticLabel, + this.min = 0.0, + this.max = 1.0, }); - /// Double value to represent slider percentage + /// Double value to represent slider percentage. + /// + /// Default [min] / [max] are 0.0 and 1.0 respectively; this value should be between [min] and [max]. final double value; /// Callback to handle changing of slider @@ -30,6 +34,12 @@ class ZetaSlider extends ZetaStatefulWidget { /// {@macro zeta-widget-semantic-label} final String? semanticLabel; + /// Minimum value of the slider. + final double min; + + /// Maximum value of the slider. + final double max; + @override State createState() => _ZetaSliderState(); @override @@ -40,7 +50,9 @@ class ZetaSlider extends ZetaStatefulWidget { ..add(DoubleProperty('value', value)) ..add(ObjectFlagProperty?>.has('onChange', onChange)) ..add(IntProperty('divisions', divisions)) - ..add(StringProperty('semanticLabel', semanticLabel)); + ..add(StringProperty('semanticLabel', semanticLabel)) + ..add(DoubleProperty('max', max)) + ..add(DoubleProperty('min', min)); } } @@ -93,6 +105,8 @@ class _ZetaSliderState extends State { _selected = false; }); }, + min: widget.min, + max: widget.max, ), ), ), diff --git a/lib/src/components/stepper_input/stepper_input.dart b/lib/src/components/stepper_input/stepper_input.dart index 20f389ae..562ef77d 100644 --- a/lib/src/components/stepper_input/stepper_input.dart +++ b/lib/src/components/stepper_input/stepper_input.dart @@ -23,13 +23,15 @@ class ZetaStepperInput extends ZetaStatefulWidget { super.key, super.rounded, this.size = ZetaStepperInputSize.medium, - this.initialValue, + int? value, + @Deprecated('Use value instead. ' 'Deprecated in 0.15.0') int? initialValue, this.min, this.max, this.onChange, this.semanticDecrement, this.semanticIncrement, - }) : assert( + }) : value = value ?? initialValue, + assert( (min == null || (initialValue ?? 0) >= min) && (max == null || (initialValue ?? 0) <= max), 'Initial value must be inside given min and max values', ); @@ -40,7 +42,7 @@ class ZetaStepperInput extends ZetaStatefulWidget { /// The initial value of the stepper input. /// /// Must be in the bounds of [min] and [max] (if given). - final int? initialValue; + final int? value; /// The minimum value of the stepper input. final int? min; @@ -68,14 +70,14 @@ class ZetaStepperInput extends ZetaStatefulWidget { final String? semanticIncrement; @override - State createState() => _ZetaStepperInputState(); + State createState() => ZetaStepperInputState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('rounded', rounded)) ..add(EnumProperty('size', size)) - ..add(IntProperty('initialValue', initialValue)) + ..add(IntProperty('value', value)) ..add(IntProperty('min', min)) ..add(IntProperty('max', max)) ..add(ObjectFlagProperty?>.has('onChange', onChange)) @@ -84,18 +86,35 @@ class ZetaStepperInput extends ZetaStatefulWidget { } } -class _ZetaStepperInputState extends State { +/// Internal state for [ZetaStepperInput]. +/// +/// Not to be used directly. +@visibleForTesting +class ZetaStepperInputState extends State { final TextEditingController _controller = TextEditingController(); - int _value = 0; - bool get _disabled => widget.onChange == null; + + /// Current value of the stepper input. + int value = 0; + + /// Shortcut to check if the stepper input is disabled. + bool get disabled => widget.onChange == null; + + @override + void didUpdateWidget(covariant ZetaStepperInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value != oldWidget.value) { + value = widget.value!; + _controller.text = value.toString(); + } + } @override void initState() { super.initState(); - if (widget.initialValue != null) { - _value = widget.initialValue!; + if (widget.value != null) { + value = widget.value!; } - _controller.text = _value.toString(); + _controller.text = value.toString(); } @override @@ -109,7 +128,7 @@ class _ZetaStepperInputState extends State { return OutlineInputBorder( borderSide: BorderSide( - color: !_disabled ? colors.borderSubtle : colors.borderDisabled, + color: !disabled ? colors.borderSubtle : colors.borderDisabled, ), borderRadius: context.rounded ? ZetaRadius.minimal : ZetaRadius.none, ); @@ -136,10 +155,10 @@ class _ZetaStepperInputState extends State { } } - void _onChange(int value) { - if (!(widget.max != null && value > widget.max! || widget.min != null && value < widget.min!)) { + void _onChange(int newValue) { + if (!(widget.max != null && newValue > widget.max! || widget.min != null && newValue < widget.min!)) { setState(() { - _value = value; + value = newValue; }); _controller.text = value.toString(); widget.onChange?.call(value); @@ -152,8 +171,8 @@ class _ZetaStepperInputState extends State { icon: increase ? ZetaIcons.add : ZetaIcons.remove, type: ZetaButtonType.outlineSubtle, size: widget.size == ZetaStepperInputSize.medium ? ZetaWidgetSize.medium : ZetaWidgetSize.large, - onPressed: !_disabled && (increase ? _value != widget.max : _value != widget.min) - ? () => _onChange(_value + (increase ? 1 : -1)) + onPressed: !disabled && (increase ? value != widget.max : value != widget.min) + ? () => _onChange(value + (increase ? 1 : -1)) : null, ); } @@ -172,22 +191,22 @@ class _ZetaStepperInputState extends State { width: ZetaSpacing.xl_9, child: TextFormField( keyboardType: TextInputType.number, - enabled: !_disabled, + enabled: !disabled, controller: _controller, onChanged: _onTextChange, textAlign: TextAlign.center, inputFormatters: [FilteringTextInputFormatter.digitsOnly], style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: _disabled ? colors.textDisabled : null, + color: disabled ? colors.textDisabled : null, ), onTapOutside: (_) { if (_controller.text.isEmpty) { - _controller.text = _value.toString(); + _controller.text = value.toString(); } }, decoration: InputDecoration( filled: true, - fillColor: _disabled ? colors.surfaceDisabled : null, + fillColor: disabled ? colors.surfaceDisabled : null, contentPadding: EdgeInsets.zero, constraints: BoxConstraints(maxHeight: _height), border: _border, @@ -202,4 +221,12 @@ class _ZetaStepperInputState extends State { ), ); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('value', value)) + ..add(DiagnosticsProperty('disabled', disabled)); + } } diff --git a/lib/src/components/text_input/text_input.dart b/lib/src/components/text_input/text_input.dart index 6f64798f..14677de6 100644 --- a/lib/src/components/text_input/text_input.dart +++ b/lib/src/components/text_input/text_input.dart @@ -389,15 +389,13 @@ class ZetaTextInputState extends State implements ZetaFormFieldSt @override void didUpdateWidget(covariant ZetaTextInput oldWidget) { + super.didUpdateWidget(oldWidget); if (oldWidget.errorText != widget.errorText) { - setState(() { - _errorText = widget.errorText; - }); + _errorText = widget.errorText; } if (oldWidget.initialValue != widget.initialValue && widget.initialValue != null) { _controller.text = widget.initialValue!; } - super.didUpdateWidget(oldWidget); } @override diff --git a/lib/src/components/top_app_bar/search_top_app_bar.dart b/lib/src/components/top_app_bar/search_top_app_bar.dart index aa4febca..53bfd6a5 100644 --- a/lib/src/components/top_app_bar/search_top_app_bar.dart +++ b/lib/src/components/top_app_bar/search_top_app_bar.dart @@ -121,11 +121,10 @@ class _ZetaTopAppBarSearchFieldState extends State wit @override void didUpdateWidget(covariant ZetaTopAppBarSearchField oldWidget) { + super.didUpdateWidget(oldWidget); if (oldWidget.searchController != widget.searchController) { _setNextSearchState(); } - - super.didUpdateWidget(oldWidget); } @override diff --git a/test/src/components/accordion/accordion_test.dart b/test/src/components/accordion/accordion_test.dart new file mode 100644 index 00000000..7cc78fe4 --- /dev/null +++ b/test/src/components/accordion/accordion_test.dart @@ -0,0 +1,153 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../test_utils/test_app.dart'; +import '../../../test_utils/utils.dart'; + +void main() { + testWidgets('ZetaAccordion expands and collapses correctly', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaAccordion( + title: 'Accordion Title', + child: Text('Accordion Content'), + ), + ), + ); + + // Verify that the accordion is initially collapsed + final Finder accordionContent = find.byType(SizeTransition); + expect(accordionContent, findsOneWidget); + + final SizeTransition sizeTransition = tester.widget(accordionContent); + expect(sizeTransition.sizeFactor.value, 0); + + // Tap on the accordion to expand it + await tester.tap(find.text('Accordion Title')); + await tester.pumpAndSettle(); + + // Verify that the accordion is now expanded + expect(sizeTransition.sizeFactor.value, 1); + + // Tap on the accordion again to collapse it + await tester.tap(find.text('Accordion Title')); + await tester.pumpAndSettle(); + + expect(sizeTransition.sizeFactor.value, 0); + }); + + testWidgets('ZetaAccordion changes isOpen property correctly', (WidgetTester tester) async { + bool isOpen = false; + StateSetter? setState; + + await tester.pumpWidget( + TestApp( + home: StatefulBuilder( + builder: (context, setState2) { + setState = setState2; + return ZetaAccordion( + title: 'Accordion Title', + isOpen: isOpen, + child: const Text('Accordion Content'), + ); + }, + ), + ), + ); + + // Verify that the accordion is initially closed + final Finder accordionContent = find.byType(SizeTransition); + expect(accordionContent, findsOneWidget); + + final SizeTransition sizeTransition = tester.widget(accordionContent); + expect(sizeTransition.sizeFactor.value, 0); + + // Change isOpen property to true + setState?.call(() => isOpen = true); + + await tester.pumpAndSettle(); + + // Verify that the accordion is now open + expect(sizeTransition.sizeFactor.value, 1); + + // Change isOpen property to false + setState?.call(() => isOpen = false); + + await tester.pumpAndSettle(); + + // Verify that the accordion is now closed + expect(sizeTransition.sizeFactor.value, 0); + }); + + testWidgets('debugFillProperties works correctly', (WidgetTester tester) async { + final diagnostics = DiagnosticPropertiesBuilder(); + const ZetaAccordion( + title: 'Title', + ).debugFillProperties(diagnostics); + + expect(diagnostics.finder('title'), '"Title"'); + expect(diagnostics.finder('rounded'), 'null'); + expect(diagnostics.finder('contained'), 'false'); + expect(diagnostics.finder('isOpen'), 'false'); + }); + + testWidgets('Programatically change child', (WidgetTester tester) async { + Widget? child = const Text('Text 1'); + StateSetter? setState; + + await tester.pumpWidget( + StatefulBuilder( + builder: (context, setState2) { + setState = setState2; + return TestApp( + home: ZetaAccordion( + title: 'Accordion Title', + child: child, + ), + ); + }, + ), + ); + + final Finder accordionContent = find.text('Text 1'); + expect(accordionContent, findsOneWidget); + setState?.call(() => child = null); + await tester.pumpAndSettle(); + expect(accordionContent, findsNothing); + }); + + testWidgets('ZetaAccordion changes color on hover', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaAccordion( + title: 'Accordion Title', + child: Text('Accordion Content'), + ), + ), + ); + + final textButtonFinder = find.byType(TextButton); + final textButton = tester.widget(textButtonFinder); + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.byType(ZetaAccordion))); + await tester.pumpAndSettle(); + // Verify that the textButton color matches the hover color + expect( + textButton.style!.overlayColor?.resolve({WidgetState.hovered}), + ZetaColorBase.cool.shade20, + ); + expect( + textButton.style!.overlayColor?.resolve({WidgetState.focused}), + Colors.transparent, + ); + expect(textButton.style!.side?.resolve({WidgetState.focused})?.color, ZetaColorBase.blue.shade50); + }); +} diff --git a/test/src/components/chips/chip_test.dart b/test/src/components/chips/chip_test.dart new file mode 100644 index 00000000..0d1eb2d1 --- /dev/null +++ b/test/src/components/chips/chip_test.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../test_utils/test_app.dart'; + +void main() { + group('ZetaChip', () { + testWidgets('renders label correctly', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp(home: ZetaChip(label: 'Test Chip')), + ); + + expect(find.text('Test Chip'), findsOneWidget); + }); + + testWidgets('triggers onTap callback when tapped', (WidgetTester tester) async { + bool tapped = false; + + await tester.pumpWidget( + TestApp( + home: ZetaChip( + label: 'Test Chip', + onTap: () => tapped = true, + ), + ), + ); + + await tester.tap(find.byType(ZetaChip)); + expect(tapped, isTrue); + }); + + testWidgets('triggers onToggle callback when selected', (WidgetTester tester) async { + bool selected = false; + + await tester.pumpWidget( + TestApp( + home: ZetaChip( + label: 'Test Chip', + selected: selected, + onToggle: (value) => selected = value, + ), + ), + ); + + await tester.tap(find.byType(ZetaChip)); + expect(selected, isTrue); + }); + + testWidgets('renders leading widget correctly', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaChip( + label: 'Test Chip', + leading: Icon(Icons.check), + ), + ), + ); + + expect(find.byIcon(Icons.check), findsOneWidget); + }); + + testWidgets('renders trailing widget correctly', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaChip( + label: 'Test Chip', + trailing: Icon(Icons.close), + ), + ), + ); + + expect(find.byIcon(Icons.close), findsOneWidget); + }); + }); + + testWidgets('ZetaChip changes selected property correctly', (WidgetTester tester) async { + bool selected = false; + StateSetter? setState; + + await tester.pumpWidget( + TestApp( + home: StatefulBuilder( + builder: (context, setState2) { + setState = setState2; + return ZetaChip(label: 'Chip', selected: selected); + }, + ), + ), + ); + + final Finder iconFinder = find.byIcon(ZetaIcons.check_mark_round); + expect(iconFinder, findsNothing); + + // Change isOpen property to true + setState?.call(() => selected = true); + await tester.pumpAndSettle(); + + expect(iconFinder, findsOne); + }); +} diff --git a/test/src/components/fabs/fab_test.dart b/test/src/components/fabs/fab_test.dart index da430aa9..64c191ca 100644 --- a/test/src/components/fabs/fab_test.dart +++ b/test/src/components/fabs/fab_test.dart @@ -72,7 +72,7 @@ void main() { final fabFinder = find.byType(ZetaFAB); final ZetaFAB fab = tester.firstWidget(fabFinder); - expect(fab.initiallyExpanded, null); + expect(fab.expanded, false); expect(fab.type, ZetaFabType.inverse); expect(fab.shape, ZetaWidgetBorder.rounded); @@ -86,7 +86,7 @@ void main() { await tester.pumpWidget( TestApp( home: ZetaFAB( - initiallyExpanded: true, + expanded: true, onPressed: () {}, label: 'Label', type: ZetaFabType.secondary, @@ -98,7 +98,7 @@ void main() { final fabFinder = find.byType(ZetaFAB); final ZetaFAB fab = tester.firstWidget(fabFinder); - expect(fab.initiallyExpanded, true); + expect(fab.expanded, true); expect(fab.type, ZetaFabType.secondary); expect(fab.shape, ZetaWidgetBorder.sharp); @@ -113,7 +113,7 @@ void main() { await tester.pumpWidget( TestApp( home: ZetaFAB( - initiallyExpanded: true, + expanded: true, onPressed: () {}, label: 'Label', type: ZetaFabType.secondary, @@ -128,7 +128,7 @@ void main() { final filledButtonFinder = find.byType(FilledButton); final FilledButton filledButton = tester.firstWidget(filledButtonFinder); - expect(fab.initiallyExpanded, true); + expect(fab.expanded, true); expect(fab.type, ZetaFabType.secondary); expect(fab.shape, ZetaWidgetBorder.sharp); @@ -171,6 +171,7 @@ void main() { matchesGoldenFile(join(getCurrentPath('fabs'), 'FAB_disabled.png')), ); }); + testWidgets('debugFillProperties works correctly', (WidgetTester tester) async { final diagnostics = DiagnosticPropertiesBuilder(); const ZetaFAB().debugFillProperties(diagnostics); @@ -181,7 +182,38 @@ void main() { expect(diagnostics.finder('size'), 'small'); expect(diagnostics.finder('shape'), 'full'); expect(diagnostics.finder('icon'), 'IconData(U+0E009)'); - expect(diagnostics.finder('initiallyExpanded'), 'null'); + expect(diagnostics.finder('initiallyExpanded'), 'false'); expect(diagnostics.finder('focusNode'), 'null'); }); + + testWidgets('Expanded changes when label is null', (WidgetTester tester) async { + final scrollController = ScrollController(); + StateSetter? setState; + bool expanded = false; + + await tester.pumpWidget( + TestApp( + home: StatefulBuilder( + builder: (context, setState2) { + setState = setState2; + return ZetaFAB( + scrollController: scrollController, + expanded: expanded, + label: 'Label', + onPressed: () {}, + ); + }, + ), + ), + ); + + final labelFinder = find.text('Label'); + + expect(labelFinder, findsNothing); + + setState?.call(() => expanded = true); + + await tester.pumpAndSettle(); + expect(labelFinder, findsOne); + }); } diff --git a/test/src/components/slider/slider_test.dart b/test/src/components/slider/slider_test.dart new file mode 100644 index 00000000..70522376 --- /dev/null +++ b/test/src/components/slider/slider_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../test_utils/test_app.dart'; + +void main() { + testWidgets('ZetaSlider min/max values', (WidgetTester tester) async { + const double sliderValue = 0.5; + double? changedValue; + + await tester.pumpWidget( + TestApp( + home: ZetaSlider( + value: sliderValue, + onChange: (value) { + changedValue = value; + }, + ), + ), + ); + + final slider = tester.widget(find.byType(Slider)); + expect(slider.min, 0.0); + expect(slider.max, 1.0); + + // Drag the slider to the minimum value + await tester.drag(find.byType(Slider), const Offset(-400, 0)); + expect(changedValue, 0.0); + + // Drag the slider to the maximum value + await tester.drag(find.byType(Slider), const Offset(400, 0)); + expect(changedValue, 1.0); + }); +} diff --git a/test/src/components/stepper input/stepper_input_test.dart b/test/src/components/stepper input/stepper_input_test.dart new file mode 100644 index 00000000..53c4b4c1 --- /dev/null +++ b/test/src/components/stepper input/stepper_input_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/src/components/stepper_input/stepper_input.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../test_utils/test_app.dart'; + +void main() { + testWidgets('ZetaStepperInput increases value when increment button is pressed', (WidgetTester tester) async { + int value = 0; + await tester.pumpWidget( + TestApp( + home: ZetaStepperInput( + value: value, + onChange: (newValue) { + value = newValue; + }, + ), + ), + ); + + expect(value, 0); + + await tester.tap(find.byIcon(ZetaIcons.add_round)); + await tester.pump(); + + expect(value, 1); + }); + + testWidgets('ZetaStepperInput increases value when programatically changed', (WidgetTester tester) async { + int value = 0; + StateSetter? setState; + await tester.pumpWidget( + StatefulBuilder( + builder: (context, setState2) { + setState = setState2; + + return TestApp( + home: ZetaStepperInput( + value: value, + onChange: (newValue) { + value = newValue; + }, + ), + ); + }, + ), + ); + + final ZetaStepperInputState stepperInputState = tester.state(find.byType(ZetaStepperInput)); + + expect(value, stepperInputState.value); + + setState?.call(() { + value = 1; + }); + + await tester.pump(); + + expect(value, stepperInputState.value); + }); +}