From 61c2e56d600e0a4f705a5dbcbfe8759590fbee6c Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 1 Sep 2024 18:46:46 +0800 Subject: [PATCH 01/57] Initial draft --- forui/lib/src/widgets/accordion.dart | 101 +++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 forui/lib/src/widgets/accordion.dart diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart new file mode 100644 index 000000000..6d7fb4332 --- /dev/null +++ b/forui/lib/src/widgets/accordion.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'dart:math' as math; + +import 'package:forui/forui.dart'; +import 'package:forui/src/foundation/tappable.dart'; + +class FAccordion extends StatefulWidget { + final String title; + final bool initiallyExpanded; + final VoidCallback onExpanded; + final double childHeight; + final double removeChildAnimationPercentage; + final Widget child; + + const FAccordion({ + required this.child, + required this.childHeight, + required this.initiallyExpanded, + required this.onExpanded, + this.title = '', + this.removeChildAnimationPercentage = 0, + super.key, + }); + + @override + _FAccordionState createState() => _FAccordionState(); +} + +class _FAccordionState extends State with SingleTickerProviderStateMixin { + late Animation animation; + late AnimationController controller; + bool _isOpened = false; + + @override + void initState() { + super.initState(); + _isOpened = widget.initiallyExpanded; + controller = AnimationController( + duration: const Duration(milliseconds: 500), + value: _isOpened ? 1.0 : 0.0, + vsync: this, + ); + animation = Tween( + begin: 0, + end: 100, + ).animate( + CurvedAnimation( + curve: Curves.ease, + parent: controller, + ), + )..addListener(() { + setState(() {}); + }); + _isOpened ? controller.forward() : controller.reverse(); + } + + @override + Widget build(BuildContext context) => Column( + children: [ + FTappable( + onPress: () { + if (_isOpened) { + controller.reverse(); + } else { + controller.forward(); + } + setState(() => _isOpened = !_isOpened); + widget.onExpanded(); + }, + child: Container( + color: Colors.red, + child: Row( + children: [ + Expanded( + child: Text( + widget.title, + style: TextStyle(), + ), + ), + Transform.rotate( + angle: (animation.value / 100 * -180 + 90) * math.pi / 180.0, + child: FAssets.icons.chevronRight( + height: 20, + colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), + ), + ), + ], + ), + ), + ), + SizedBox( + height: animation.value / 100.0 * widget.childHeight, + child: animation.value >= widget.removeChildAnimationPercentage ? widget.child : Container(), + ), + FDivider( + style: context.theme.dividerStyles.horizontal.copyWith(padding: EdgeInsets.zero, color: Colors.blue), + ), + ], + ); +} From b0850d357b7edac440e31ab0b2853010f96e1d58 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Mon, 2 Sep 2024 15:39:53 +0800 Subject: [PATCH 02/57] Almost done, How to calculate child height effectively? --- forui/example/lib/sandbox.dart | 49 ++++++++++- forui/lib/src/theme/theme_data.dart | 12 +++ forui/lib/src/widgets/accordion.dart | 118 ++++++++++++++++++++++----- 3 files changed, 154 insertions(+), 25 deletions(-) diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index a579264b0..0bc50f26c 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; +import 'package:forui/src/widgets/accordion.dart'; class Sandbox extends StatefulWidget { const Sandbox({super.key}); @@ -22,10 +23,50 @@ class _SandboxState extends State { Widget build(BuildContext context) => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - FTextField.email( - autovalidateMode: AutovalidateMode.always, - description: const Text('Description'), - validator: (value) => value?.length == 5 ? 'Error message' : null, + FAccordion( + title: 'Is it Accessible?', + childHeight: 100, + initiallyExpanded: false, + onExpanded: () {}, + child: const Text('Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left,), + ), + FAccordion( + title: 'Is it Accessible?', + childHeight: 100, + initiallyExpanded: false, + onExpanded: () {}, + child: const Text('Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left,), + ), + FAccordion( + title: 'Is it Accessible?', + childHeight: 100, + initiallyExpanded: false, + onExpanded: () {}, + child: const Text('Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left,), + ), + FAccordion( + title: 'Is it Accessible?', + childHeight: 100, + initiallyExpanded: false, + onExpanded: () {}, + child: const Text('Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left,), + ), + FAccordion( + title: 'Is it Accessible?', + childHeight: 100, + initiallyExpanded: false, + onExpanded: () {}, + child: const Text('Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left,), + ), + SizedBox(height: 20), + FTooltip( + longPressExitDuration: const Duration(seconds: 5000), + tipBuilder: (context, style, _) => const Text('Add to library'), + child: FButton( + style: FButtonStyle.outline, + onPress: () {}, + label: const Text('Hover'), + ), ), const SizedBox(height: 20), const FTextField.password(), diff --git a/forui/lib/src/theme/theme_data.dart b/forui/lib/src/theme/theme_data.dart index 49e4e20a7..02f466191 100644 --- a/forui/lib/src/theme/theme_data.dart +++ b/forui/lib/src/theme/theme_data.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import 'package:forui/src/widgets/accordion.dart'; import 'package:meta/meta.dart'; @@ -25,6 +26,9 @@ final class FThemeData with Diagnosticable { /// The style. It is used to configure the miscellaneous properties, such as border radii, of Forui widgets. final FStyle style; + /// The accordion styles. + final FAccordionStyle accordionStyle; + /// The alert styles. final FAlertStyles alertStyles; @@ -103,6 +107,7 @@ final class FThemeData with Diagnosticable { FThemeData({ required this.colorScheme, required this.style, + required this.accordionStyle, required this.alertStyles, required this.avatarStyle, required this.badgeStyles, @@ -141,6 +146,7 @@ final class FThemeData with Diagnosticable { colorScheme: colorScheme, typography: typography, style: style, + accordionStyle: FAccordionStyle.inherit(colorScheme: colorScheme, style: style, typography: typography), alertStyles: FAlertStyles.inherit(colorScheme: colorScheme, typography: typography, style: style), avatarStyle: FAvatarStyle.inherit(colorScheme: colorScheme, typography: typography), badgeStyles: FBadgeStyles.inherit(colorScheme: colorScheme, typography: typography, style: style), @@ -214,6 +220,7 @@ final class FThemeData with Diagnosticable { ///``` @useResult FThemeData copyWith({ + FAccordionStyle? accordionStyle, FAlertStyles? alertStyles, FAvatarStyle? avatarStyle, FBadgeStyles? badgeStyles, @@ -242,6 +249,7 @@ final class FThemeData with Diagnosticable { colorScheme: colorScheme, typography: typography, style: style, + accordionStyle: accordionStyle ?? this.accordionStyle, alertStyles: alertStyles ?? this.alertStyles, avatarStyle: avatarStyle ?? this.avatarStyle, badgeStyles: badgeStyles ?? this.badgeStyles, @@ -274,6 +282,7 @@ final class FThemeData with Diagnosticable { ..add(DiagnosticsProperty('colorScheme', colorScheme, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('typography', typography, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('style', style, level: DiagnosticLevel.debug)) + ..add(DiagnosticsProperty('accordionStyle', accordionStyle)) ..add(DiagnosticsProperty('alertStyles', alertStyles, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('avatarStyle', avatarStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('badgeStyles', badgeStyles, level: DiagnosticLevel.debug)) @@ -297,6 +306,7 @@ final class FThemeData with Diagnosticable { ..add(DiagnosticsProperty('selectGroupStyle', selectGroupStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('sliderStyles', sliderStyles, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('switchStyle', switchStyle, level: DiagnosticLevel.debug)); + } @override @@ -307,6 +317,7 @@ final class FThemeData with Diagnosticable { colorScheme == other.colorScheme && typography == other.typography && style == other.style && + accordionStyle == other.accordionStyle && alertStyles == other.alertStyles && avatarStyle == other.avatarStyle && badgeStyles == other.badgeStyles && @@ -336,6 +347,7 @@ final class FThemeData with Diagnosticable { colorScheme.hashCode ^ typography.hashCode ^ style.hashCode ^ + accordionStyle.hashCode ^ alertStyles.hashCode ^ avatarStyle.hashCode ^ badgeStyles.hashCode ^ diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 6d7fb4332..51341de7a 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -1,11 +1,16 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'dart:math' as math; import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; +import 'package:forui/src/foundation/util.dart'; +import 'package:meta/meta.dart'; class FAccordion extends StatefulWidget { + /// The divider's style. Defaults to the appropriate style in [FThemeData.dividerStyles]. + final FAccordionStyle? style; final String title; final bool initiallyExpanded; final VoidCallback onExpanded; @@ -19,6 +24,7 @@ class FAccordion extends StatefulWidget { required this.initiallyExpanded, required this.onExpanded, this.title = '', + this.style, this.removeChildAnimationPercentage = 0, super.key, }); @@ -30,15 +36,16 @@ class FAccordion extends StatefulWidget { class _FAccordionState extends State with SingleTickerProviderStateMixin { late Animation animation; late AnimationController controller; - bool _isOpened = false; + bool _isExpanded = false; + bool _hovered = false; @override void initState() { super.initState(); - _isOpened = widget.initiallyExpanded; + _isExpanded = widget.initiallyExpanded; controller = AnimationController( duration: const Duration(milliseconds: 500), - value: _isOpened ? 1.0 : 0.0, + value: _isExpanded ? 1.0 : 0.0, vsync: this, ); animation = Tween( @@ -52,30 +59,44 @@ class _FAccordionState extends State with SingleTickerProviderStateM )..addListener(() { setState(() {}); }); - _isOpened ? controller.forward() : controller.reverse(); + + _isExpanded ? controller.forward() : controller.reverse(); } @override - Widget build(BuildContext context) => Column( - children: [ - FTappable( + Widget build(BuildContext context) { + final style = widget.style ?? context.theme.accordionStyle; + return Column( + children: [ + MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + child: FTappable( onPress: () { - if (_isOpened) { + if (_isExpanded) { controller.reverse(); } else { controller.forward(); } - setState(() => _isOpened = !_isOpened); + setState(() => _isExpanded = !_isExpanded); widget.onExpanded(); }, child: Container( - color: Colors.red, + padding: style.padding, child: Row( children: [ Expanded( - child: Text( - widget.title, - style: TextStyle(), + child: merge( + // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + style: TextStyle(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), + child: Text( + widget.title, + style: style.title, + ), ), ), Transform.rotate( @@ -89,13 +110,68 @@ class _FAccordionState extends State with SingleTickerProviderStateM ), ), ), - SizedBox( - height: animation.value / 100.0 * widget.childHeight, - child: animation.value >= widget.removeChildAnimationPercentage ? widget.child : Container(), - ), - FDivider( - style: context.theme.dividerStyles.horizontal.copyWith(padding: EdgeInsets.zero, color: Colors.blue), - ), - ], + ), + SizedBox( + height: animation.value / 100.0 * widget.childHeight, + child: animation.value >= widget.removeChildAnimationPercentage ? widget.child : Container(), + ), + FDivider( + style: context.theme.dividerStyles.horizontal + .copyWith(padding: EdgeInsets.zero, color: context.theme.colorScheme.border), + ), + ], + ); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } +} + +/// The [FAccordion] styles. +final class FAccordionStyle with Diagnosticable { + /// The horizontal divider's style. + final TextStyle title; + + /// The vertical divider's style. + final EdgeInsets padding; + + /// Creates a [FAccordionStyle]. + FAccordionStyle({required this.title, required this.padding}); + + /// Creates a [FDividerStyles] that inherits its properties from [colorScheme] and [style]. + FAccordionStyle.inherit({required FColorScheme colorScheme, required FStyle style, required FTypography typography}) + : title = typography.base.copyWith( + fontWeight: FontWeight.w500, + ), + padding = const EdgeInsets.symmetric(vertical: 15); + + /// Returns a copy of this [FAccordionStyle] with the given properties replaced. + @useResult + FAccordionStyle copyWith({ + TextStyle? title, + EdgeInsets? padding, + }) => + FAccordionStyle( + title: title ?? this.title, + padding: padding ?? this.padding, ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('title', title)) + ..add(DiagnosticsProperty('padding', padding)); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FAccordionStyle && runtimeType == other.runtimeType && title == other.title && padding == other.padding; + + @override + int get hashCode => title.hashCode ^ padding.hashCode; } From 181aaea497c7676cfbfa375abb9879065bcc2252 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Wed, 4 Sep 2024 16:58:56 +0800 Subject: [PATCH 03/57] Added custom RenderObject --- forui/lib/src/widgets/accordion.dart | 101 +++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 51341de7a..48571afa1 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'dart:math' as math; @@ -82,7 +83,7 @@ class _FAccordionState extends State with SingleTickerProviderStateM widget.onExpanded(); }, child: Container( - padding: style.padding, + padding: style.titlePadding, child: Row( children: [ Expanded( @@ -111,9 +112,9 @@ class _FAccordionState extends State with SingleTickerProviderStateM ), ), ), - SizedBox( - height: animation.value / 100.0 * widget.childHeight, - child: animation.value >= widget.removeChildAnimationPercentage ? widget.child : Container(), + _Size( + percentage: animation.value / 100.0, + child: widget.child, ), FDivider( style: context.theme.dividerStyles.horizontal @@ -132,31 +133,37 @@ class _FAccordionState extends State with SingleTickerProviderStateM /// The [FAccordion] styles. final class FAccordionStyle with Diagnosticable { - /// The horizontal divider's style. + /// The title's text style. final TextStyle title; - /// The vertical divider's style. - final EdgeInsets padding; + /// The padding of the title. + final EdgeInsets titlePadding; + + /// The padding of the content. + final EdgeInsets contentPadding; /// Creates a [FAccordionStyle]. - FAccordionStyle({required this.title, required this.padding}); + FAccordionStyle({required this.title, required this.titlePadding, required this.contentPadding}); /// Creates a [FDividerStyles] that inherits its properties from [colorScheme] and [style]. FAccordionStyle.inherit({required FColorScheme colorScheme, required FStyle style, required FTypography typography}) : title = typography.base.copyWith( fontWeight: FontWeight.w500, ), - padding = const EdgeInsets.symmetric(vertical: 15); + titlePadding = const EdgeInsets.symmetric(vertical: 15), + contentPadding = const EdgeInsets.only(bottom: 15); /// Returns a copy of this [FAccordionStyle] with the given properties replaced. @useResult FAccordionStyle copyWith({ TextStyle? title, - EdgeInsets? padding, + EdgeInsets? titlePadding, + EdgeInsets? contentPadding, }) => FAccordionStyle( title: title ?? this.title, - padding: padding ?? this.padding, + titlePadding: titlePadding ?? this.titlePadding, + contentPadding: contentPadding ?? this.contentPadding, ); @override @@ -164,14 +171,80 @@ final class FAccordionStyle with Diagnosticable { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('title', title)) - ..add(DiagnosticsProperty('padding', padding)); + ..add(DiagnosticsProperty('padding', titlePadding)) + ..add(DiagnosticsProperty('contentPadding', contentPadding)); } @override bool operator ==(Object other) => identical(this, other) || - other is FAccordionStyle && runtimeType == other.runtimeType && title == other.title && padding == other.padding; + other is FAccordionStyle && + runtimeType == other.runtimeType && + title == other.title && + titlePadding == other.titlePadding && + contentPadding == other.contentPadding; + + @override + int get hashCode => title.hashCode ^ titlePadding.hashCode ^ contentPadding.hashCode; +} + +class _Size extends SingleChildRenderObjectWidget { + final double _percentage; + + const _Size({ + required Widget child, + required double percentage, + }) : _percentage = percentage, + super(child: child); + + @override + RenderObject createRenderObject(BuildContext context) => _RenderBox(percentage: _percentage); + + @override + void updateRenderObject(BuildContext context, _RenderBox renderObject) { + renderObject.percentage = _percentage; + } +} + +class _RenderBox extends RenderBox with RenderObjectWithChildMixin { + double _percentage; + + _RenderBox({ + required double percentage, + }) : _percentage = percentage; @override - int get hashCode => title.hashCode ^ padding.hashCode; + void performLayout() { + if (child case final child?) { + child.layout(constraints.normalize(), parentUsesSize: true); + size = Size(child.size.width, child.size.height * _percentage); + child.layout(BoxConstraints.tight(size)); + } else { + size = constraints.smallest; + } + } + + @override + void paint(PaintingContext context, Offset offset) { + if (child case final child?) { + context.paintChild(child, offset); + } + } + + double get percentage => _percentage; + + set percentage(double value) { + if (_percentage == value) { + return; + } + + _percentage = value; + markNeedsLayout(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('percentage', percentage)); + } } From 77f3a582fb9665470875ae256f6e548cfad009ea Mon Sep 17 00:00:00 2001 From: Matthias Ngeo Date: Wed, 4 Sep 2024 18:20:21 +0800 Subject: [PATCH 04/57] Fix accordion --- forui/lib/src/widgets/accordion.dart | 42 +++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 48571afa1..26751ef37 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -112,9 +112,18 @@ class _FAccordionState extends State with SingleTickerProviderStateM ), ), ), - _Size( + // We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by + // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the + // child to layout at a smaller size while the amount of padding remains the same. + _Expandable( percentage: animation.value / 100.0, - child: widget.child, + child: ClipRect( + clipper: _Clipper(percentage: animation.value / 100.0), + child: Padding( + padding: style.contentPadding, + child: widget.child, + ), + ), ), FDivider( style: context.theme.dividerStyles.horizontal @@ -188,28 +197,29 @@ final class FAccordionStyle with Diagnosticable { int get hashCode => title.hashCode ^ titlePadding.hashCode ^ contentPadding.hashCode; } -class _Size extends SingleChildRenderObjectWidget { + +class _Expandable extends SingleChildRenderObjectWidget { final double _percentage; - const _Size({ + const _Expandable({ required Widget child, required double percentage, }) : _percentage = percentage, super(child: child); @override - RenderObject createRenderObject(BuildContext context) => _RenderBox(percentage: _percentage); + RenderObject createRenderObject(BuildContext context) => _ExpandableBox(percentage: _percentage); @override - void updateRenderObject(BuildContext context, _RenderBox renderObject) { + void updateRenderObject(BuildContext context, _ExpandableBox renderObject) { renderObject.percentage = _percentage; } } -class _RenderBox extends RenderBox with RenderObjectWithChildMixin { +class _ExpandableBox extends RenderBox with RenderObjectWithChildMixin { double _percentage; - _RenderBox({ + _ExpandableBox({ required double percentage, }) : _percentage = percentage; @@ -218,7 +228,7 @@ class _RenderBox extends RenderBox with RenderObjectWithChildMixin { if (child case final child?) { child.layout(constraints.normalize(), parentUsesSize: true); size = Size(child.size.width, child.size.height * _percentage); - child.layout(BoxConstraints.tight(size)); + } else { size = constraints.smallest; } @@ -241,10 +251,16 @@ class _RenderBox extends RenderBox with RenderObjectWithChildMixin { _percentage = value; markNeedsLayout(); } +} + +class _Clipper extends CustomClipper { + final double percentage; + + _Clipper({required this.percentage}); @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DoubleProperty('percentage', percentage)); - } + Rect getClip(Size size) => Offset.zero & Size(size.width, size.height * percentage); + + @override + bool shouldReclip(covariant _Clipper oldClipper) => oldClipper.percentage != percentage; } From 721bcd7746702a27a79d8ba028c9ea67777fc829 Mon Sep 17 00:00:00 2001 From: Matthias Ngeo Date: Wed, 4 Sep 2024 18:47:00 +0800 Subject: [PATCH 05/57] Fix hit testing --- forui/lib/src/widgets/accordion.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 26751ef37..ac345627f 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -241,6 +241,16 @@ class _ExpandableBox extends RenderBox with RenderObjectWithChildMixin _percentage; set percentage(double value) { From 3a3e3129bc28008ca6924b2a95ab5471ac6d2952 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Wed, 11 Sep 2024 21:51:24 +0800 Subject: [PATCH 06/57] Ready for checking, controller implemented --- forui/example/lib/sandbox.dart | 26 +++--- forui/lib/forui.dart | 1 + forui/lib/src/theme/theme_data.dart | 4 +- forui/lib/src/widgets/accordion.dart | 134 +++++++++++++++++++-------- forui/lib/widgets/accordion.dart | 8 ++ 5 files changed, 121 insertions(+), 52 deletions(-) create mode 100644 forui/lib/widgets/accordion.dart diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index 0bc50f26c..e853c63f7 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; -import 'package:forui/src/widgets/accordion.dart'; class Sandbox extends StatefulWidget { const Sandbox({super.key}); @@ -25,17 +24,22 @@ class _SandboxState extends State { children: [ FAccordion( title: 'Is it Accessible?', - childHeight: 100, - initiallyExpanded: false, - onExpanded: () {}, - child: const Text('Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left,), + controller: FAccordionController(initiallyExpanded: false), + child: const Text( + 'Yes. It adheres to the WAI-ARIA design pattern', + textAlign: TextAlign.left, + ), ), - FAccordion( - title: 'Is it Accessible?', - childHeight: 100, - initiallyExpanded: false, - onExpanded: () {}, - child: const Text('Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left,), + const SizedBox(height: 20), + FSelectGroup( + label: const Text('Select Group'), + description: const Text('Select Group Description'), + controller: FMultiSelectGroupController(min: 1, max: 2, values: {1}), + items: [ + FSelectGroupItem.checkbox(value: 1, label: const Text('Checkbox 1'), semanticLabel: 'Checkbox 1'), + FSelectGroupItem.checkbox(value: 2, label: const Text('Checkbox 2'), semanticLabel: 'Checkbox 2'), + FSelectGroupItem.checkbox(value: 3, label: const Text('Checkbox 3'), semanticLabel: 'Checkbox 3'), + ], ), FAccordion( title: 'Is it Accessible?', diff --git a/forui/lib/forui.dart b/forui/lib/forui.dart index bc0e4f1fa..248cc0135 100644 --- a/forui/lib/forui.dart +++ b/forui/lib/forui.dart @@ -5,6 +5,7 @@ export 'assets.dart'; export 'foundation.dart'; export 'theme.dart'; +export 'widgets/accordion.dart'; export 'widgets/alert.dart'; export 'widgets/avatar.dart'; export 'widgets/badge.dart'; diff --git a/forui/lib/src/theme/theme_data.dart b/forui/lib/src/theme/theme_data.dart index 02f466191..76f075aa0 100644 --- a/forui/lib/src/theme/theme_data.dart +++ b/forui/lib/src/theme/theme_data.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; -import 'package:forui/src/widgets/accordion.dart'; import 'package:meta/meta.dart'; @@ -146,7 +145,7 @@ final class FThemeData with Diagnosticable { colorScheme: colorScheme, typography: typography, style: style, - accordionStyle: FAccordionStyle.inherit(colorScheme: colorScheme, style: style, typography: typography), + accordionStyle: FAccordionStyle.inherit(colorScheme: colorScheme, typography: typography), alertStyles: FAlertStyles.inherit(colorScheme: colorScheme, typography: typography, style: style), avatarStyle: FAvatarStyle.inherit(colorScheme: colorScheme, typography: typography), badgeStyles: FBadgeStyles.inherit(colorScheme: colorScheme, typography: typography, style: style), @@ -306,7 +305,6 @@ final class FThemeData with Diagnosticable { ..add(DiagnosticsProperty('selectGroupStyle', selectGroupStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('sliderStyles', sliderStyles, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('switchStyle', switchStyle, level: DiagnosticLevel.debug)); - } @override diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index ac345627f..58a59ae5c 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'dart:math' as math; import 'package:forui/forui.dart'; @@ -9,44 +10,78 @@ import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; import 'package:meta/meta.dart'; +/// A controller that stores the expanded state of an [FAccordion]. +class FAccordionController extends ValueNotifier { + bool _expanded; + + /// Creates a [FAccordionController]. + FAccordionController({ + bool? initiallyExpanded, + }) : _expanded = initiallyExpanded ?? true, + super(initiallyExpanded ?? true); + + /// whether the accordion is expanded. + bool get expanded => _expanded; + + /// Toggles the expansion state of the accordion. + bool toggle() { + _expanded = !_expanded; + notifyListeners(); + return _expanded; + } +} + +/// A vertically stacked set of interactive headings that each reveal a section of content. +/// +/// See: +/// * https://forui.dev/docs/accordion for working examples. class FAccordion extends StatefulWidget { - /// The divider's style. Defaults to the appropriate style in [FThemeData.dividerStyles]. + /// The accordion's style. Defaults to the appropriate style in [FThemeData.accordionStyle]. final FAccordionStyle? style; + + /// The title. final String title; - final bool initiallyExpanded; - final VoidCallback onExpanded; - final double childHeight; - final double removeChildAnimationPercentage; + + /// The accordion's controller. + final FAccordionController controller; + + /// The child. final Widget child; + /// Creates an [FAccordion]. const FAccordion({ required this.child, - required this.childHeight, - required this.initiallyExpanded, - required this.onExpanded, + required this.controller, this.title = '', this.style, - this.removeChildAnimationPercentage = 0, super.key, }); @override + //ignore:library_private_types_in_public_api _FAccordionState createState() => _FAccordionState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(StringProperty('title', title)) + ..add(DiagnosticsProperty('controller', controller)); + } } class _FAccordionState extends State with SingleTickerProviderStateMixin { late Animation animation; late AnimationController controller; - bool _isExpanded = false; bool _hovered = false; @override void initState() { super.initState(); - _isExpanded = widget.initiallyExpanded; controller = AnimationController( duration: const Duration(milliseconds: 500), - value: _isExpanded ? 1.0 : 0.0, + value: widget.controller.expanded ? 1.0 : 0.0, vsync: this, ); animation = Tween( @@ -60,8 +95,6 @@ class _FAccordionState extends State with SingleTickerProviderStateM )..addListener(() { setState(() {}); }); - - _isExpanded ? controller.forward() : controller.reverse(); } @override @@ -73,15 +106,7 @@ class _FAccordionState extends State with SingleTickerProviderStateM onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: FTappable( - onPress: () { - if (_isExpanded) { - controller.reverse(); - } else { - controller.forward(); - } - setState(() => _isExpanded = !_isExpanded); - widget.onExpanded(); - }, + onPress: () => widget.controller.toggle() ? controller.forward() : controller.reverse(), child: Container( padding: style.titlePadding, child: Row( @@ -102,10 +127,7 @@ class _FAccordionState extends State with SingleTickerProviderStateM ), Transform.rotate( angle: (animation.value / 100 * -180 + 90) * math.pi / 180.0, - child: FAssets.icons.chevronRight( - height: 20, - colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), - ), + child: style.icon, ), ], ), @@ -126,8 +148,7 @@ class _FAccordionState extends State with SingleTickerProviderStateM ), ), FDivider( - style: context.theme.dividerStyles.horizontal - .copyWith(padding: EdgeInsets.zero, color: context.theme.colorScheme.border), + style: context.theme.dividerStyles.horizontal.copyWith(padding: EdgeInsets.zero, color: style.dividerColor), ), ], ); @@ -138,6 +159,14 @@ class _FAccordionState extends State with SingleTickerProviderStateM controller.dispose(); super.dispose(); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('animation', animation)) + ..add(DiagnosticsProperty('controller', controller)); + } } /// The [FAccordion] styles. @@ -151,16 +180,33 @@ final class FAccordionStyle with Diagnosticable { /// The padding of the content. final EdgeInsets contentPadding; + /// The icon. + final SvgPicture icon; + + /// The divider's color. + final Color dividerColor; + /// Creates a [FAccordionStyle]. - FAccordionStyle({required this.title, required this.titlePadding, required this.contentPadding}); + FAccordionStyle({ + required this.title, + required this.titlePadding, + required this.contentPadding, + required this.icon, + required this.dividerColor, + }); - /// Creates a [FDividerStyles] that inherits its properties from [colorScheme] and [style]. - FAccordionStyle.inherit({required FColorScheme colorScheme, required FStyle style, required FTypography typography}) + /// Creates a [FDividerStyles] that inherits its properties from [colorScheme]. + FAccordionStyle.inherit({required FColorScheme colorScheme, required FTypography typography}) : title = typography.base.copyWith( fontWeight: FontWeight.w500, ), titlePadding = const EdgeInsets.symmetric(vertical: 15), - contentPadding = const EdgeInsets.only(bottom: 15); + contentPadding = const EdgeInsets.only(bottom: 15), + icon = FAssets.icons.chevronRight( + height: 20, + colorFilter: ColorFilter.mode(colorScheme.primary, BlendMode.srcIn), + ), + dividerColor = colorScheme.border; /// Returns a copy of this [FAccordionStyle] with the given properties replaced. @useResult @@ -168,11 +214,15 @@ final class FAccordionStyle with Diagnosticable { TextStyle? title, EdgeInsets? titlePadding, EdgeInsets? contentPadding, + SvgPicture? icon, + Color? dividerColor, }) => FAccordionStyle( title: title ?? this.title, titlePadding: titlePadding ?? this.titlePadding, contentPadding: contentPadding ?? this.contentPadding, + icon: icon ?? this.icon, + dividerColor: dividerColor ?? this.dividerColor, ); @override @@ -181,7 +231,8 @@ final class FAccordionStyle with Diagnosticable { properties ..add(DiagnosticsProperty('title', title)) ..add(DiagnosticsProperty('padding', titlePadding)) - ..add(DiagnosticsProperty('contentPadding', contentPadding)); + ..add(DiagnosticsProperty('contentPadding', contentPadding)) + ..add(ColorProperty('dividerColor', dividerColor)); } @override @@ -191,13 +242,15 @@ final class FAccordionStyle with Diagnosticable { runtimeType == other.runtimeType && title == other.title && titlePadding == other.titlePadding && - contentPadding == other.contentPadding; + contentPadding == other.contentPadding && + icon == other.icon && + dividerColor == other.dividerColor; @override - int get hashCode => title.hashCode ^ titlePadding.hashCode ^ contentPadding.hashCode; + int get hashCode => + title.hashCode ^ titlePadding.hashCode ^ contentPadding.hashCode ^ icon.hashCode ^ dividerColor.hashCode; } - class _Expandable extends SingleChildRenderObjectWidget { final double _percentage; @@ -228,7 +281,6 @@ class _ExpandableBox extends RenderBox with RenderObjectWithChildMixin { diff --git a/forui/lib/widgets/accordion.dart b/forui/lib/widgets/accordion.dart new file mode 100644 index 000000000..a2867fd57 --- /dev/null +++ b/forui/lib/widgets/accordion.dart @@ -0,0 +1,8 @@ +/// {@category Widgets} +/// +/// An image element with a fallback for representing the user. +/// +/// See https://forui.dev/docs/avatar for working examples. +library forui.widgets.accordion; + +export '../src/widgets/accordion.dart'; From 022811506c5a88fd1ca4fd50a49e0a4fd88d19d5 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Thu, 12 Sep 2024 22:20:09 +0800 Subject: [PATCH 07/57] reconfigured FAccordionController --- forui/example/lib/sandbox.dart | 5 +- forui/lib/src/widgets/accordion.dart | 197 ++++++++++++++++----------- 2 files changed, 120 insertions(+), 82 deletions(-) diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index e853c63f7..eecbe2129 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -22,10 +22,9 @@ class _SandboxState extends State { Widget build(BuildContext context) => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - FAccordion( + const FAccordion( title: 'Is it Accessible?', - controller: FAccordionController(initiallyExpanded: false), - child: const Text( + child: Text( 'Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left, ), diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 58a59ae5c..5e32b6a10 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -11,23 +11,63 @@ import 'package:forui/src/foundation/util.dart'; import 'package:meta/meta.dart'; /// A controller that stores the expanded state of an [FAccordion]. -class FAccordionController extends ValueNotifier { - bool _expanded; +class FAccordionController extends ChangeNotifier { + late final AnimationController _animation; + late final Animation _expand; /// Creates a [FAccordionController]. FAccordionController({ + required TickerProvider vsync, + duration = const Duration(milliseconds: 500), bool? initiallyExpanded, - }) : _expanded = initiallyExpanded ?? true, - super(initiallyExpanded ?? true); + }) { + _animation = AnimationController( + duration: duration, + value: initiallyExpanded ?? true ? 1.0 : 0.0, + vsync: vsync, + ); + _expand = Tween( + begin: 0, + end: 100, + ).animate( + CurvedAnimation( + curve: Curves.ease, + parent: _animation, + ), + )..addListener(notifyListeners); + } + + /// Convenience method for toggling the current [expanded] status. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future toggle() async => expanded ? close() : expand(); - /// whether the accordion is expanded. - bool get expanded => _expanded; + /// Expands the accordion. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future expand() async { + await _animation.forward(); + notifyListeners(); + } - /// Toggles the expansion state of the accordion. - bool toggle() { - _expanded = !_expanded; + /// closes the accordion. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future close() async { + await _animation.reverse(); notifyListeners(); - return _expanded; + } + + /// True if the accordion is expanded. False if it is closed. + bool get expanded => _animation.value == 1.0; + + /// The animation value. + double get value => _expand.value; + + @override + void dispose() { + _animation.dispose(); + super.dispose(); } } @@ -43,7 +83,7 @@ class FAccordion extends StatefulWidget { final String title; /// The accordion's controller. - final FAccordionController controller; + final FAccordionController? controller; /// The child. final Widget child; @@ -51,9 +91,9 @@ class FAccordion extends StatefulWidget { /// Creates an [FAccordion]. const FAccordion({ required this.child, - required this.controller, - this.title = '', this.style, + this.controller, + this.title = '', super.key, }); @@ -72,100 +112,99 @@ class FAccordion extends StatefulWidget { } class _FAccordionState extends State with SingleTickerProviderStateMixin { - late Animation animation; - late AnimationController controller; + late FAccordionController _controller; bool _hovered = false; @override void initState() { super.initState(); - controller = AnimationController( - duration: const Duration(milliseconds: 500), - value: widget.controller.expanded ? 1.0 : 0.0, - vsync: this, - ); - animation = Tween( - begin: 0, - end: 100, - ).animate( - CurvedAnimation( - curve: Curves.ease, - parent: controller, - ), - )..addListener(() { - setState(() {}); - }); + _controller = widget.controller ?? FAccordionController(vsync: this); + } + + @override + void didUpdateWidget(covariant FAccordion old) { + super.didUpdateWidget(old); + if (widget.controller != old.controller) { + if (old.controller == null) { + _controller.dispose(); + } + + _controller = widget.controller ?? FAccordionController(vsync: this); + } } @override Widget build(BuildContext context) { final style = widget.style ?? context.theme.accordionStyle; - return Column( - children: [ - MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: FTappable( - onPress: () => widget.controller.toggle() ? controller.forward() : controller.reverse(), - child: Container( - padding: style.titlePadding, - child: Row( - children: [ - Expanded( - child: merge( - // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. - textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, - ), - style: TextStyle(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), - child: Text( - widget.title, - style: style.title, + return AnimatedBuilder( + animation: _controller, + builder: (context, _) => Column( + children: [ + MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + child: FTappable( + onPress: () => _controller.toggle(), + child: Container( + padding: style.titlePadding, + child: Row( + children: [ + Expanded( + child: merge( + // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + style: TextStyle(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), + child: Text( + widget.title, + style: style.title, + ), ), ), - ), - Transform.rotate( - angle: (animation.value / 100 * -180 + 90) * math.pi / 180.0, - child: style.icon, - ), - ], + Transform.rotate( + angle: (_controller.value / 100 * -180 + 90) * math.pi / 180.0, + child: style.icon, + ), + ], + ), ), ), ), - ), - // We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by - // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the - // child to layout at a smaller size while the amount of padding remains the same. - _Expandable( - percentage: animation.value / 100.0, - child: ClipRect( - clipper: _Clipper(percentage: animation.value / 100.0), - child: Padding( - padding: style.contentPadding, - child: widget.child, + // We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by + // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the + // child to layout at a smaller size while the amount of padding remains the same. + _Expandable( + percentage: _controller.value / 100.0, + child: ClipRect( + clipper: _Clipper(percentage: _controller.value / 100.0), + child: Padding( + padding: style.contentPadding, + child: widget.child, + ), ), ), - ), - FDivider( - style: context.theme.dividerStyles.horizontal.copyWith(padding: EdgeInsets.zero, color: style.dividerColor), - ), - ], + FDivider( + style: context.theme.dividerStyles.horizontal.copyWith(padding: EdgeInsets.zero, color: style.dividerColor), + ), + ], + ), ); } @override void dispose() { - controller.dispose(); + if (widget.controller == null) { + _controller.dispose(); + } super.dispose(); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('animation', animation)) - ..add(DiagnosticsProperty('controller', controller)); + properties.add(DiagnosticsProperty('controller', _controller)); } } From 40d3d9e0b263cca4bf8ed62a31b8b14873ff2a92 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 15 Sep 2024 15:16:28 +0800 Subject: [PATCH 08/57] Ready for review, Docs and Tests implemented --- docs/pages/docs/accordion.mdx | 55 ++++++++++++++ forui/lib/src/widgets/accordion.dart | 56 +++++++++++---- forui/test/golden/accordion/closed.png | Bin 0 -> 4023 bytes forui/test/golden/accordion/expanded.png | Bin 0 -> 4096 bytes .../accordion/accordion_golden_test.dart | 68 ++++++++++++++++++ .../src/widgets/accordion/accordion_test.dart | 42 +++++++++++ samples/lib/main.dart | 1 + samples/lib/widgets/accordion.dart | 37 ++++++++++ 8 files changed, 244 insertions(+), 15 deletions(-) create mode 100644 docs/pages/docs/accordion.mdx create mode 100644 forui/test/golden/accordion/closed.png create mode 100644 forui/test/golden/accordion/expanded.png create mode 100644 forui/test/src/widgets/accordion/accordion_golden_test.dart create mode 100644 forui/test/src/widgets/accordion/accordion_test.dart create mode 100644 samples/lib/widgets/accordion.dart diff --git a/docs/pages/docs/accordion.mdx b/docs/pages/docs/accordion.mdx new file mode 100644 index 000000000..cc9559a5e --- /dev/null +++ b/docs/pages/docs/accordion.mdx @@ -0,0 +1,55 @@ +import { Tabs } from 'nextra/components'; +import { Widget } from "../../components/widget"; +import LinkBadge from "../../components/link-badge/link-badge"; +import LinkBadgeGroup from "../../components/link-badge/link-badge-group"; + +# Accordion +A vertically stacked set of interactive headings that each reveal a section of content. + + + + + + + + + + + ```dart + const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FAccordion( + title: 'Is it Styled?', + child: Text( + "Yes. It comes with default styles that matches the other components' aesthetics", + textAlign: TextAlign.left, + ), + ), + FAccordion( + title: 'Is it Animated?', + initiallyExpanded: false, + child: Text( + 'Yes. It is animated by default, but you can disable it if you prefer', + textAlign: TextAlign.left, + ), + ), + ], + ); + ``` + + + +## Usage + +### `FAccordion(...)` + +```dart +FAccordion( + title: 'Is it Styled?', + child: Text( + "Yes. It comes with default styles that matches the other components' aesthetics", + textAlign: TextAlign.left, + ), +); +``` diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 5e32b6a10..c35fb64cf 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -19,11 +19,11 @@ class FAccordionController extends ChangeNotifier { FAccordionController({ required TickerProvider vsync, duration = const Duration(milliseconds: 500), - bool? initiallyExpanded, + bool initiallyExpanded = true, }) { _animation = AnimationController( duration: duration, - value: initiallyExpanded ?? true ? 1.0 : 0.0, + value: initiallyExpanded ? 1.0 : 0.0, vsync: vsync, ); _expand = Tween( @@ -85,6 +85,9 @@ class FAccordion extends StatefulWidget { /// The accordion's controller. final FAccordionController? controller; + /// Whether the accordion is initially expanded. Defaults to true. + final bool initiallyExpanded; + /// The child. final Widget child; @@ -92,8 +95,9 @@ class FAccordion extends StatefulWidget { const FAccordion({ required this.child, this.style, - this.controller, this.title = '', + this.controller, + this.initiallyExpanded = true, super.key, }); @@ -107,7 +111,8 @@ class FAccordion extends StatefulWidget { properties ..add(DiagnosticsProperty('style', style)) ..add(StringProperty('title', title)) - ..add(DiagnosticsProperty('controller', controller)); + ..add(DiagnosticsProperty('controller', controller)) + ..add(DiagnosticsProperty('initiallyExpanded', initiallyExpanded)); } } @@ -118,7 +123,11 @@ class _FAccordionState extends State with SingleTickerProviderStateM @override void initState() { super.initState(); - _controller = widget.controller ?? FAccordionController(vsync: this); + _controller = widget.controller ?? + FAccordionController( + vsync: this, + initiallyExpanded: widget.initiallyExpanded, + ); } @override @@ -159,7 +168,7 @@ class _FAccordionState extends State with SingleTickerProviderStateM style: TextStyle(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), child: Text( widget.title, - style: style.title, + style: style.titleTextStyle, ), ), ), @@ -181,7 +190,7 @@ class _FAccordionState extends State with SingleTickerProviderStateM clipper: _Clipper(percentage: _controller.value / 100.0), child: Padding( padding: style.contentPadding, - child: widget.child, + child: DefaultTextStyle(style: style.childTextStyle, child: widget.child), ), ), ), @@ -211,7 +220,10 @@ class _FAccordionState extends State with SingleTickerProviderStateM /// The [FAccordion] styles. final class FAccordionStyle with Diagnosticable { /// The title's text style. - final TextStyle title; + final TextStyle titleTextStyle; + + /// The child's default text style. + final TextStyle childTextStyle; /// The padding of the title. final EdgeInsets titlePadding; @@ -227,7 +239,8 @@ final class FAccordionStyle with Diagnosticable { /// Creates a [FAccordionStyle]. FAccordionStyle({ - required this.title, + required this.titleTextStyle, + required this.childTextStyle, required this.titlePadding, required this.contentPadding, required this.icon, @@ -236,8 +249,12 @@ final class FAccordionStyle with Diagnosticable { /// Creates a [FDividerStyles] that inherits its properties from [colorScheme]. FAccordionStyle.inherit({required FColorScheme colorScheme, required FTypography typography}) - : title = typography.base.copyWith( + : titleTextStyle = typography.base.copyWith( fontWeight: FontWeight.w500, + color: colorScheme.foreground, + ), + childTextStyle = typography.sm.copyWith( + color: colorScheme.foreground, ), titlePadding = const EdgeInsets.symmetric(vertical: 15), contentPadding = const EdgeInsets.only(bottom: 15), @@ -250,14 +267,16 @@ final class FAccordionStyle with Diagnosticable { /// Returns a copy of this [FAccordionStyle] with the given properties replaced. @useResult FAccordionStyle copyWith({ - TextStyle? title, + TextStyle? titleTextStyle, + TextStyle? childTextStyle, EdgeInsets? titlePadding, EdgeInsets? contentPadding, SvgPicture? icon, Color? dividerColor, }) => FAccordionStyle( - title: title ?? this.title, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + childTextStyle: childTextStyle ?? this.childTextStyle, titlePadding: titlePadding ?? this.titlePadding, contentPadding: contentPadding ?? this.contentPadding, icon: icon ?? this.icon, @@ -268,7 +287,8 @@ final class FAccordionStyle with Diagnosticable { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('title', title)) + ..add(DiagnosticsProperty('title', titleTextStyle)) + ..add(DiagnosticsProperty('childTextStyle', childTextStyle)) ..add(DiagnosticsProperty('padding', titlePadding)) ..add(DiagnosticsProperty('contentPadding', contentPadding)) ..add(ColorProperty('dividerColor', dividerColor)); @@ -279,7 +299,8 @@ final class FAccordionStyle with Diagnosticable { identical(this, other) || other is FAccordionStyle && runtimeType == other.runtimeType && - title == other.title && + titleTextStyle == other.titleTextStyle && + childTextStyle == other.childTextStyle && titlePadding == other.titlePadding && contentPadding == other.contentPadding && icon == other.icon && @@ -287,7 +308,12 @@ final class FAccordionStyle with Diagnosticable { @override int get hashCode => - title.hashCode ^ titlePadding.hashCode ^ contentPadding.hashCode ^ icon.hashCode ^ dividerColor.hashCode; + titleTextStyle.hashCode ^ + childTextStyle.hashCode ^ + titlePadding.hashCode ^ + contentPadding.hashCode ^ + icon.hashCode ^ + dividerColor.hashCode; } class _Expandable extends SingleChildRenderObjectWidget { diff --git a/forui/test/golden/accordion/closed.png b/forui/test/golden/accordion/closed.png new file mode 100644 index 0000000000000000000000000000000000000000..dca728e42f39a4ff07ba0d23352ed843c362e5fd GIT binary patch literal 4023 zcmeHK?N5_e6u%%sLnpYgtO&EmO`;Df?MVg zL+6r$j6&lGF^gRhyJE3VX=7v{;L{146==bTp%kQr0)1Ngw%dPUpO&TP!@aq`B3obBP6~Aa-tMI*_5!X1EApr)TBi zpzv{Le*hrzbXLZZJg#`fsB6}9Vzg`d=JJSHJdx%SVbdGbpOVgX4+38_0_o0 zRu8K?wCsZdn*Pla(Jwt643$FK$l0Ze*dSy~#zo36mwx}rFOI zqLEllU7JvX0Kge|sw8NJAvFXCz>lxS0+9A`0|a&j1p{z400sgQNQS^e2>cBH?_aTB zCX=B}QP#rR3A7W_k#uB|3GM6G5Cp%=;Hk#;ky34+@zSu29?JF}$y`2X+W${H@=Udz64AC>Rb@NT%>e$QO04Vnv zRp<0Fr+ua}(U*8;-c>@Z>f>P+r(^!2&SF$05tP;DF=FT|0IV*rF5Mb7Ek16mNVYCM zCYT&+n!<$o&#(q5w0L6T#ur6Jo$c*CJsd-P#)Q=s+)W zZy*8y7DLH*+Qek1Foo|J5u*N4Os*>%bXjbl>nq+#OB^gv^!L|H5(Tam4LT?gKI$)@ zp_@>b>G_b3VM6D|M@L6b^lpd6GG@3%o!s2)GHsYK#y4i?E!lhTrc!QFH55PC)7DbV z5iI}4ci3zlely~W5$;fGET!7(-p_t9g3mV~UdOyp7eyr}RH`@a&?xN7Daa6Z78KW) z(CMf+g*W8O&erMLNeo6+m2cr`q%pU_op*}AHZ(qNsGSq!i@D^DKdlQR*9iU^Z8%;3 zRvfXnm#~-gX#;=r?|c$z{n@t~^}?n|B$`s3%kO&wfM<+5i2Dzw@g65fQh$+7@4B$` zu+FnNuNP2XizOa*z0~NVG3ictKP0txYi;UI=@@hBycKcV`iLks_{FtMJJXwI#i$44 z3zhHV@p$>|+b5aZ0B~$a?9&G*b(RyJ@Fxo9On*7u>r1D8DYYZVSrFEL6^_Wd#S= pQ-DqesC1x>2((cDt)9{wc03zjdpX&96!tYB>u64fEd8rX{{ZfdfkTQ^xNCp|(urfhup$w3R8O1peHlCq8<_eUqLJ2JoH3rRE zU=qqpFoqy1Qt6PVr66J-Q6Ppuc@+l57RpwjyjlwFF8&E@KP*|l$<6tGKl$Es&;6Wp z&pG$j1wyd3<)@Ya0PE0@z>5HwIRRiAjd&j-H96O^pfI6c4E6_{VTWb-VNUfAjX=PM zfk?gy0P<02;5QMucm9%w-;8^Eyl9@Bl=DOK_rHh9imH~+pKSDRrr3srgno^vu*@z? z`so2%e81%a7aI_EYa;TCm!F?^e(pE^9U}VC5$Q}-Kf?Co9@pei3jLX@M??+Y`lGqa zHHxcd#Z*3BMb`Z?(w&y+Svv$R^Gh5JLPBF^e9RF5lpS?~P*4#B>`Y7nNH&5-LX5#+ zJP5|jaJa1K8QwHXSr}~gihW8|>=P*z${j2%)4_Xl@JzO{j!RpeUEw73%6~h7x1a7L z_enj>0H8e>a&vQ&wM+C_FU}Ps@mK+hINgy!9I5v?kR%169(VSuxZQLI?6!J}J4W)| z-wfwhKQsYF2D`h3dM%2WIYVD9spUwYwzi(+?<&!x>7Ud0blkD7iFT6YO#ZHvOOkNf ztzvsNC&SEO70?*2Xw?;5@*Gmg&({en+@dmj^z3?*YC;q7^72v@6if`8T~dO?@4cGR zi_2Ze8ZA{Z%xY|GIQSuVxp9sYX8}OhMl7F^g4>?BN7+)2y&G$P+^Y=|efge9*%Ar& z)rp%~m-V5GZG12tnNsf@6|XRsrx8zdZ(xb1s%O;g02Hy>c|4qrBZKiewT&`AU?Cg0 zffS^B+FRJ*eA$(iPjubc`&Q#zqI+qvN~eqmwZw4@Gi&}-hh7oH~6<)!h~2A&8Z zoGWADSw8e_W>oxHe*ICXdqS;NPxKl3QAB22+6Yst)nf2?{BXUG2c+4cHV3+QdJGZk8()!fKDzlIs>omf~*0k5;!MLyI2o zYGi%!8(p*;P;K43W_^woR;#kZ<}h?UB-OK3{`xGvNq9eOXHVF@cpYn>G^Z@{gbx<`Kh4zZ+RDsE!>=l$O!QFXpsiBn9ow}ov%C1^CqDF8VqZIv~ z4)VYn-wlwU`I^DN}O*MDM^8hXHH7@&MqQ6dLqF;aPOX%0N&!bc@{fV3k8D z{D=1ZSEhS-L2uamKytHaYgYeOSC_g`nV`>TNP~`4eMoJaOz(IT0N-gAor%zw#%|Hr lDLWaj4#tD taps++, + label: const Text('button'), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Title')); + await tester.pumpAndSettle(); + await tester.tap(find.byType(FButton), warnIfMissed: false); + expect(taps, 0); + + await tester.tap(find.text('Title')); + await tester.pumpAndSettle(); + await tester.tap(find.byType(FButton)); + expect(taps, 1); + }); + }); +} diff --git a/samples/lib/main.dart b/samples/lib/main.dart index 383a4cc74..93fdf7ce2 100644 --- a/samples/lib/main.dart +++ b/samples/lib/main.dart @@ -36,6 +36,7 @@ class _AppRouter extends RootStackRouter { @override List get routes => [ AutoRoute(page: EmptyRoute.page, initial: true), + AutoRoute(path: '/accordion/default', page: AccordionRoute.page), AutoRoute(path: '/alert/default', page: AlertRoute.page), AutoRoute(path: '/avatar/default', page: AvatarRoute.page), AutoRoute(path: '/avatar/raw', page: AvatarRawRoute.page), diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart new file mode 100644 index 000000000..86e8f4c35 --- /dev/null +++ b/samples/lib/widgets/accordion.dart @@ -0,0 +1,37 @@ + +import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; +import 'package:forui/forui.dart'; + +import 'package:forui_samples/sample_scaffold.dart'; + + +@RoutePage() +class AccordionPage extends SampleScaffold { + AccordionPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FAccordion( + title: 'Is it Styled?', + child: Text( + "Yes. It comes with default styles that matches the other components' aesthetics", + textAlign: TextAlign.left, + ), + ), + FAccordion( + title: 'Is it Animated?', + initiallyExpanded: false, + child: Text( + 'Yes. It is animated by default, but you can disable it if you prefer', + textAlign: TextAlign.left, + ), + ), + ], + ); +} From 55d0a4ac4ca2f129315e3c0a223a6a68dc680934 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 15 Sep 2024 19:52:08 +0800 Subject: [PATCH 09/57] Update forui/lib/src/widgets/accordion.dart Co-authored-by: Matthias Ngeo --- forui/lib/src/widgets/accordion.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index c35fb64cf..723a41f7a 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -71,7 +71,7 @@ class FAccordionController extends ChangeNotifier { } } -/// A vertically stacked set of interactive headings that each reveal a section of content. +/// An interactive heading that reveals a section of content. /// /// See: /// * https://forui.dev/docs/accordion for working examples. From 00ad586b3a0c1664a9d86d7511397be434604b37 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 15 Sep 2024 19:52:19 +0800 Subject: [PATCH 10/57] Update forui/lib/src/widgets/accordion.dart Co-authored-by: Matthias Ngeo --- forui/lib/src/widgets/accordion.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 723a41f7a..d8253e522 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -210,11 +210,6 @@ class _FAccordionState extends State with SingleTickerProviderStateM super.dispose(); } - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('controller', _controller)); - } } /// The [FAccordion] styles. From 850f04d8523005ebc5572d8c78a0516646ab7263 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 15 Sep 2024 11:53:44 +0000 Subject: [PATCH 11/57] Commit from GitHub Actions (Forui Presubmit) --- forui/lib/src/widgets/accordion.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index d8253e522..feec311a9 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -1,14 +1,16 @@ +import 'dart:math' as math; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; + import 'package:flutter_svg/flutter_svg.dart'; -import 'dart:math' as math; +import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; -import 'package:meta/meta.dart'; /// A controller that stores the expanded state of an [FAccordion]. class FAccordionController extends ChangeNotifier { @@ -209,7 +211,6 @@ class _FAccordionState extends State with SingleTickerProviderStateM } super.dispose(); } - } /// The [FAccordion] styles. From a560fa9e7b7b80b33dc0a0db399747dc4d3c6c94 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 15 Sep 2024 11:53:46 +0000 Subject: [PATCH 12/57] Commit from GitHub Actions (Forui Samples Presubmit) --- samples/lib/widgets/accordion.dart | 40 ++++++++++++++---------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart index 86e8f4c35..282fd62d8 100644 --- a/samples/lib/widgets/accordion.dart +++ b/samples/lib/widgets/accordion.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; @@ -6,7 +5,6 @@ import 'package:forui/forui.dart'; import 'package:forui_samples/sample_scaffold.dart'; - @RoutePage() class AccordionPage extends SampleScaffold { AccordionPage({ @@ -15,23 +13,23 @@ class AccordionPage extends SampleScaffold { @override Widget child(BuildContext context) => const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FAccordion( - title: 'Is it Styled?', - child: Text( - "Yes. It comes with default styles that matches the other components' aesthetics", - textAlign: TextAlign.left, - ), - ), - FAccordion( - title: 'Is it Animated?', - initiallyExpanded: false, - child: Text( - 'Yes. It is animated by default, but you can disable it if you prefer', - textAlign: TextAlign.left, - ), - ), - ], - ); + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FAccordion( + title: 'Is it Styled?', + child: Text( + "Yes. It comes with default styles that matches the other components' aesthetics", + textAlign: TextAlign.left, + ), + ), + FAccordion( + title: 'Is it Animated?', + initiallyExpanded: false, + child: Text( + 'Yes. It is animated by default, but you can disable it if you prefer', + textAlign: TextAlign.left, + ), + ), + ], + ); } From 31833902ad7650d35982800d7673d6f373d3166b Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 15 Sep 2024 20:29:28 +0800 Subject: [PATCH 13/57] fixed pr issues --- docs/pages/docs/accordion.mdx | 6 +- forui/example/lib/sandbox.dart | 2 +- forui/lib/src/widgets/accordion.dart | 206 +++++++++--------- forui/lib/widgets/accordion.dart | 4 +- .../accordion/{closed.png => hidden.png} | Bin .../accordion/{expanded.png => shown.png} | Bin .../accordion/accordion_golden_test.dart | 12 +- .../src/widgets/accordion/accordion_test.dart | 2 +- samples/lib/widgets/accordion.dart | 4 +- 9 files changed, 118 insertions(+), 118 deletions(-) rename forui/test/golden/accordion/{closed.png => hidden.png} (100%) rename forui/test/golden/accordion/{expanded.png => shown.png} (100%) diff --git a/docs/pages/docs/accordion.mdx b/docs/pages/docs/accordion.mdx index cc9559a5e..a31fe1df0 100644 --- a/docs/pages/docs/accordion.mdx +++ b/docs/pages/docs/accordion.mdx @@ -20,14 +20,14 @@ A vertically stacked set of interactive headings that each reveal a section of c mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - title: 'Is it Styled?', + title: Text('Is it Styled?'), child: Text( "Yes. It comes with default styles that matches the other components' aesthetics", textAlign: TextAlign.left, ), ), FAccordion( - title: 'Is it Animated?', + title: Text('Is it Animated?'), initiallyExpanded: false, child: Text( 'Yes. It is animated by default, but you can disable it if you prefer', @@ -46,7 +46,7 @@ A vertically stacked set of interactive headings that each reveal a section of c ```dart FAccordion( - title: 'Is it Styled?', + title: Text('Is it Styled?'), child: Text( "Yes. It comes with default styles that matches the other components' aesthetics", textAlign: TextAlign.left, diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index eecbe2129..c71e6b702 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -23,7 +23,7 @@ class _SandboxState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const FAccordion( - title: 'Is it Accessible?', + title: Text('Is it Accessible?'), child: Text( 'Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left, diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index feec311a9..1c2cac77b 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -42,20 +42,20 @@ class FAccordionController extends ChangeNotifier { /// Convenience method for toggling the current [expanded] status. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future toggle() async => expanded ? close() : expand(); + Future toggle() async => expanded ? hide() : show(); - /// Expands the accordion. + /// Shows the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future expand() async { + Future show() async { await _animation.forward(); notifyListeners(); } - /// closes the accordion. + /// Hides the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future close() async { + Future hide() async { await _animation.reverse(); notifyListeners(); } @@ -63,8 +63,8 @@ class FAccordionController extends ChangeNotifier { /// True if the accordion is expanded. False if it is closed. bool get expanded => _animation.value == 1.0; - /// The animation value. - double get value => _expand.value; + /// The percentage value of the animation. + double get percentage => _expand.value; @override void dispose() { @@ -82,12 +82,14 @@ class FAccordion extends StatefulWidget { final FAccordionStyle? style; /// The title. - final String title; + final Widget title; /// The accordion's controller. final FAccordionController? controller; /// Whether the accordion is initially expanded. Defaults to true. + /// + /// This flag will be ignored if a [controller] is provided. final bool initiallyExpanded; /// The child. @@ -97,24 +99,24 @@ class FAccordion extends StatefulWidget { const FAccordion({ required this.child, this.style, - this.title = '', + this.title = const Text(''), this.controller, this.initiallyExpanded = true, super.key, }); @override - //ignore:library_private_types_in_public_api - _FAccordionState createState() => _FAccordionState(); + State createState() => _FAccordionState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('style', style)) - ..add(StringProperty('title', title)) ..add(DiagnosticsProperty('controller', controller)) - ..add(DiagnosticsProperty('initiallyExpanded', initiallyExpanded)); + ..add( + FlagProperty('initiallyExpanded', value: initiallyExpanded, defaultValue: true, ifTrue: 'initiallyExpanded'), + ); } } @@ -167,15 +169,13 @@ class _FAccordionState extends State with SingleTickerProviderStateM applyHeightToFirstAscent: false, applyHeightToLastDescent: false, ), - style: TextStyle(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), - child: Text( - widget.title, - style: style.titleTextStyle, - ), + style: style.titleTextStyle + .copyWith(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), + child: widget.title, ), ), Transform.rotate( - angle: (_controller.value / 100 * -180 + 90) * math.pi / 180.0, + angle: (_controller.percentage / 100 * -180 + 90) * math.pi / 180.0, child: style.icon, ), ], @@ -187,9 +187,9 @@ class _FAccordionState extends State with SingleTickerProviderStateM // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the // child to layout at a smaller size while the amount of padding remains the same. _Expandable( - percentage: _controller.value / 100.0, + percentage: _controller.percentage / 100.0, child: ClipRect( - clipper: _Clipper(percentage: _controller.value / 100.0), + clipper: _Clipper(percentage: _controller.percentage / 100.0), child: Padding( padding: style.contentPadding, child: DefaultTextStyle(style: style.childTextStyle, child: widget.child), @@ -213,6 +213,88 @@ class _FAccordionState extends State with SingleTickerProviderStateM } } +class _Expandable extends SingleChildRenderObjectWidget { + final double _percentage; + + const _Expandable({ + required Widget child, + required double percentage, + }) : _percentage = percentage, + super(child: child); + + @override + RenderObject createRenderObject(BuildContext context) => _ExpandableBox(percentage: _percentage); + + @override + void updateRenderObject(BuildContext context, _ExpandableBox renderObject) { + renderObject.percentage = _percentage; + } +} + +class _ExpandableBox extends RenderBox with RenderObjectWithChildMixin { + double _percentage; + + _ExpandableBox({ + required double percentage, + }) : _percentage = percentage; + + @override + void performLayout() { + if (child case final child?) { + child.layout(constraints.normalize(), parentUsesSize: true); + size = Size(child.size.width, child.size.height * _percentage); + } else { + size = constraints.smallest; + } + } + + @override + void paint(PaintingContext context, Offset offset) { + if (child case final child?) { + context.paintChild(child, offset); + } + } + + @override + bool hitTest(BoxHitTestResult result, {required Offset position}) { + if (size.contains(position) && child!.hitTest(result, position: position)) { + result.add(BoxHitTestEntry(this, position)); + return true; + } + + return false; + } + + double get percentage => _percentage; + + set percentage(double value) { + if (_percentage == value) { + return; + } + + _percentage = value; + markNeedsLayout(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('percentage', percentage)); + } +} + +class _Clipper extends CustomClipper { + final double percentage; + + _Clipper({required this.percentage}); + + @override + Rect getClip(Size size) => Offset.zero & Size(size.width, size.height * percentage); + + @override + bool shouldReclip(covariant _Clipper oldClipper) => oldClipper.percentage != percentage; +} + /// The [FAccordion] styles. final class FAccordionStyle with Diagnosticable { /// The title's text style. @@ -311,85 +393,3 @@ final class FAccordionStyle with Diagnosticable { icon.hashCode ^ dividerColor.hashCode; } - -class _Expandable extends SingleChildRenderObjectWidget { - final double _percentage; - - const _Expandable({ - required Widget child, - required double percentage, - }) : _percentage = percentage, - super(child: child); - - @override - RenderObject createRenderObject(BuildContext context) => _ExpandableBox(percentage: _percentage); - - @override - void updateRenderObject(BuildContext context, _ExpandableBox renderObject) { - renderObject.percentage = _percentage; - } -} - -class _ExpandableBox extends RenderBox with RenderObjectWithChildMixin { - double _percentage; - - _ExpandableBox({ - required double percentage, - }) : _percentage = percentage; - - @override - void performLayout() { - if (child case final child?) { - child.layout(constraints.normalize(), parentUsesSize: true); - size = Size(child.size.width, child.size.height * _percentage); - } else { - size = constraints.smallest; - } - } - - @override - void paint(PaintingContext context, Offset offset) { - if (child case final child?) { - context.paintChild(child, offset); - } - } - - @override - bool hitTest(BoxHitTestResult result, {required Offset position}) { - if (size.contains(position) && child!.hitTest(result, position: position)) { - result.add(BoxHitTestEntry(this, position)); - return true; - } - - return false; - } - - double get percentage => _percentage; - - set percentage(double value) { - if (_percentage == value) { - return; - } - - _percentage = value; - markNeedsLayout(); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DoubleProperty('percentage', percentage)); - } -} - -class _Clipper extends CustomClipper { - final double percentage; - - _Clipper({required this.percentage}); - - @override - Rect getClip(Size size) => Offset.zero & Size(size.width, size.height * percentage); - - @override - bool shouldReclip(covariant _Clipper oldClipper) => oldClipper.percentage != percentage; -} diff --git a/forui/lib/widgets/accordion.dart b/forui/lib/widgets/accordion.dart index a2867fd57..2bcf059f9 100644 --- a/forui/lib/widgets/accordion.dart +++ b/forui/lib/widgets/accordion.dart @@ -1,8 +1,8 @@ /// {@category Widgets} /// -/// An image element with a fallback for representing the user. +/// An interactive heading that reveals a section of content. /// -/// See https://forui.dev/docs/avatar for working examples. +/// See https://forui.dev/docs/accordion for working examples. library forui.widgets.accordion; export '../src/widgets/accordion.dart'; diff --git a/forui/test/golden/accordion/closed.png b/forui/test/golden/accordion/hidden.png similarity index 100% rename from forui/test/golden/accordion/closed.png rename to forui/test/golden/accordion/hidden.png diff --git a/forui/test/golden/accordion/expanded.png b/forui/test/golden/accordion/shown.png similarity index 100% rename from forui/test/golden/accordion/expanded.png rename to forui/test/golden/accordion/shown.png diff --git a/forui/test/src/widgets/accordion/accordion_golden_test.dart b/forui/test/src/widgets/accordion/accordion_golden_test.dart index 4ff96897e..2291fd198 100644 --- a/forui/test/src/widgets/accordion/accordion_golden_test.dart +++ b/forui/test/src/widgets/accordion/accordion_golden_test.dart @@ -10,7 +10,7 @@ import '../../test_scaffold.dart'; void main() { group('FAccordion', () { - testWidgets('expanded', (tester) async { + testWidgets('shown', (tester) async { await tester.pumpWidget( MaterialApp( home: TestScaffold( @@ -19,7 +19,7 @@ void main() { mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - title: 'Title', + title: Text('Title'), child: ColoredBox( color: Colors.yellow, child: SizedBox.square( @@ -33,10 +33,10 @@ void main() { ), ); - await expectLater(find.byType(TestScaffold), matchesGoldenFile('accordion/expanded.png')); + await expectLater(find.byType(TestScaffold), matchesGoldenFile('accordion/shown.png')); }); - testWidgets('closed', (tester) async { + testWidgets('hidden', (tester) async { await tester.pumpWidget( MaterialApp( home: TestScaffold( @@ -45,7 +45,7 @@ void main() { mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - title: 'Title', + title: Text('Title'), child: ColoredBox( color: Colors.yellow, child: SizedBox.square( @@ -62,7 +62,7 @@ void main() { await tester.tap(find.text('Title')); await tester.pumpAndSettle(); - await expectLater(find.byType(TestScaffold), matchesGoldenFile('accordion/closed.png')); + await expectLater(find.byType(TestScaffold), matchesGoldenFile('accordion/hidden.png')); }); }); } diff --git a/forui/test/src/widgets/accordion/accordion_test.dart b/forui/test/src/widgets/accordion/accordion_test.dart index 043ecb270..cd086f427 100644 --- a/forui/test/src/widgets/accordion/accordion_test.dart +++ b/forui/test/src/widgets/accordion/accordion_test.dart @@ -18,7 +18,7 @@ void main() { home: TestScaffold( data: FThemes.zinc.light, child: FAccordion( - title: 'Title', + title: const Text('Title'), child: FButton( onPress: () => taps++, label: const Text('button'), diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart index 282fd62d8..583bb8960 100644 --- a/samples/lib/widgets/accordion.dart +++ b/samples/lib/widgets/accordion.dart @@ -16,14 +16,14 @@ class AccordionPage extends SampleScaffold { mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - title: 'Is it Styled?', + title: Text('Is it Styled?'), child: Text( "Yes. It comes with default styles that matches the other components' aesthetics", textAlign: TextAlign.left, ), ), FAccordion( - title: 'Is it Animated?', + title: Text('Is it Animated?'), initiallyExpanded: false, child: Text( 'Yes. It is animated by default, but you can disable it if you prefer', From 3756b3c33748a3bb9a39f5c5445126fd799c4ce7 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 15 Sep 2024 20:33:49 +0800 Subject: [PATCH 14/57] Update forui/lib/src/widgets/accordion.dart Co-authored-by: Matthias Ngeo --- forui/lib/src/widgets/accordion.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 1c2cac77b..61621a45a 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -78,7 +78,7 @@ class FAccordionController extends ChangeNotifier { /// See: /// * https://forui.dev/docs/accordion for working examples. class FAccordion extends StatefulWidget { - /// The accordion's style. Defaults to the appropriate style in [FThemeData.accordionStyle]. + /// The accordion's style. Defaults to [FThemeData.accordionStyle]. final FAccordionStyle? style; /// The title. From e2b9c2e858280774871d2cfd81e1958217c98e15 Mon Sep 17 00:00:00 2001 From: Matthias Ngeo Date: Sun, 15 Sep 2024 22:17:51 +0800 Subject: [PATCH 15/57] Update accordion --- forui/lib/src/widgets/accordion.dart | 69 +++++++++++++++------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 61621a45a..43f115d3d 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -14,30 +14,13 @@ import 'package:forui/src/foundation/util.dart'; /// A controller that stores the expanded state of an [FAccordion]. class FAccordionController extends ChangeNotifier { - late final AnimationController _animation; - late final Animation _expand; + AnimationController? _animation; + Animation? _expand; /// Creates a [FAccordionController]. FAccordionController({ - required TickerProvider vsync, duration = const Duration(milliseconds: 500), - bool initiallyExpanded = true, - }) { - _animation = AnimationController( - duration: duration, - value: initiallyExpanded ? 1.0 : 0.0, - vsync: vsync, - ); - _expand = Tween( - begin: 0, - end: 100, - ).animate( - CurvedAnimation( - curve: Curves.ease, - parent: _animation, - ), - )..addListener(notifyListeners); - } + }); /// Convenience method for toggling the current [expanded] status. /// @@ -48,7 +31,7 @@ class FAccordionController extends ChangeNotifier { /// /// This method should typically not be called while the widget tree is being rebuilt. Future show() async { - await _animation.forward(); + await _animation?.forward(); notifyListeners(); } @@ -56,19 +39,19 @@ class FAccordionController extends ChangeNotifier { /// /// This method should typically not be called while the widget tree is being rebuilt. Future hide() async { - await _animation.reverse(); + await _animation?.reverse(); notifyListeners(); } /// True if the accordion is expanded. False if it is closed. - bool get expanded => _animation.value == 1.0; + bool get expanded => _animation?.value == 1.0; /// The percentage value of the animation. double get percentage => _expand.value; @override void dispose() { - _animation.dispose(); + _animation?.dispose(); super.dispose(); } } @@ -127,11 +110,21 @@ class _FAccordionState extends State with SingleTickerProviderStateM @override void initState() { super.initState(); - _controller = widget.controller ?? - FAccordionController( - vsync: this, - initiallyExpanded: widget.initiallyExpanded, - ); + _controller = widget.controller ?? FAccordionController(initiallyExpanded: widget.initiallyExpanded); + _controller._animation = AnimationController( + duration: duration, + value: initiallyExpanded ? 1.0 : 0.0, + vsync: this, + ); + _controller._expand = Tween( + begin: 0, + end: 100, + ).animate( + CurvedAnimation( + curve: Curves.ease, + parent: _animation, + ), + ); } @override @@ -142,7 +135,21 @@ class _FAccordionState extends State with SingleTickerProviderStateM _controller.dispose(); } - _controller = widget.controller ?? FAccordionController(vsync: this); + _controller = widget.controller ?? FAccordionController(initiallyExpanded: widget.initiallyExpanded); + _controller._animation = AnimationController( + duration: duration, + value: initiallyExpanded ? 1.0 : 0.0, + vsync: this, + ); + _controller._expand = Tween( + begin: 0, + end: 100, + ).animate( + CurvedAnimation( + curve: Curves.ease, + parent: _animation, + ), + ); } } @@ -150,7 +157,7 @@ class _FAccordionState extends State with SingleTickerProviderStateM Widget build(BuildContext context) { final style = widget.style ?? context.theme.accordionStyle; return AnimatedBuilder( - animation: _controller, + animation: _controller._animation, builder: (context, _) => Column( children: [ MouseRegion( From 345b55d047544c7e86753209df9d18a74e68bf14 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 15 Sep 2024 22:35:31 +0800 Subject: [PATCH 16/57] Update accordion.dart --- forui/lib/src/widgets/accordion.dart | 73 +++++++++++++++------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 43f115d3d..0a59ec37b 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -14,23 +14,24 @@ import 'package:forui/src/foundation/util.dart'; /// A controller that stores the expanded state of an [FAccordion]. class FAccordionController extends ChangeNotifier { + final Duration duration; AnimationController? _animation; Animation? _expand; /// Creates a [FAccordionController]. FAccordionController({ - duration = const Duration(milliseconds: 500), + this.duration = const Duration(milliseconds: 500), }); /// Convenience method for toggling the current [expanded] status. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future toggle() async => expanded ? hide() : show(); + Future toggle() async => expanded ? collapse() : expand(); /// Shows the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future show() async { + Future expand() async { await _animation?.forward(); notifyListeners(); } @@ -38,7 +39,7 @@ class FAccordionController extends ChangeNotifier { /// Hides the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future hide() async { + Future collapse() async { await _animation?.reverse(); notifyListeners(); } @@ -47,7 +48,7 @@ class FAccordionController extends ChangeNotifier { bool get expanded => _animation?.value == 1.0; /// The percentage value of the animation. - double get percentage => _expand.value; + double get percentage => _expand!.value; @override void dispose() { @@ -110,21 +111,22 @@ class _FAccordionState extends State with SingleTickerProviderStateM @override void initState() { super.initState(); - _controller = widget.controller ?? FAccordionController(initiallyExpanded: widget.initiallyExpanded); - _controller._animation = AnimationController( - duration: duration, - value: initiallyExpanded ? 1.0 : 0.0, - vsync: this, - ); - _controller._expand = Tween( - begin: 0, - end: 100, - ).animate( - CurvedAnimation( - curve: Curves.ease, - parent: _animation, - ), - ); + _controller = widget.controller ?? FAccordionController(); + _controller + .._animation = AnimationController( + duration: _controller.duration, + value: widget.initiallyExpanded ? 1.0 : 0.0, + vsync: this, + ) + .._expand = Tween( + begin: 0, + end: 100, + ).animate( + CurvedAnimation( + curve: Curves.ease, + parent: _controller._animation, + ), + ); } @override @@ -135,21 +137,22 @@ class _FAccordionState extends State with SingleTickerProviderStateM _controller.dispose(); } - _controller = widget.controller ?? FAccordionController(initiallyExpanded: widget.initiallyExpanded); - _controller._animation = AnimationController( - duration: duration, - value: initiallyExpanded ? 1.0 : 0.0, - vsync: this, - ); - _controller._expand = Tween( - begin: 0, - end: 100, - ).animate( - CurvedAnimation( - curve: Curves.ease, - parent: _animation, - ), - ); + _controller = widget.controller ?? FAccordionController(); + _controller + .._animation = AnimationController( + duration: _controller.duration, + value: widget.initiallyExpanded ? 1.0 : 0.0, + vsync: this, + ) + .._expand = Tween( + begin: 0, + end: 100, + ).animate( + CurvedAnimation( + curve: Curves.ease, + parent: _controller._animation, + ), + ); } } From 4a279397e8307cdb779e9d58f4617e7914c676a8 Mon Sep 17 00:00:00 2001 From: Pante Date: Sun, 15 Sep 2024 14:19:20 +0000 Subject: [PATCH 17/57] Commit from GitHub Actions (Forui Presubmit) --- forui/lib/src/widgets/accordion.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 0a59ec37b..d289cee25 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -19,9 +19,7 @@ class FAccordionController extends ChangeNotifier { Animation? _expand; /// Creates a [FAccordionController]. - FAccordionController({ - this.duration = const Duration(milliseconds: 500), - }); + FAccordionController(); /// Convenience method for toggling the current [expanded] status. /// From 2ac6ec236c2c7d91cc8895a8a9c0d43f71fa7071 Mon Sep 17 00:00:00 2001 From: Matthias Ngeo Date: Sun, 15 Sep 2024 23:09:14 +0800 Subject: [PATCH 18/57] Sketch accordion --- forui/lib/src/widgets/accordion.dart | 163 ++++++++++++++------------- forui/lib/widgets/accordion.dart | 2 - 2 files changed, 84 insertions(+), 81 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index d289cee25..17fa21d41 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -12,145 +12,150 @@ import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; -/// A controller that stores the expanded state of an [FAccordion]. +/// A controller that stores the expanded state of an [_Item]. class FAccordionController extends ChangeNotifier { final Duration duration; - AnimationController? _animation; - Animation? _expand; + final Map _controllers; + final int? min; + final int? max; /// Creates a [FAccordionController]. FAccordionController(); + void addItem(int index, AnimationController controller, Animation expand) { + _controllers[index] = (controller, expand); + } + /// Convenience method for toggling the current [expanded] status. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future toggle() async => expanded ? collapse() : expand(); + Future toggle(int index) async {} /// Shows the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future expand() async { - await _animation?.forward(); - notifyListeners(); - } + Future expand(int index) async {} /// Hides the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future collapse() async { - await _animation?.reverse(); - notifyListeners(); - } + Future collapse(int index) async {} - /// True if the accordion is expanded. False if it is closed. - bool get expanded => _animation?.value == 1.0; + void removeItem(int index) => _controllers.remove(index); +} + +class FAccordion extends StatefulWidget { + final FAccordionController? controller; + final List items; - /// The percentage value of the animation. - double get percentage => _expand!.value; + const FAccordion({super.key}); @override - void dispose() { - _animation?.dispose(); - super.dispose(); - } + State createState() => _FAccordionState(); } +class _FAccordionState extends State { + late final FAccordionController _controller; + + @override + Widget build(BuildContext context) => Column(children: [ + + ],); +} + +class FAccordionItem { + /// The title. + final Widget title; + + /// The child. + final Widget child; + + final bool initiallyExpanded; + + FAccordionItem({required this.title, required this.child, this.initiallyExpanded = false}); +} + + /// An interactive heading that reveals a section of content. /// /// See: /// * https://forui.dev/docs/accordion for working examples. -class FAccordion extends StatefulWidget { +class _Item extends StatefulWidget { /// The accordion's style. Defaults to [FThemeData.accordionStyle]. - final FAccordionStyle? style; - - /// The title. - final Widget title; + final FAccordionStyle style; /// The accordion's controller. - final FAccordionController? controller; + final FAccordionController controller; - /// Whether the accordion is initially expanded. Defaults to true. - /// - /// This flag will be ignored if a [controller] is provided. - final bool initiallyExpanded; + final FAccordionItem item; - /// The child. - final Widget child; + final int index; - /// Creates an [FAccordion]. - const FAccordion({ - required this.child, - this.style, - this.title = const Text(''), - this.controller, - this.initiallyExpanded = true, - super.key, + /// Creates an [_Item]. + const _Item({ + required this.style, + required this.index, }); @override - State createState() => _FAccordionState(); + State<_Item> createState() => _ItemState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('style', style)) - ..add(DiagnosticsProperty('controller', controller)) - ..add( - FlagProperty('initiallyExpanded', value: initiallyExpanded, defaultValue: true, ifTrue: 'initiallyExpanded'), - ); + ..add(DiagnosticsProperty('controller', controller)); } } -class _FAccordionState extends State with SingleTickerProviderStateMixin { - late FAccordionController _controller; +class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _expand; bool _hovered = false; @override void initState() { super.initState(); - _controller = widget.controller ?? FAccordionController(); - _controller - .._animation = AnimationController( - duration: _controller.duration, - value: widget.initiallyExpanded ? 1.0 : 0.0, - vsync: this, - ) - .._expand = Tween( + _controller = AnimationController( + duration: _controller.duration, + value: widget.initiallyExpanded ? 1.0 : 0.0, + vsync: this, + ); + _expand = Tween( begin: 0, end: 100, ).animate( CurvedAnimation( curve: Curves.ease, - parent: _controller._animation, + parent: _controller._controllers, ), ); + + widget.controller.addItem(widget.index, _controller, _expand); } @override - void didUpdateWidget(covariant FAccordion old) { + void didUpdateWidget(covariant _Item old) { super.didUpdateWidget(old); if (widget.controller != old.controller) { - if (old.controller == null) { - _controller.dispose(); - } - - _controller = widget.controller ?? FAccordionController(); - _controller - .._animation = AnimationController( - duration: _controller.duration, - value: widget.initiallyExpanded ? 1.0 : 0.0, - vsync: this, - ) - .._expand = Tween( - begin: 0, - end: 100, - ).animate( - CurvedAnimation( - curve: Curves.ease, - parent: _controller._animation, - ), - ); + _controller = AnimationController( + duration: _controller.duration, + value: widget.initiallyExpanded ? 1.0 : 0.0, + vsync: this, + ); + _expand = Tween( + begin: 0, + end: 100, + ).animate( + CurvedAnimation( + curve: Curves.ease, + parent: _controller._controllers, + ), + ); + + old.controller.removeItem(old.index); + widget.controller.addItem(widget.index, _controller, _expand); } } @@ -158,7 +163,7 @@ class _FAccordionState extends State with SingleTickerProviderStateM Widget build(BuildContext context) { final style = widget.style ?? context.theme.accordionStyle; return AnimatedBuilder( - animation: _controller._animation, + animation: _controller._controllers, builder: (context, _) => Column( children: [ MouseRegion( @@ -303,7 +308,7 @@ class _Clipper extends CustomClipper { bool shouldReclip(covariant _Clipper oldClipper) => oldClipper.percentage != percentage; } -/// The [FAccordion] styles. +/// The [_Item] styles. final class FAccordionStyle with Diagnosticable { /// The title's text style. final TextStyle titleTextStyle; diff --git a/forui/lib/widgets/accordion.dart b/forui/lib/widgets/accordion.dart index 2bcf059f9..0bbc299be 100644 --- a/forui/lib/widgets/accordion.dart +++ b/forui/lib/widgets/accordion.dart @@ -4,5 +4,3 @@ /// /// See https://forui.dev/docs/accordion for working examples. library forui.widgets.accordion; - -export '../src/widgets/accordion.dart'; From d7f1265d9ef295e4394eb378a04cfc1cf70753dc Mon Sep 17 00:00:00 2001 From: Matthias Ngeo Date: Sun, 15 Sep 2024 23:11:03 +0800 Subject: [PATCH 19/57] ops --- forui/lib/src/widgets/accordion.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 17fa21d41..cf198e080 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -15,7 +15,7 @@ import 'package:forui/src/foundation/util.dart'; /// A controller that stores the expanded state of an [_Item]. class FAccordionController extends ChangeNotifier { final Duration duration; - final Map _controllers; + final List<(AnimationController, Animation)> _controllers; final int? min; final int? max; From 9c5bca2f1d846aae2fbb6c9ac14fcc16eeac97c0 Mon Sep 17 00:00:00 2001 From: Pante Date: Sun, 15 Sep 2024 15:12:26 +0000 Subject: [PATCH 20/57] Commit from GitHub Actions (Forui Samples Presubmit) --- samples/lib/widgets/accordion.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart index 583bb8960..4f2699c5d 100644 --- a/samples/lib/widgets/accordion.dart +++ b/samples/lib/widgets/accordion.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:forui/forui.dart'; import 'package:forui_samples/sample_scaffold.dart'; From c9900d0cb766edb213d88b1f98c0d6bcf2962ce5 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 15 Sep 2024 23:48:33 +0800 Subject: [PATCH 21/57] Fixed branch issues, ready to reimplement accordion group --- forui/example/lib/sandbox.dart | 23 +++-- forui/lib/src/widgets/accordion.dart | 145 +++++++++++++++++++++------ forui/lib/widgets/accordion.dart | 2 + 3 files changed, 131 insertions(+), 39 deletions(-) diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index c71e6b702..a8c6d3207 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -22,12 +22,23 @@ class _SandboxState extends State { Widget build(BuildContext context) => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const FAccordion( - title: Text('Is it Accessible?'), - child: Text( - 'Yes. It adheres to the WAI-ARIA design pattern', - textAlign: TextAlign.left, - ), + FAccordion( + items: [ + FAccordionItem( + title: const Text('Title 1'), + child: const Text( + 'Yes. It adheres to the WAI-ARIA design pattern', + textAlign: TextAlign.left, + ), + ), + FAccordionItem( + title: const Text('Title 2'), + child: const Text( + 'Yes. It adheres to the WAI-ARIA design pattern', + textAlign: TextAlign.left, + ), + ), + ], ), const SizedBox(height: 20), FSelectGroup( diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index cf198e080..89c7df6d1 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -13,19 +12,37 @@ import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; /// A controller that stores the expanded state of an [_Item]. -class FAccordionController extends ChangeNotifier { +interface class FAccordionController extends ChangeNotifier { final Duration duration; final List<(AnimationController, Animation)> _controllers; - final int? min; - final int? max; + final int _min; + final int? _max; /// Creates a [FAccordionController]. - FAccordionController(); + /// + /// The [min] and [max] values are the minimum and maximum number of selections allowed. Defaults to no minimum or maximum. + /// + /// # Contract: + /// * Throws [AssertionError] if [min] < 0. + /// * Throws [AssertionError] if [max] < 0. + /// * Throws [AssertionError] if [min] > [max]. + FAccordionController({ + int min = 0, + int? max, + this.duration = const Duration(milliseconds: 500), + }) : _min = min, + _max = max, + _controllers = [], + assert(min >= 0, 'The min value must be greater than or equal to 0.'), + assert(max == null || max >= 0, 'The max value must be greater than or equal to 0.'), + assert(max == null || min <= max, 'The max value must be greater than or equal to the min value.'); void addItem(int index, AnimationController controller, Animation expand) { _controllers[index] = (controller, expand); } + void removeItem(int index) => _controllers.remove(index); + /// Convenience method for toggling the current [expanded] status. /// /// This method should typically not be called while the widget tree is being rebuilt. @@ -40,15 +57,56 @@ class FAccordionController extends ChangeNotifier { /// /// This method should typically not be called while the widget tree is being rebuilt. Future collapse(int index) async {} - - void removeItem(int index) => _controllers.remove(index); } +// class RadioAccordionController implements FAccordionController { +// final Duration duration; +// final List<(AnimationController, Animation)> _controllers; +// final int? min; +// final int? max; +// +// RadioAccordionController({ +// this.duration = const Duration(milliseconds: 500), +// }) : super(duration: duration); +// +// @override +// void addItem(int index, AnimationController controller, Animation expand) { +// _controllers[index] = (controller, expand); +// } +// +// @override +// void removeItem(int index) => _controllers.remove(index); +// +// @override +// Future toggle(int index) async { +// final controller = _controllers[index].item; +// controller.isCompleted ? controller.reverse() : controller.forward(); +// } +// +// @override +// Future expand(int index) async { +// final controller = _controllers[index].item; +// controller.forward(); +// } +// +// @override +// Future collapse(int index) async { +// final controller = _controllers[index].item1; +// controller.reverse(); +// } +// } + class FAccordion extends StatefulWidget { final FAccordionController? controller; final List items; + final FAccordionStyle? style; - const FAccordion({super.key}); + const FAccordion({ + required this.items, + this.controller, + this.style, + super.key, + }); @override State createState() => _FAccordionState(); @@ -58,9 +116,26 @@ class _FAccordionState extends State { late final FAccordionController _controller; @override - Widget build(BuildContext context) => Column(children: [ + void initState() { + super.initState(); + _controller = widget.controller ?? FAccordionController(); + } - ],); + @override + Widget build(BuildContext context) { + final style = widget.style ?? context.theme.accordionStyle; + return Column( + children: [ + for (var i = 0; i < widget.items.length; i++) + _Item( + style: style, + index: i, + item: widget.items[i], + controller: _controller, + ), + ], + ); + } } class FAccordionItem { @@ -70,12 +145,12 @@ class FAccordionItem { /// The child. final Widget child; + /// Whether the item is initially expanded. final bool initiallyExpanded; FAccordionItem({required this.title, required this.child, this.initiallyExpanded = false}); } - /// An interactive heading that reveals a section of content. /// /// See: @@ -95,6 +170,8 @@ class _Item extends StatefulWidget { const _Item({ required this.style, required this.index, + required this.item, + required this.controller, }); @override @@ -119,19 +196,18 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { super.initState(); _controller = AnimationController( duration: _controller.duration, - value: widget.initiallyExpanded ? 1.0 : 0.0, + value: widget.item.initiallyExpanded ? 1.0 : 0.0, vsync: this, ); _expand = Tween( - begin: 0, - end: 100, - ).animate( - CurvedAnimation( - curve: Curves.ease, - parent: _controller._controllers, - ), - ); - + begin: 0, + end: 100, + ).animate( + CurvedAnimation( + curve: Curves.ease, + parent: _controller, + ), + ); widget.controller.addItem(widget.index, _controller, _expand); } @@ -141,7 +217,7 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { if (widget.controller != old.controller) { _controller = AnimationController( duration: _controller.duration, - value: widget.initiallyExpanded ? 1.0 : 0.0, + value: widget.item.initiallyExpanded ? 1.0 : 0.0, vsync: this, ); _expand = Tween( @@ -150,7 +226,7 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { ).animate( CurvedAnimation( curve: Curves.ease, - parent: _controller._controllers, + parent: _controller, ), ); @@ -163,14 +239,20 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { Widget build(BuildContext context) { final style = widget.style ?? context.theme.accordionStyle; return AnimatedBuilder( - animation: _controller._controllers, + animation: _controller, builder: (context, _) => Column( children: [ MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: FTappable( - onPress: () => _controller.toggle(), + onPress: () { + if (_controller.value == 1) { + _controller.reverse(); + } else { + _controller.forward(); + } + }, child: Container( padding: style.titlePadding, child: Row( @@ -184,11 +266,11 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { ), style: style.titleTextStyle .copyWith(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), - child: widget.title, + child: widget.item.title, ), ), Transform.rotate( - angle: (_controller.percentage / 100 * -180 + 90) * math.pi / 180.0, + angle: (_controller.value / 100 * -180 + 90) * math.pi / 180.0, child: style.icon, ), ], @@ -200,12 +282,12 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the // child to layout at a smaller size while the amount of padding remains the same. _Expandable( - percentage: _controller.percentage / 100.0, + percentage: _controller.value / 100.0, child: ClipRect( - clipper: _Clipper(percentage: _controller.percentage / 100.0), + clipper: _Clipper(percentage: _controller.value / 100.0), child: Padding( padding: style.contentPadding, - child: DefaultTextStyle(style: style.childTextStyle, child: widget.child), + child: DefaultTextStyle(style: style.childTextStyle, child: widget.item.child), ), ), ), @@ -219,9 +301,6 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { @override void dispose() { - if (widget.controller == null) { - _controller.dispose(); - } super.dispose(); } } diff --git a/forui/lib/widgets/accordion.dart b/forui/lib/widgets/accordion.dart index 0bbc299be..2bcf059f9 100644 --- a/forui/lib/widgets/accordion.dart +++ b/forui/lib/widgets/accordion.dart @@ -4,3 +4,5 @@ /// /// See https://forui.dev/docs/accordion for working examples. library forui.widgets.accordion; + +export '../src/widgets/accordion.dart'; From 08e677494f31c31aacb6fab07a82bdf5510319bd Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 15 Sep 2024 15:51:41 +0000 Subject: [PATCH 22/57] Commit from GitHub Actions (Forui Presubmit) --- forui/lib/src/widgets/accordion.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 89c7df6d1..1161b420c 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -110,6 +110,14 @@ class FAccordion extends StatefulWidget { @override State createState() => _FAccordionState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('controller', controller)); + properties.add(IterableProperty('items', items)); + properties.add(DiagnosticsProperty('style', style)); + } } class _FAccordionState extends State { @@ -183,6 +191,8 @@ class _Item extends StatefulWidget { properties ..add(DiagnosticsProperty('style', style)) ..add(DiagnosticsProperty('controller', controller)); + properties.add(DiagnosticsProperty('item', item)); + properties.add(IntProperty('index', index)); } } @@ -298,11 +308,6 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { ), ); } - - @override - void dispose() { - super.dispose(); - } } class _Expandable extends SingleChildRenderObjectWidget { From 93c2cb1aafce0748ea5188bfd31cfb6690554408 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Mon, 16 Sep 2024 19:11:24 +0800 Subject: [PATCH 23/57] individual controllers hooked up --- forui/example/lib/sandbox.dart | 1 + forui/lib/src/widgets/accordion.dart | 54 +++++++++++++--------------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index a8c6d3207..f1ec2425a 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -26,6 +26,7 @@ class _SandboxState extends State { items: [ FAccordionItem( title: const Text('Title 1'), + initiallyExpanded: true, child: const Text( 'Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left, diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 1161b420c..a43ae9a3c 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -38,7 +38,7 @@ interface class FAccordionController extends ChangeNotifier { assert(max == null || min <= max, 'The max value must be greater than or equal to the min value.'); void addItem(int index, AnimationController controller, Animation expand) { - _controllers[index] = (controller, expand); + _controllers.add((controller, expand)); } void removeItem(int index) => _controllers.remove(index); @@ -115,8 +115,6 @@ class FAccordion extends StatefulWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); - properties.add(IterableProperty('items', items)); - properties.add(DiagnosticsProperty('style', style)); } } @@ -205,7 +203,7 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { void initState() { super.initState(); _controller = AnimationController( - duration: _controller.duration, + duration: widget.controller.duration, value: widget.item.initiallyExpanded ? 1.0 : 0.0, vsync: this, ); @@ -226,7 +224,7 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { super.didUpdateWidget(old); if (widget.controller != old.controller) { _controller = AnimationController( - duration: _controller.duration, + duration: widget.controller.duration, value: widget.item.initiallyExpanded ? 1.0 : 0.0, vsync: this, ); @@ -246,25 +244,24 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { } @override - Widget build(BuildContext context) { - final style = widget.style ?? context.theme.accordionStyle; - return AnimatedBuilder( + Widget build(BuildContext context) => AnimatedBuilder( animation: _controller, builder: (context, _) => Column( + crossAxisAlignment: CrossAxisAlignment.start, // Should all content in the accordion be left-aligned? children: [ - MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: FTappable( - onPress: () { - if (_controller.value == 1) { - _controller.reverse(); - } else { - _controller.forward(); - } - }, + FTappable( + onPress: () { + if (_controller.value == 1) { + _controller.reverse(); + } else { + _controller.forward(); + } + }, + child: MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), child: Container( - padding: style.titlePadding, + padding: widget.style.titlePadding, child: Row( children: [ Expanded( @@ -274,14 +271,14 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { applyHeightToFirstAscent: false, applyHeightToLastDescent: false, ), - style: style.titleTextStyle + style: widget.style.titleTextStyle .copyWith(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), child: widget.item.title, ), ), Transform.rotate( - angle: (_controller.value / 100 * -180 + 90) * math.pi / 180.0, - child: style.icon, + angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, + child: widget.style.icon, ), ], ), @@ -292,22 +289,21 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the // child to layout at a smaller size while the amount of padding remains the same. _Expandable( - percentage: _controller.value / 100.0, + percentage: _expand.value / 100.0, child: ClipRect( - clipper: _Clipper(percentage: _controller.value / 100.0), + clipper: _Clipper(percentage: _expand.value / 100.0), child: Padding( - padding: style.contentPadding, - child: DefaultTextStyle(style: style.childTextStyle, child: widget.item.child), + padding: widget.style.contentPadding, + child: DefaultTextStyle(style: widget.style.childTextStyle, child: widget.item.child), ), ), ), FDivider( - style: context.theme.dividerStyles.horizontal.copyWith(padding: EdgeInsets.zero, color: style.dividerColor), + style: context.theme.dividerStyles.horizontal.copyWith(padding: EdgeInsets.zero, color: widget.style.dividerColor), ), ], ), ); - } } class _Expandable extends SingleChildRenderObjectWidget { From 151542699ade75692857d0c512d4bfda5b803360 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Tue, 17 Sep 2024 02:21:50 +0800 Subject: [PATCH 24/57] Hooked up to FAcoordion controller, RadioFAccordionController next --- forui/lib/src/widgets/accordion.dart | 135 +++++++++++++++------------ 1 file changed, 76 insertions(+), 59 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index a43ae9a3c..b1ca4b808 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -27,7 +27,7 @@ interface class FAccordionController extends ChangeNotifier { /// * Throws [AssertionError] if [max] < 0. /// * Throws [AssertionError] if [min] > [max]. FAccordionController({ - int min = 0, + int min = 1, int? max, this.duration = const Duration(milliseconds: 500), }) : _min = min, @@ -46,17 +46,39 @@ interface class FAccordionController extends ChangeNotifier { /// Convenience method for toggling the current [expanded] status. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future toggle(int index) async {} + Future toggle(int index) async { + if (_max != null && _controllers.length >= _max) { + print('Cannot expand more than $_max items.'); + return; + } + + if (_controllers.length <= _min) { + print('Cannot collapse more than $_min items.'); + return; + } + + return await _controllers[index].$2.value == 100 ? collapse(index) : expand(index); + } /// Shows the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future expand(int index) async {} + Future expand(int index) async { + final controller = _controllers[index].$1; + await controller.forward(); + notifyListeners(); + } /// Hides the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future collapse(int index) async {} + Future collapse(int index) async { + final controller = _controllers[index].$1; + await controller.reverse(); + notifyListeners(); + } + + double percentage(int index) => _controllers[index].$2.value / 100; } // class RadioAccordionController implements FAccordionController { @@ -188,9 +210,9 @@ class _Item extends StatefulWidget { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('style', style)) - ..add(DiagnosticsProperty('controller', controller)); - properties.add(DiagnosticsProperty('item', item)); - properties.add(IntProperty('index', index)); + ..add(DiagnosticsProperty('controller', controller)) + ..add(DiagnosticsProperty('item', item)) + ..add(IntProperty('index', index)); } } @@ -245,65 +267,60 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) => AnimatedBuilder( - animation: _controller, - builder: (context, _) => Column( - crossAxisAlignment: CrossAxisAlignment.start, // Should all content in the accordion be left-aligned? - children: [ - FTappable( - onPress: () { - if (_controller.value == 1) { - _controller.reverse(); - } else { - _controller.forward(); - } - }, - child: MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: Container( - padding: widget.style.titlePadding, - child: Row( - children: [ - Expanded( - child: merge( - // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. - textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, + animation: widget.controller._controllers[widget.index].$2, + builder: (context, _) => Column( + crossAxisAlignment: CrossAxisAlignment.start, //TODO: Should all content in the accordion be left-aligned? + children: [ + FTappable( + onPress: () => widget.controller.toggle(widget.index), + child: MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + child: Container( + padding: widget.style.titlePadding, + child: Row( + children: [ + Expanded( + child: merge( + // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + style: widget.style.titleTextStyle + .copyWith(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), + child: widget.item.title, ), - style: widget.style.titleTextStyle - .copyWith(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), - child: widget.item.title, ), - ), - Transform.rotate( - angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, - child: widget.style.icon, - ), - ], + Transform.rotate( + angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, //TODO: use FAccordionController to get the percentage + child: widget.style.icon, + ), + ], + ), ), ), ), - ), - // We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by - // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the - // child to layout at a smaller size while the amount of padding remains the same. - _Expandable( - percentage: _expand.value / 100.0, - child: ClipRect( - clipper: _Clipper(percentage: _expand.value / 100.0), - child: Padding( - padding: widget.style.contentPadding, - child: DefaultTextStyle(style: widget.style.childTextStyle, child: widget.item.child), + // We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by + // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the + // child to layout at a smaller size while the amount of padding remains the same. + _Expandable( + percentage: _expand.value / 100, //TODO: use FAccordionController to get the percentage + child: ClipRect( + clipper: _Clipper(percentage: _expand.value / 100), //TODO: use FAccordionController to get the percentage + child: Padding( + padding: widget.style.contentPadding, + child: DefaultTextStyle(style: widget.style.childTextStyle, child: widget.item.child), + ), ), ), - ), - FDivider( - style: context.theme.dividerStyles.horizontal.copyWith(padding: EdgeInsets.zero, color: widget.style.dividerColor), - ), - ], - ), - ); + FDivider( + style: context.theme.dividerStyles.horizontal + .copyWith(padding: EdgeInsets.zero, color: widget.style.dividerColor), + ), + ], + ), + ); } class _Expandable extends SingleChildRenderObjectWidget { From 6be9e140a2425fa8e6ff94f06ae749dc3e45b558 Mon Sep 17 00:00:00 2001 From: Matthias Ngeo Date: Thu, 19 Sep 2024 11:29:32 +0800 Subject: [PATCH 25/57] Update installation guide (#197) * Update installation guode * Update changelog * Commit from GitHub Actions (Forui Presubmit) --------- Co-authored-by: Pante --- forui/example/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forui/example/pubspec.lock b/forui/example/pubspec.lock index 3569e2b1e..dd43f20f3 100644 --- a/forui/example/pubspec.lock +++ b/forui/example/pubspec.lock @@ -243,7 +243,7 @@ packages: path: ".." relative: true source: path - version: "0.5.1" + version: "0.5.0" forui_assets: dependency: transitive description: From 983da2dc8206d718dab120591d9fc2f15d27f608 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Thu, 19 Sep 2024 17:49:12 +0800 Subject: [PATCH 26/57] Accordion ready for review --- docs/pages/docs/accordion.mdx | 86 +++++++++--- forui/example/lib/sandbox.dart | 19 ++- forui/lib/src/widgets/accordion.dart | 132 ++++++++++-------- forui/test/golden/accordion/hidden.png | Bin 4023 -> 4021 bytes forui/test/golden/accordion/shown.png | Bin 4096 -> 4090 bytes .../accordion/accordion_golden_test.dart | 38 +++-- .../src/widgets/accordion/accordion_test.dart | 15 +- samples/lib/widgets/accordion.dart | 46 ++++-- 8 files changed, 226 insertions(+), 110 deletions(-) diff --git a/docs/pages/docs/accordion.mdx b/docs/pages/docs/accordion.mdx index a31fe1df0..d4b3e6d76 100644 --- a/docs/pages/docs/accordion.mdx +++ b/docs/pages/docs/accordion.mdx @@ -20,19 +20,25 @@ A vertically stacked set of interactive headings that each reveal a section of c mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - title: Text('Is it Styled?'), - child: Text( - "Yes. It comes with default styles that matches the other components' aesthetics", - textAlign: TextAlign.left, - ), - ), - FAccordion( - title: Text('Is it Animated?'), - initiallyExpanded: false, - child: Text( - 'Yes. It is animated by default, but you can disable it if you prefer', - textAlign: TextAlign.left, - ), + items: [ + FAccordionItem( + title: const Text('Is it accessible?'), + child: const Text('Yes. It adheres to the WAI-ARIA design pattern.'), + ), + FAccordionItem( + title: const Text('Is it Styled?'), + initiallyExpanded: true, + child: const Text( + "Yes. It comes with default styles that matches the other components' aesthetics", + ), + ), + FAccordionItem( + title: const Text('Is it Animated?'), + child: const Text( + 'Yes. It is animated by default, but you can disable it if you prefer', + ), + ), + ], ), ], ); @@ -46,10 +52,52 @@ A vertically stacked set of interactive headings that each reveal a section of c ```dart FAccordion( - title: Text('Is it Styled?'), - child: Text( - "Yes. It comes with default styles that matches the other components' aesthetics", - textAlign: TextAlign.left, - ), -); + controller: FAccordionController(), // or FRadioAccordionController() + items: [ + FAccordionItem( + title: const Text('Is it accessible?'), + child: const Text('Yes. It adheres to the WAI-ARIA design pattern.'), + ), + ], +), ``` + +## Examples + +### With FRadioAccordionController + + + + + + ```dart + const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FAccordion( + controller: FRadioAccordionController(), + items: [ + FAccordionItem( + title: const Text('Is it accessible?'), + child: const Text('Yes. It adheres to the WAI-ARIA design pattern.'), + ), + FAccordionItem( + title: const Text('Is it Styled?'), + initiallyExpanded: true, + child: const Text( + "Yes. It comes with default styles that matches the other components' aesthetics", + ), + ), + FAccordionItem( + title: const Text('Is it Animated?'), + child: const Text( + 'Yes. It is animated by default, but you can disable it if you prefer', + ), + ), + ], + ), + ], + ); + ``` + + \ No newline at end of file diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index f1ec2425a..7db29e265 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -27,13 +27,30 @@ class _SandboxState extends State { FAccordionItem( title: const Text('Title 1'), initiallyExpanded: true, + child: const Text( + 'Yes. It adheres to the WAI-ARIA design pattern, wfihwe fdhfiwf dfhwiodf dfwhoif', + ), + ), + FAccordionItem( + title: const Text('Title 2'), + child: Container( + width: 100, + color: Colors.yellow, + child: const Text( + 'Yes. It adheres to the WAI-ARIA design pattern geg wjfiweo dfjiowjf dfjio', + textAlign: TextAlign.center, + ), + ), + ), + FAccordionItem( + title: const Text('Title 3'), child: const Text( 'Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left, ), ), FAccordionItem( - title: const Text('Title 2'), + title: const Text('Title 4'), child: const Text( 'Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left, diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index b1ca4b808..9ecdf21f5 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -13,8 +13,10 @@ import 'package:forui/src/foundation/util.dart'; /// A controller that stores the expanded state of an [_Item]. interface class FAccordionController extends ChangeNotifier { + /// The duration of the expansion and collapse animations. final Duration duration; final List<(AnimationController, Animation)> _controllers; + final Set _values; final int _min; final int? _max; @@ -27,37 +29,50 @@ interface class FAccordionController extends ChangeNotifier { /// * Throws [AssertionError] if [max] < 0. /// * Throws [AssertionError] if [min] > [max]. FAccordionController({ - int min = 1, + int min = 0, int? max, + Set? values, this.duration = const Duration(milliseconds: 500), }) : _min = min, _max = max, _controllers = [], + _values = values ?? {}, assert(min >= 0, 'The min value must be greater than or equal to 0.'), assert(max == null || max >= 0, 'The max value must be greater than or equal to 0.'), assert(max == null || min <= max, 'The max value must be greater than or equal to the min value.'); + /// Adds an item to the accordion. void addItem(int index, AnimationController controller, Animation expand) { _controllers.add((controller, expand)); + if (controller.value == 1.0) { + _values.add(index); + } } - void removeItem(int index) => _controllers.remove(index); + /// Removes an item from the accordion. + void removeItem(int index) { + _controllers.removeAt(index); + _values.remove(index); + } - /// Convenience method for toggling the current [expanded] status. + /// Convenience method for toggling the current expanded status. /// /// This method should typically not be called while the widget tree is being rebuilt. Future toggle(int index) async { - if (_max != null && _controllers.length >= _max) { - print('Cannot expand more than $_max items.'); - return; - } - - if (_controllers.length <= _min) { - print('Cannot collapse more than $_min items.'); - return; + if (_controllers[index].$2.value == 100) { + if (_values.length <= _min) { + return; + } + _values.remove(index); + await collapse(index); + } else { + if (_max != null && _values.length >= _max) { + return; + } + _values.add(index); + await expand(index); } - - return await _controllers[index].$2.value == 100 ? collapse(index) : expand(index); + notifyListeners(); } /// Shows the content in the accordion. @@ -77,52 +92,48 @@ interface class FAccordionController extends ChangeNotifier { await controller.reverse(); notifyListeners(); } - - double percentage(int index) => _controllers[index].$2.value / 100; } -// class RadioAccordionController implements FAccordionController { -// final Duration duration; -// final List<(AnimationController, Animation)> _controllers; -// final int? min; -// final int? max; -// -// RadioAccordionController({ -// this.duration = const Duration(milliseconds: 500), -// }) : super(duration: duration); -// -// @override -// void addItem(int index, AnimationController controller, Animation expand) { -// _controllers[index] = (controller, expand); -// } -// -// @override -// void removeItem(int index) => _controllers.remove(index); -// -// @override -// Future toggle(int index) async { -// final controller = _controllers[index].item; -// controller.isCompleted ? controller.reverse() : controller.forward(); -// } -// -// @override -// Future expand(int index) async { -// final controller = _controllers[index].item; -// controller.forward(); -// } -// -// @override -// Future collapse(int index) async { -// final controller = _controllers[index].item1; -// controller.reverse(); -// } -// } +/// An [FAccordionController] that allows one section to be expanded at a time. +class FRadioAccordionController extends FAccordionController { + /// Creates a [FRadioAccordionController]. + FRadioAccordionController({ + int? value, + super.duration, + super.min, + int super.max = 1, + }) : super(values: value == null ? {} : {value}); + + @override + Future toggle(int index) async { + final toggle = []; + if (!(_values.length <= _min)) { + if (!_values.contains(index) && _values.length >= _max!) { + toggle.add(collapse(_values.first)); + _values.remove(_values.first); + } + } + toggle.add(super.toggle(index)); + await Future.wait(toggle); + } +} +/// A vertically stacked set of interactive headings that each reveal a section of content. class FAccordion extends StatefulWidget { + /// The controller. + /// + /// See: + /// * [FRadioAccordionController] for a single radio like selection. + /// * [FAccordionController] for default multiple selections. final FAccordionController? controller; + + /// The items. final List items; + + /// The style. Defaults to [FThemeData.accordionStyle]. final FAccordionStyle? style; + /// Creates a [FAccordion]. const FAccordion({ required this.items, this.controller, @@ -136,7 +147,10 @@ class FAccordion extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('controller', controller)); + properties + ..add(DiagnosticsProperty('controller', controller)) + ..add(DiagnosticsProperty('style', style)) + ..add(IterableProperty('items', items)); } } @@ -166,6 +180,7 @@ class _FAccordionState extends State { } } +/// An item that represents a header in a [FAccordion]. class FAccordionItem { /// The title. final Widget title; @@ -176,6 +191,7 @@ class FAccordionItem { /// Whether the item is initially expanded. final bool initiallyExpanded; + /// FAccordionItem({required this.title, required this.child, this.initiallyExpanded = false}); } @@ -269,7 +285,7 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { Widget build(BuildContext context) => AnimatedBuilder( animation: widget.controller._controllers[widget.index].$2, builder: (context, _) => Column( - crossAxisAlignment: CrossAxisAlignment.start, //TODO: Should all content in the accordion be left-aligned? + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FTappable( onPress: () => widget.controller.toggle(widget.index), @@ -293,7 +309,9 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { ), ), Transform.rotate( - angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, //TODO: use FAccordionController to get the percentage + //TODO: Should I be getting the percentage value from the controller or from its local state? + angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, + child: widget.style.icon, ), ], @@ -305,9 +323,11 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the // child to layout at a smaller size while the amount of padding remains the same. _Expandable( - percentage: _expand.value / 100, //TODO: use FAccordionController to get the percentage + //TODO: Should I be getting the percentage value from the controller or from its local state? + percentage: _expand.value / 100, child: ClipRect( - clipper: _Clipper(percentage: _expand.value / 100), //TODO: use FAccordionController to get the percentage + //TODO: Should I be getting the percentage value from the controller or from its local state? + clipper: _Clipper(percentage: _expand.value / 100), child: Padding( padding: widget.style.contentPadding, child: DefaultTextStyle(style: widget.style.childTextStyle, child: widget.item.child), diff --git a/forui/test/golden/accordion/hidden.png b/forui/test/golden/accordion/hidden.png index dca728e42f39a4ff07ba0d23352ed843c362e5fd..8bbaf629f5fced8fe64db43f610aed24171dbce3 100644 GIT binary patch delta 962 zcmV;z13mn=AGIHlL4RLKL_t(|obBDui=EXSz~QeZyIEwHZE$DOm0-H7#+{IEgOWj| ziWEabHxn1p#@}hs0WA>=O=Gk)L9w{YI_@lu5Zz4N4)_;{r(;1A3i6Qk9Vx9*E z?wt4BbIP=z}>fc0{{TPokM^C000>Q0<(w;83mKz0o;FP>+0(2 z96$c{H7D)ddG9>+)Khcz?Ah73?~(cFqYr2A-n~CMe`C#q2mdfTcI@~WwhaIP+>W-c zt*y;F@BHgwt&EghQG)zz7qSz0i_@%w}(rYF3pJ(f1ksLpa0Q0hYvqLCr+H4@4kP#bj7tB3XqMj zR;{hA-S9@=dh0K=oCSmY$tcr{0^z#r^Zb3xB%e`qrEI@a5$HZk_kv|HsVCJn_WibL@ZE(QCdZ36Kq+efFQL zj(b)J007*cjvYHXTefVOEnDuMmtTJAy7Q9&dFY`%n?Ke2{gaOK=g-f=!on;pEEL-S z004dluf6ulPo8^2zk+o&+qP|+`QHNx&!0bk)j50i?3w%SyKkO;`uB70+$XcLvN9ih z@Zmi8;6wB7yQgSwb_)Oiz-{8DPe2}j{IR)k;qy6o@Q<^&xc{<;6mD2pSeP?s{yE2w zzdd{R{&sWxz=3DxrI)V%5EB3Z;Fsa9_4W1j@&nh_)@F5eb$0C7;j32w0C0!N0|Wp7 zxO3g~Js$iFn006+9 k!3m?G3I&tk0hku~FDq;dckKDYssI2007*qoM6N<$f-D>gH2?qr delta 962 zcmV;z13mn;AGaTnL4RRML_t(|obBDui`R7?!0}hyS1!=bs4)3^cO_aArG^MWrj@C#CnSPc)-AS z-kA->KW@cvl_U-fNqmRz^?b|o6ojP@DE?xR^mY3h$9NSp)>8Jnj%j_Be z0JtOFyKC33`S#myHvcwaZ`}+ z=+T#MIPd7u7w7o#zg#tbQ}=+ZuCC6~(xF*hU7eYkrKLmj>8JnMe0}QFsq4P~+_`hJ zv~*~emJZD;ue`ds4gdgfXSjU%@|-;Rk2!q!$dArBeE7(mJb7xq`|f}8Ro8ARKsLTw zwYIi)(;I#J?Z3{xeUHtyZQEw&&im)$#m{Hw&fm<+%F0cz1polt9>=k)3K zXKCsAtFA9CJwK;UzdwtM`{$*Xj$U#cnF^75Orv~*}LUc7k4Js>}H{`@C%J7|K`{a z-BN)3R9}4Y#mvmyci(+ioOnF+(C_BRkrxCA007)!ei$Jeudf#$Gf{wi_0?B1GxOks zzrE)5m6es*wQEXR^Hf#%$fXbxxmte`aQ$dg{q}{q=w2*L_bCAR9jW?7!C> z_pA^A0Jt-~{`&FRvSrI`*>dl^{PM9I&QAj5kw^Ay{#5VxPdYAKxG)O~3$w7WP;3JL z0Qd#G_S!2ydG1a93f8r3+qP|H{{KM23l}b2bIzVUd**=$9++pJ{o|ZJ|H-VZtjq@= zd^it3{K&lb-alw>bqfFhz#Zb2Pe7i0@`<^4@$)%&@Xxcjxc`cW6mD2pSeUbC|1~F0 zyfb_E{(f`(z=7xH*s&Ww!~_5U_;t8veSLks{J^!fwOL(VogF)N`05n^0NiEr00961 z?q0Wi4+sDNz)vNUy$LCkya^>CZ2kJ{Z*KP<004lygcA?|03dVE`uh6%?cM|c0C0D3 k0<%F1UImkI0Xi1`2TDr|cZWFdMF0Q*07*qoM6N<$f+@ZY4*&oF diff --git a/forui/test/golden/accordion/shown.png b/forui/test/golden/accordion/shown.png index 06d7c7c0a901452019108a1928cbcf8ad62a1f44..90db5e11dcd18d70ab5d20c6b869260268d85d49 100644 GIT binary patch delta 1060 zcmah|YfPF~82&VFrs6fJ2vfu#b8c;wsi_)-GHbiGYd3d^QbmdtqiI5+Os58|m~Pf} zMX{3U)H>62(=9}dGOG+PV-~(y?YwnTB8uf(yrFnOZu$v3Wc$A#@B5tOyzg_K^Ioc> z4u}nDUOzlI8=YMed?IusNkEEX;_SNusWmg_i45CnO*ajgJ;YfqOy!d&!` zFMIiW2@5SHWCAQ66)c`lEn7-$YHHHxol??ZYaHZEJxR#~P^|vnE^p{{ZqQCN5|IutmK?l?9cKSFCw^#={i; z;#3EgdX7dqJ}oVhJJ=Li%#bW z2H9{G4I4%bXAE!&p1Q>4Itn)R&f)l$E8K$|z|!5{*Ow3&Sy?h!DCfISUj%gqX*fyY zzJU~SR#ukGsR%g2%+EX~iG3p-ssq>T>};mCJ=N|iH!E0TNsIwc8rngx3UhMa;0^6| z^%hNizx(VYoRr`VLBWjdZ2X~}h+~WRE0gQ-G#YJXbro(&?1FLZO@nZ`HKY*@*zW46 zB5QDLxSqgHXc3JUBy`TLm+z>Eb;*WGDb5Rm9Ixlbo|#9JL;4iBz5ApQ51;$UFuLIc z!B{Nzn@=DiR2@*h(!#1!C=~pS7vnu@xYg-&@(=cv$g)bkyD6VArS-jatl>0Kh-#&g zc3Cj!KQ$uNrtG~j#{`dWUn_o2wd)g?Dz;y|{AFF{PHEQPvF?m1we~P9c7CA0RyV-W zeJTQaIMXPKet{tGo01};Je5UQLeiV)+GDZo*Fw8_5g9o&hi6c&7QR`3==sxtp{j_o zKRt$6weG>05D2PXczTzo5*Y7w)MLWV;K5XWPH{OhMr@70pJqJa;CaK=RX*yFvbUFR zo8TaVKrrMm@{-7)sJGg_YkOWO-^0-9bQny_!T_(#6dDFW`s;fT5NFOEPhSJ;kzNg4 zwB(i7rBEHk@0fLM2%=2$c$TkYZ@4mdU2AP_a8hO^Mi5EX68HB*8$|Q3zP7 z5Z&CEzz7!(?$;f z007)Qv#|kV1e4GK@_#l@r_*ub#H%-4v~%bEV{vhDoI7`J96I#GICbisv3Kv@>#kqD z=8+>m8{4;Uzuhwf004KW%_}P_K|*{YHa-b zvDLWPwQJWnefr(8YuB!|3tYPN*-Zlm008*^boug?v2WiG$G(00$CWErZo2-Ko&lLo zr(hUkZ}|Ch=gy6#rK4kM>F9XrrQfby2LJ%L^IX1s zWjyxSp>g%<)qipI>eX@Z;GvsF$SnoP>aA5PD=W7=-0|bTABPS-F}7~qI(F{7e_Xuy z$=JE`J7Y4L-14;m0Dz6Pyu3UP9(;VvX0vhO!UyBRg%8GTHX8>HJU*6}m#_P|b^Qz0 zv12cfhaURg+Wz9=;x*TuKmY#tY%zW8E{F~)-re)pQ^CzHuo zSXd}V0e=7h*5SG5elh0e=EmvMr@j^+tLEqD$C)$lj)Mmejpv_VS__g@Nr0^S_~XA` z^SF0~006+9;rQ|2jZK^G89R1-`?_oHyYIg7-g~FVrcL+!$GPMdtVbW+y|%xwu&}nR zKFq#l%a&pc003Z}=I7^c7#}xVx9$Mhx^?Rq<9}Z-B>d>3kFL39_wL=}fd?KK&ph+f zasK@KV=|eHx8HteJo3n+Zu=(ix)o`M~?h_Jo)60zV?#B zRa>@f8E?Jy=W*i1t7GrpeQU=LAAWYc_~LKYI|2XzaEF+i&1SRm3s+WF#&kLz+qZ9b z>lqaQ0NiEr00961?q115=l}o!V+arc03alj;R#6?n>Ts@0KNf#|Ncf#aC>~~!bVR3 zz-{Iiqmc^%4l_5K&1M@t1ONbVckm07@c~F2X7iU{{%xaY0002)5_7ZJY_`!u0001Y k2e)7WvjG9_1Q(fq0vMhr*U{?MJOBUy07*qoM6N<$g3}riQ2+n{ diff --git a/forui/test/src/widgets/accordion/accordion_golden_test.dart b/forui/test/src/widgets/accordion/accordion_golden_test.dart index 2291fd198..ddcb72203 100644 --- a/forui/test/src/widgets/accordion/accordion_golden_test.dart +++ b/forui/test/src/widgets/accordion/accordion_golden_test.dart @@ -15,17 +15,22 @@ void main() { MaterialApp( home: TestScaffold( data: FThemes.zinc.light, - child: const Column( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - title: Text('Title'), - child: ColoredBox( - color: Colors.yellow, - child: SizedBox.square( - dimension: 50, + items: [ + FAccordionItem( + title: const Text('Title'), + initiallyExpanded: true, + child: const ColoredBox( + color: Colors.yellow, + child: SizedBox.square( + dimension: 50, + ), + ), ), - ), + ], ), ], ), @@ -41,17 +46,22 @@ void main() { MaterialApp( home: TestScaffold( data: FThemes.zinc.light, - child: const Column( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - title: Text('Title'), - child: ColoredBox( - color: Colors.yellow, - child: SizedBox.square( - dimension: 50, + items: [ + FAccordionItem( + title: const Text('Title'), + initiallyExpanded: true, + child: const ColoredBox( + color: Colors.yellow, + child: SizedBox.square( + dimension: 50, + ), + ), ), - ), + ], ), ], ), diff --git a/forui/test/src/widgets/accordion/accordion_test.dart b/forui/test/src/widgets/accordion/accordion_test.dart index cd086f427..a7a8acefd 100644 --- a/forui/test/src/widgets/accordion/accordion_test.dart +++ b/forui/test/src/widgets/accordion/accordion_test.dart @@ -18,11 +18,16 @@ void main() { home: TestScaffold( data: FThemes.zinc.light, child: FAccordion( - title: const Text('Title'), - child: FButton( - onPress: () => taps++, - label: const Text('button'), - ), + items: [ + FAccordionItem( + title: const Text('Title'), + initiallyExpanded: true, + child: FButton( + onPress: () => taps++, + label: const Text('button'), + ), + ), + ], ), ), ), diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart index 4f2699c5d..83347bb86 100644 --- a/samples/lib/widgets/accordion.dart +++ b/samples/lib/widgets/accordion.dart @@ -1,33 +1,49 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; +import 'package:forui/forui.dart'; import 'package:forui_samples/sample_scaffold.dart'; +final controllers = { + 'default': FAccordionController(), + 'radio': FRadioAccordionController(), +}; + @RoutePage() class AccordionPage extends SampleScaffold { + final FAccordionController controller; + AccordionPage({ @queryParam super.theme, - }); + @queryParam String controller = 'default', + }) : controller = controllers[controller] ?? FRadioAccordionController(); @override - Widget child(BuildContext context) => const Column( + Widget child(BuildContext context) => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - title: Text('Is it Styled?'), - child: Text( - "Yes. It comes with default styles that matches the other components' aesthetics", - textAlign: TextAlign.left, - ), - ), - FAccordion( - title: Text('Is it Animated?'), - initiallyExpanded: false, - child: Text( - 'Yes. It is animated by default, but you can disable it if you prefer', - textAlign: TextAlign.left, - ), + controller: controller, + items: [ + FAccordionItem( + title: const Text('Is it accessible?'), + child: const Text('Yes. It adheres to the WAI-ARIA design pattern.'), + ), + FAccordionItem( + title: const Text('Is it Styled?'), + initiallyExpanded: true, + child: const Text( + "Yes. It comes with default styles that matches the other components' aesthetics", + ), + ), + FAccordionItem( + title: const Text('Is it Animated?'), + child: const Text( + 'Yes. It is animated by default, but you can disable it if you prefer', + ), + ), + ], ), ], ); From 2322594368cab1703363eb730b63bde03bd7df45 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Thu, 19 Sep 2024 17:57:12 +0800 Subject: [PATCH 27/57] Update shown.png --- forui/test/golden/accordion/shown.png | Bin 4090 -> 4082 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/forui/test/golden/accordion/shown.png b/forui/test/golden/accordion/shown.png index 90db5e11dcd18d70ab5d20c6b869260268d85d49..5db898e278f2866f9d7793b8e026b457f74db5ec 100644 GIT binary patch delta 1014 zcmVN%b zuC9*PUi$%ocgLPRd)6;- z`SNGC3>W|a;Q!N=D_6&X13w%G4jdd;uU@_7`rCR2WICOW<>g~zI-QO&##mlHHa`6D z&+GfAPMx~x=g*%%KbDt|jpgNI{j66Tcgai;s`(+qaM1yT3gyUHW9~-u<01nM`i`S^xmRowc&E zG7cYpY|Lh}aq;2@JAhxoyG7cU*G{zX?+_|%3 zetv$;&(Du@=gy8X#yEKJ&hZo0l&n>S9$(kfU)_nZ&U#@%HyFvf};O=nZ#BaxzE%%IF zyS{b9HTT_j-+1r6Gh@q^d;aZQatqcYkL+FFUszaJ-_{;x-?nXAF$MqtutD?l^EZu; zTdmu0fNbBseT;v9zL4;vk3PEYn!S7Xj{EPwe?0y4PsfD|?~lo3GTwgso$>I)kBrw} z|HDQ{0002)5*xk+>&Yj7JT6`OWE?&E^YO$JKl<8B3fFAgwr#xi)}O}7ldq2b`wy%i zKXT-m@#2fW-slJb0Ki>hZZ@0E$}e19T^-ZubnM)@)2&wv006MbLA6?P%V>nH>)RfukGOkjiy3SAVk>A1b!bcRav zr(yI>(>xCxIGpp{bAQ{Hd(J&`v)ODm00002n{M?0006+9L$mP;>jaa)0cn3`^K?2L z$B(~y-9t>cgEhmd#|~E?V5)V|6*+4zWrMm8vp>f9c^A+ zT^+Bz_J`{(I&|piF~%6%wrv}G_UswkwryKKcjCl}aq;43YY&IJQ4m>(mR#vY0xefgb*3qLcj|U$3!TSEv($ZDeojdpb zc<#AhkCP|g885u>{8)clS{iS>@u#t8&z|whE5|l60ssIITUl8d`}ZFhV~la;%;_;d zKR@Q@=f{~dr^gs$?B9Rjnh}!x4ezI)emce&W57cfDL%&nO}{$xw&!b z)XA^K$C~;1`EmO6yW`-&#qsR3%j-e1CJB%=AAkJUs~-2R5C8zUJsdmsyRm80U1P_N z?_P7wJ@?!*-h1!V*tF@c|2UW2g7xsjyVv&@78cgGwTIcaY}rzb0RRAO(ER-Tb>rhk z>oy!9Teof<<6nO-B>d>3kFL6A_wL=}zWeSQPd)YXaqisvV=|eHx8HteJoM1R+0D#-XhHt@o;)$P*3l}~ahY$a9JpTAkzV?#BHCwi98E?Jy=W+b_t7Grpee1^$ z9eR4a_~LIiIsyOyaGRK$&1SRm3s+ZH$8lF(C0Ni2n00961?p%`@304$5 z{{F*TJ;OKSJLhlp1ONcQ4fq9b002ovPDHLkV1g^$Bqjg= From 2a4f6326a87d5c8838d68c60889b85ccb85564ae Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Thu, 19 Sep 2024 19:42:08 +0800 Subject: [PATCH 28/57] Apply suggestions from code review Co-authored-by: Matthias Ngeo --- docs/pages/docs/accordion.mdx | 4 ++-- forui/lib/src/theme/theme_data.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/pages/docs/accordion.mdx b/docs/pages/docs/accordion.mdx index d4b3e6d76..33a1e6890 100644 --- a/docs/pages/docs/accordion.mdx +++ b/docs/pages/docs/accordion.mdx @@ -12,7 +12,7 @@ A vertically stacked set of interactive headings that each reveal a section of c - + ```dart @@ -64,7 +64,7 @@ FAccordion( ## Examples -### With FRadioAccordionController +### With Radio Behaviour diff --git a/forui/lib/src/theme/theme_data.dart b/forui/lib/src/theme/theme_data.dart index 76f075aa0..b23712efa 100644 --- a/forui/lib/src/theme/theme_data.dart +++ b/forui/lib/src/theme/theme_data.dart @@ -25,7 +25,7 @@ final class FThemeData with Diagnosticable { /// The style. It is used to configure the miscellaneous properties, such as border radii, of Forui widgets. final FStyle style; - /// The accordion styles. + /// The accordion style. final FAccordionStyle accordionStyle; /// The alert styles. From 78b5546211ec99be32367f6f13866ac6709e44ef Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Thu, 19 Sep 2024 19:44:37 +0800 Subject: [PATCH 29/57] Apply suggestions from code review Co-authored-by: Matthias Ngeo --- forui/lib/src/widgets/accordion.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 9ecdf21f5..5ab70f0b0 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -11,11 +11,11 @@ import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; -/// A controller that stores the expanded state of an [_Item]. +/// A controller that controls which sections are shown and hidden. interface class FAccordionController extends ChangeNotifier { /// The duration of the expansion and collapse animations. final Duration duration; - final List<(AnimationController, Animation)> _controllers; + final List<({AnimationController controller, Animation animation})> _controllers; final Set _values; final int _min; final int? _max; @@ -31,7 +31,7 @@ interface class FAccordionController extends ChangeNotifier { FAccordionController({ int min = 0, int? max, - Set? values, + Set values = {}, this.duration = const Duration(milliseconds: 500), }) : _min = min, _max = max, @@ -42,7 +42,7 @@ interface class FAccordionController extends ChangeNotifier { assert(max == null || min <= max, 'The max value must be greater than or equal to the min value.'); /// Adds an item to the accordion. - void addItem(int index, AnimationController controller, Animation expand) { + void addItem(int index, AnimationController controller, Animation animation) { _controllers.add((controller, expand)); if (controller.value == 1.0) { _values.add(index); From fe1d45be5571a279f66123caef7ac98157239796 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Thu, 19 Sep 2024 20:18:40 +0800 Subject: [PATCH 30/57] Fixed some issues --- forui/lib/src/widgets/accordion.dart | 80 +++++++++++++++------------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart index 5ab70f0b0..92b2f4457 100644 --- a/forui/lib/src/widgets/accordion.dart +++ b/forui/lib/src/widgets/accordion.dart @@ -12,11 +12,11 @@ import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; /// A controller that controls which sections are shown and hidden. -interface class FAccordionController extends ChangeNotifier { +base class FAccordionController extends ChangeNotifier { /// The duration of the expansion and collapse animations. - final Duration duration; + final Duration animationDuration; final List<({AnimationController controller, Animation animation})> _controllers; - final Set _values; + final Set _expanded; final int _min; final int? _max; @@ -31,45 +31,45 @@ interface class FAccordionController extends ChangeNotifier { FAccordionController({ int min = 0, int? max, - Set values = {}, - this.duration = const Duration(milliseconds: 500), + this.animationDuration = const Duration(milliseconds: 500), }) : _min = min, _max = max, _controllers = [], - _values = values ?? {}, + _expanded = {}, assert(min >= 0, 'The min value must be greater than or equal to 0.'), assert(max == null || max >= 0, 'The max value must be greater than or equal to 0.'), assert(max == null || min <= max, 'The max value must be greater than or equal to the min value.'); /// Adds an item to the accordion. - void addItem(int index, AnimationController controller, Animation animation) { - _controllers.add((controller, expand)); - if (controller.value == 1.0) { - _values.add(index); + // ignore: avoid_positional_boolean_parameters + void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { + _controllers.add((controller: controller, animation: animation)); + if (initiallyExpanded) { + _expanded.add(index); } } /// Removes an item from the accordion. void removeItem(int index) { _controllers.removeAt(index); - _values.remove(index); + _expanded.remove(index); } /// Convenience method for toggling the current expanded status. /// /// This method should typically not be called while the widget tree is being rebuilt. Future toggle(int index) async { - if (_controllers[index].$2.value == 100) { - if (_values.length <= _min) { + if (_controllers[index].controller.value == 1) { + if (_expanded.length <= _min) { return; } - _values.remove(index); + _expanded.remove(index); await collapse(index); } else { - if (_max != null && _values.length >= _max) { + if (_max != null && _expanded.length >= _max) { return; } - _values.add(index); + _expanded.add(index); await expand(index); } notifyListeners(); @@ -79,7 +79,7 @@ interface class FAccordionController extends ChangeNotifier { /// /// This method should typically not be called while the widget tree is being rebuilt. Future expand(int index) async { - final controller = _controllers[index].$1; + final controller = _controllers[index].controller; await controller.forward(); notifyListeners(); } @@ -88,29 +88,37 @@ interface class FAccordionController extends ChangeNotifier { /// /// This method should typically not be called while the widget tree is being rebuilt. Future collapse(int index) async { - final controller = _controllers[index].$1; + final controller = _controllers[index].controller; await controller.reverse(); notifyListeners(); } } /// An [FAccordionController] that allows one section to be expanded at a time. -class FRadioAccordionController extends FAccordionController { +final class FRadioAccordionController extends FAccordionController { /// Creates a [FRadioAccordionController]. FRadioAccordionController({ - int? value, - super.duration, + super.animationDuration, super.min, int super.max = 1, - }) : super(values: value == null ? {} : {value}); + }); + + @override + void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { + if (_expanded.length > _max!) { + super.addItem(index, controller, animation, false); + } else { + super.addItem(index, controller, animation, initiallyExpanded); + } + } @override Future toggle(int index) async { final toggle = []; - if (!(_values.length <= _min)) { - if (!_values.contains(index) && _values.length >= _max!) { - toggle.add(collapse(_values.first)); - _values.remove(_values.first); + if (_expanded.length > _min) { + if (!_expanded.contains(index) && _expanded.length >= _max!) { + toggle.add(collapse(_expanded.first)); + _expanded.remove(_expanded.first); } } toggle.add(super.toggle(index)); @@ -160,7 +168,7 @@ class _FAccordionState extends State { @override void initState() { super.initState(); - _controller = widget.controller ?? FAccordionController(); + _controller = widget.controller ?? FRadioAccordionController(min: 1, max: 2); } @override @@ -168,11 +176,11 @@ class _FAccordionState extends State { final style = widget.style ?? context.theme.accordionStyle; return Column( children: [ - for (var i = 0; i < widget.items.length; i++) + for (final (index, widget) in widget.items.indexed) _Item( style: style, - index: i, - item: widget.items[i], + index: index, + item: widget, controller: _controller, ), ], @@ -191,7 +199,7 @@ class FAccordionItem { /// Whether the item is initially expanded. final bool initiallyExpanded; - /// + /// Creates an [FAccordionItem]. FAccordionItem({required this.title, required this.child, this.initiallyExpanded = false}); } @@ -241,7 +249,7 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { void initState() { super.initState(); _controller = AnimationController( - duration: widget.controller.duration, + duration: widget.controller.animationDuration, value: widget.item.initiallyExpanded ? 1.0 : 0.0, vsync: this, ); @@ -254,7 +262,7 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { parent: _controller, ), ); - widget.controller.addItem(widget.index, _controller, _expand); + widget.controller.addItem(widget.index, _controller, _expand, widget.item.initiallyExpanded); } @override @@ -262,7 +270,7 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { super.didUpdateWidget(old); if (widget.controller != old.controller) { _controller = AnimationController( - duration: widget.controller.duration, + duration: widget.controller.animationDuration, value: widget.item.initiallyExpanded ? 1.0 : 0.0, vsync: this, ); @@ -277,13 +285,13 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { ); old.controller.removeItem(old.index); - widget.controller.addItem(widget.index, _controller, _expand); + widget.controller.addItem(widget.index, _controller, _expand, widget.item.initiallyExpanded); } } @override Widget build(BuildContext context) => AnimatedBuilder( - animation: widget.controller._controllers[widget.index].$2, + animation: widget.controller._controllers[widget.index].animation, builder: (context, _) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ From 7fab65dd5e28056d633b4141805bbafa31c4e1c2 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Fri, 20 Sep 2024 01:22:31 +0800 Subject: [PATCH 31/57] Should we implement Inherited widget for FAcoordionItem --- forui/lib/src/widgets/accordion.dart | 533 ------------------ .../lib/src/widgets/accordion/accordion.dart | 173 ++++++ .../accordion/accordion_controller.dart | 122 ++++ .../src/widgets/accordion/accordion_item.dart | 253 +++++++++ forui/lib/widgets/accordion.dart | 6 +- 5 files changed, 552 insertions(+), 535 deletions(-) delete mode 100644 forui/lib/src/widgets/accordion.dart create mode 100644 forui/lib/src/widgets/accordion/accordion.dart create mode 100644 forui/lib/src/widgets/accordion/accordion_controller.dart create mode 100644 forui/lib/src/widgets/accordion/accordion_item.dart diff --git a/forui/lib/src/widgets/accordion.dart b/forui/lib/src/widgets/accordion.dart deleted file mode 100644 index 92b2f4457..000000000 --- a/forui/lib/src/widgets/accordion.dart +++ /dev/null @@ -1,533 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:meta/meta.dart'; - -import 'package:forui/forui.dart'; -import 'package:forui/src/foundation/tappable.dart'; -import 'package:forui/src/foundation/util.dart'; - -/// A controller that controls which sections are shown and hidden. -base class FAccordionController extends ChangeNotifier { - /// The duration of the expansion and collapse animations. - final Duration animationDuration; - final List<({AnimationController controller, Animation animation})> _controllers; - final Set _expanded; - final int _min; - final int? _max; - - /// Creates a [FAccordionController]. - /// - /// The [min] and [max] values are the minimum and maximum number of selections allowed. Defaults to no minimum or maximum. - /// - /// # Contract: - /// * Throws [AssertionError] if [min] < 0. - /// * Throws [AssertionError] if [max] < 0. - /// * Throws [AssertionError] if [min] > [max]. - FAccordionController({ - int min = 0, - int? max, - this.animationDuration = const Duration(milliseconds: 500), - }) : _min = min, - _max = max, - _controllers = [], - _expanded = {}, - assert(min >= 0, 'The min value must be greater than or equal to 0.'), - assert(max == null || max >= 0, 'The max value must be greater than or equal to 0.'), - assert(max == null || min <= max, 'The max value must be greater than or equal to the min value.'); - - /// Adds an item to the accordion. - // ignore: avoid_positional_boolean_parameters - void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { - _controllers.add((controller: controller, animation: animation)); - if (initiallyExpanded) { - _expanded.add(index); - } - } - - /// Removes an item from the accordion. - void removeItem(int index) { - _controllers.removeAt(index); - _expanded.remove(index); - } - - /// Convenience method for toggling the current expanded status. - /// - /// This method should typically not be called while the widget tree is being rebuilt. - Future toggle(int index) async { - if (_controllers[index].controller.value == 1) { - if (_expanded.length <= _min) { - return; - } - _expanded.remove(index); - await collapse(index); - } else { - if (_max != null && _expanded.length >= _max) { - return; - } - _expanded.add(index); - await expand(index); - } - notifyListeners(); - } - - /// Shows the content in the accordion. - /// - /// This method should typically not be called while the widget tree is being rebuilt. - Future expand(int index) async { - final controller = _controllers[index].controller; - await controller.forward(); - notifyListeners(); - } - - /// Hides the content in the accordion. - /// - /// This method should typically not be called while the widget tree is being rebuilt. - Future collapse(int index) async { - final controller = _controllers[index].controller; - await controller.reverse(); - notifyListeners(); - } -} - -/// An [FAccordionController] that allows one section to be expanded at a time. -final class FRadioAccordionController extends FAccordionController { - /// Creates a [FRadioAccordionController]. - FRadioAccordionController({ - super.animationDuration, - super.min, - int super.max = 1, - }); - - @override - void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { - if (_expanded.length > _max!) { - super.addItem(index, controller, animation, false); - } else { - super.addItem(index, controller, animation, initiallyExpanded); - } - } - - @override - Future toggle(int index) async { - final toggle = []; - if (_expanded.length > _min) { - if (!_expanded.contains(index) && _expanded.length >= _max!) { - toggle.add(collapse(_expanded.first)); - _expanded.remove(_expanded.first); - } - } - toggle.add(super.toggle(index)); - await Future.wait(toggle); - } -} - -/// A vertically stacked set of interactive headings that each reveal a section of content. -class FAccordion extends StatefulWidget { - /// The controller. - /// - /// See: - /// * [FRadioAccordionController] for a single radio like selection. - /// * [FAccordionController] for default multiple selections. - final FAccordionController? controller; - - /// The items. - final List items; - - /// The style. Defaults to [FThemeData.accordionStyle]. - final FAccordionStyle? style; - - /// Creates a [FAccordion]. - const FAccordion({ - required this.items, - this.controller, - this.style, - super.key, - }); - - @override - State createState() => _FAccordionState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('controller', controller)) - ..add(DiagnosticsProperty('style', style)) - ..add(IterableProperty('items', items)); - } -} - -class _FAccordionState extends State { - late final FAccordionController _controller; - - @override - void initState() { - super.initState(); - _controller = widget.controller ?? FRadioAccordionController(min: 1, max: 2); - } - - @override - Widget build(BuildContext context) { - final style = widget.style ?? context.theme.accordionStyle; - return Column( - children: [ - for (final (index, widget) in widget.items.indexed) - _Item( - style: style, - index: index, - item: widget, - controller: _controller, - ), - ], - ); - } -} - -/// An item that represents a header in a [FAccordion]. -class FAccordionItem { - /// The title. - final Widget title; - - /// The child. - final Widget child; - - /// Whether the item is initially expanded. - final bool initiallyExpanded; - - /// Creates an [FAccordionItem]. - FAccordionItem({required this.title, required this.child, this.initiallyExpanded = false}); -} - -/// An interactive heading that reveals a section of content. -/// -/// See: -/// * https://forui.dev/docs/accordion for working examples. -class _Item extends StatefulWidget { - /// The accordion's style. Defaults to [FThemeData.accordionStyle]. - final FAccordionStyle style; - - /// The accordion's controller. - final FAccordionController controller; - - final FAccordionItem item; - - final int index; - - /// Creates an [_Item]. - const _Item({ - required this.style, - required this.index, - required this.item, - required this.controller, - }); - - @override - State<_Item> createState() => _ItemState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('style', style)) - ..add(DiagnosticsProperty('controller', controller)) - ..add(DiagnosticsProperty('item', item)) - ..add(IntProperty('index', index)); - } -} - -class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _expand; - bool _hovered = false; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: widget.controller.animationDuration, - value: widget.item.initiallyExpanded ? 1.0 : 0.0, - vsync: this, - ); - _expand = Tween( - begin: 0, - end: 100, - ).animate( - CurvedAnimation( - curve: Curves.ease, - parent: _controller, - ), - ); - widget.controller.addItem(widget.index, _controller, _expand, widget.item.initiallyExpanded); - } - - @override - void didUpdateWidget(covariant _Item old) { - super.didUpdateWidget(old); - if (widget.controller != old.controller) { - _controller = AnimationController( - duration: widget.controller.animationDuration, - value: widget.item.initiallyExpanded ? 1.0 : 0.0, - vsync: this, - ); - _expand = Tween( - begin: 0, - end: 100, - ).animate( - CurvedAnimation( - curve: Curves.ease, - parent: _controller, - ), - ); - - old.controller.removeItem(old.index); - widget.controller.addItem(widget.index, _controller, _expand, widget.item.initiallyExpanded); - } - } - - @override - Widget build(BuildContext context) => AnimatedBuilder( - animation: widget.controller._controllers[widget.index].animation, - builder: (context, _) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FTappable( - onPress: () => widget.controller.toggle(widget.index), - child: MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: Container( - padding: widget.style.titlePadding, - child: Row( - children: [ - Expanded( - child: merge( - // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. - textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, - ), - style: widget.style.titleTextStyle - .copyWith(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), - child: widget.item.title, - ), - ), - Transform.rotate( - //TODO: Should I be getting the percentage value from the controller or from its local state? - angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, - - child: widget.style.icon, - ), - ], - ), - ), - ), - ), - // We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by - // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the - // child to layout at a smaller size while the amount of padding remains the same. - _Expandable( - //TODO: Should I be getting the percentage value from the controller or from its local state? - percentage: _expand.value / 100, - child: ClipRect( - //TODO: Should I be getting the percentage value from the controller or from its local state? - clipper: _Clipper(percentage: _expand.value / 100), - child: Padding( - padding: widget.style.contentPadding, - child: DefaultTextStyle(style: widget.style.childTextStyle, child: widget.item.child), - ), - ), - ), - FDivider( - style: context.theme.dividerStyles.horizontal - .copyWith(padding: EdgeInsets.zero, color: widget.style.dividerColor), - ), - ], - ), - ); -} - -class _Expandable extends SingleChildRenderObjectWidget { - final double _percentage; - - const _Expandable({ - required Widget child, - required double percentage, - }) : _percentage = percentage, - super(child: child); - - @override - RenderObject createRenderObject(BuildContext context) => _ExpandableBox(percentage: _percentage); - - @override - void updateRenderObject(BuildContext context, _ExpandableBox renderObject) { - renderObject.percentage = _percentage; - } -} - -class _ExpandableBox extends RenderBox with RenderObjectWithChildMixin { - double _percentage; - - _ExpandableBox({ - required double percentage, - }) : _percentage = percentage; - - @override - void performLayout() { - if (child case final child?) { - child.layout(constraints.normalize(), parentUsesSize: true); - size = Size(child.size.width, child.size.height * _percentage); - } else { - size = constraints.smallest; - } - } - - @override - void paint(PaintingContext context, Offset offset) { - if (child case final child?) { - context.paintChild(child, offset); - } - } - - @override - bool hitTest(BoxHitTestResult result, {required Offset position}) { - if (size.contains(position) && child!.hitTest(result, position: position)) { - result.add(BoxHitTestEntry(this, position)); - return true; - } - - return false; - } - - double get percentage => _percentage; - - set percentage(double value) { - if (_percentage == value) { - return; - } - - _percentage = value; - markNeedsLayout(); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DoubleProperty('percentage', percentage)); - } -} - -class _Clipper extends CustomClipper { - final double percentage; - - _Clipper({required this.percentage}); - - @override - Rect getClip(Size size) => Offset.zero & Size(size.width, size.height * percentage); - - @override - bool shouldReclip(covariant _Clipper oldClipper) => oldClipper.percentage != percentage; -} - -/// The [_Item] styles. -final class FAccordionStyle with Diagnosticable { - /// The title's text style. - final TextStyle titleTextStyle; - - /// The child's default text style. - final TextStyle childTextStyle; - - /// The padding of the title. - final EdgeInsets titlePadding; - - /// The padding of the content. - final EdgeInsets contentPadding; - - /// The icon. - final SvgPicture icon; - - /// The divider's color. - final Color dividerColor; - - /// Creates a [FAccordionStyle]. - FAccordionStyle({ - required this.titleTextStyle, - required this.childTextStyle, - required this.titlePadding, - required this.contentPadding, - required this.icon, - required this.dividerColor, - }); - - /// Creates a [FDividerStyles] that inherits its properties from [colorScheme]. - FAccordionStyle.inherit({required FColorScheme colorScheme, required FTypography typography}) - : titleTextStyle = typography.base.copyWith( - fontWeight: FontWeight.w500, - color: colorScheme.foreground, - ), - childTextStyle = typography.sm.copyWith( - color: colorScheme.foreground, - ), - titlePadding = const EdgeInsets.symmetric(vertical: 15), - contentPadding = const EdgeInsets.only(bottom: 15), - icon = FAssets.icons.chevronRight( - height: 20, - colorFilter: ColorFilter.mode(colorScheme.primary, BlendMode.srcIn), - ), - dividerColor = colorScheme.border; - - /// Returns a copy of this [FAccordionStyle] with the given properties replaced. - @useResult - FAccordionStyle copyWith({ - TextStyle? titleTextStyle, - TextStyle? childTextStyle, - EdgeInsets? titlePadding, - EdgeInsets? contentPadding, - SvgPicture? icon, - Color? dividerColor, - }) => - FAccordionStyle( - titleTextStyle: titleTextStyle ?? this.titleTextStyle, - childTextStyle: childTextStyle ?? this.childTextStyle, - titlePadding: titlePadding ?? this.titlePadding, - contentPadding: contentPadding ?? this.contentPadding, - icon: icon ?? this.icon, - dividerColor: dividerColor ?? this.dividerColor, - ); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('title', titleTextStyle)) - ..add(DiagnosticsProperty('childTextStyle', childTextStyle)) - ..add(DiagnosticsProperty('padding', titlePadding)) - ..add(DiagnosticsProperty('contentPadding', contentPadding)) - ..add(ColorProperty('dividerColor', dividerColor)); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FAccordionStyle && - runtimeType == other.runtimeType && - titleTextStyle == other.titleTextStyle && - childTextStyle == other.childTextStyle && - titlePadding == other.titlePadding && - contentPadding == other.contentPadding && - icon == other.icon && - dividerColor == other.dividerColor; - - @override - int get hashCode => - titleTextStyle.hashCode ^ - childTextStyle.hashCode ^ - titlePadding.hashCode ^ - contentPadding.hashCode ^ - icon.hashCode ^ - dividerColor.hashCode; -} diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart new file mode 100644 index 000000000..fc4e5f6a3 --- /dev/null +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -0,0 +1,173 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/svg.dart'; + +import 'package:forui/forui.dart'; +import 'package:meta/meta.dart'; + +/// A vertically stacked set of interactive headings that each reveal a section of content. +/// +/// Typically used to group multiple [FAccordionItem]s. +/// +/// See: +/// * https://forui.dev/docs/FAccordion for working examples. +/// * [FAccordionStyle] for customizing a select group's appearance. +class FAccordion extends StatefulWidget { + /// The controller. + /// + /// See: + /// * [FRadioAccordionController] for a single radio like selection. + /// * [FAccordionController] for default multiple selections. + final FAccordionController? controller; + + /// The items. + final List items; + + /// The style. Defaults to [FThemeData.accordionStyle]. + final FAccordionStyle? style; + + /// Creates a [FAccordion]. + const FAccordion({ + required this.items, + this.controller, + this.style, + super.key, + }); + + @override + State createState() => _FAccordionState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('controller', controller)) + ..add(DiagnosticsProperty('style', style)) + ..add(IterableProperty('items', items)); + } +} + +class _FAccordionState extends State { + late final FAccordionController _controller; + + @override + void initState() { + super.initState(); + _controller = widget.controller ?? FRadioAccordionController(); + } + + @override + Widget build(BuildContext context) { + final style = widget.style ?? context.theme.accordionStyle; + return Column( + children: [ + for (final (index, widget) in widget.items.indexed) + _Item( + style: style, + index: index, + item: widget, + controller: _controller, + ), + ], + ); + } +} + +/// The [FAccordion] style. +final class FAccordionStyle with Diagnosticable { + /// The title's text style. + final TextStyle titleTextStyle; + + /// The child's default text style. + final TextStyle childTextStyle; + + /// The padding of the title. + final EdgeInsets titlePadding; + + /// The padding of the content. + final EdgeInsets contentPadding; + + /// The icon. + final SvgPicture icon; + + /// The divider's color. + final Color dividerColor; + + /// Creates a [FAccordionStyle]. + FAccordionStyle({ + required this.titleTextStyle, + required this.childTextStyle, + required this.titlePadding, + required this.contentPadding, + required this.icon, + required this.dividerColor, + }); + + /// Creates a [FDividerStyles] that inherits its properties from [colorScheme]. + FAccordionStyle.inherit({required FColorScheme colorScheme, required FTypography typography}) + : titleTextStyle = typography.base.copyWith( + fontWeight: FontWeight.w500, + color: colorScheme.foreground, + ), + childTextStyle = typography.sm.copyWith( + color: colorScheme.foreground, + ), + titlePadding = const EdgeInsets.symmetric(vertical: 15), + contentPadding = const EdgeInsets.only(bottom: 15), + icon = FAssets.icons.chevronRight( + height: 20, + colorFilter: ColorFilter.mode(colorScheme.primary, BlendMode.srcIn), + ), + dividerColor = colorScheme.border; + + /// Returns a copy of this [FAccordionStyle] with the given properties replaced. + @useResult + FAccordionStyle copyWith({ + TextStyle? titleTextStyle, + TextStyle? childTextStyle, + EdgeInsets? titlePadding, + EdgeInsets? contentPadding, + SvgPicture? icon, + Color? dividerColor, + }) => + FAccordionStyle( + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + childTextStyle: childTextStyle ?? this.childTextStyle, + titlePadding: titlePadding ?? this.titlePadding, + contentPadding: contentPadding ?? this.contentPadding, + icon: icon ?? this.icon, + dividerColor: dividerColor ?? this.dividerColor, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('title', titleTextStyle)) + ..add(DiagnosticsProperty('childTextStyle', childTextStyle)) + ..add(DiagnosticsProperty('padding', titlePadding)) + ..add(DiagnosticsProperty('contentPadding', contentPadding)) + ..add(ColorProperty('dividerColor', dividerColor)); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FAccordionStyle && + runtimeType == other.runtimeType && + titleTextStyle == other.titleTextStyle && + childTextStyle == other.childTextStyle && + titlePadding == other.titlePadding && + contentPadding == other.contentPadding && + icon == other.icon && + dividerColor == other.dividerColor; + + @override + int get hashCode => + titleTextStyle.hashCode ^ + childTextStyle.hashCode ^ + titlePadding.hashCode ^ + contentPadding.hashCode ^ + icon.hashCode ^ + dividerColor.hashCode; +} diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart new file mode 100644 index 000000000..da665464e --- /dev/null +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -0,0 +1,122 @@ +import 'package:flutter/widgets.dart'; + +/// A controller that controls which sections are shown and hidden. +base class FAccordionController extends ChangeNotifier { + /// The duration of the expansion and collapse animations. + final Duration animationDuration; + /// A list of controllers for each of the headers in the accordion. + final List<({AnimationController controller, Animation animation})> controllers; + final Set _expanded; + final int _min; + final int? _max; + + /// Creates a [FAccordionController]. + /// + /// The [min] and [max] values are the minimum and maximum number of selections allowed. Defaults to no minimum or maximum. + /// + /// # Contract: + /// * Throws [AssertionError] if [min] < 0. + /// * Throws [AssertionError] if [max] < 0. + /// * Throws [AssertionError] if [min] > [max]. + FAccordionController({ + int min = 0, + int? max, + this.animationDuration = const Duration(milliseconds: 500), + }) : _min = min, + _max = max, + controllers = [], + _expanded = {}, + assert(min >= 0, 'The min value must be greater than or equal to 0.'), + assert(max == null || max >= 0, 'The max value must be greater than or equal to 0.'), + assert(max == null || min <= max, 'The max value must be greater than or equal to the min value.'); + + /// Adds an item to the accordion. + // ignore: avoid_positional_boolean_parameters + void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { + controllers.add((controller: controller, animation: animation)); + if (initiallyExpanded) { + _expanded.add(index); + } + } + + /// Removes an item from the accordion. + void removeItem(int index) { + if (controllers.length <= index) { + controllers.removeAt(index); + } + _expanded.remove(index); + } + + /// Convenience method for toggling the current expanded status. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future toggle(int index) async { + if (controllers[index].controller.value == 1) { + if (_expanded.length <= _min) { + return; + } + + _expanded.remove(index); + await collapse(index); + } else { + if (_max != null && _expanded.length >= _max) { + return; + } + + _expanded.add(index); + await expand(index); + } + + notifyListeners(); + } + + /// Shows the content in the accordion. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future expand(int index) async { + final controller = controllers[index].controller; + await controller.forward(); + notifyListeners(); + } + + /// Hides the content in the accordion. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future collapse(int index) async { + final controller = controllers[index].controller; + await controller.reverse(); + notifyListeners(); + } +} + +/// An [FAccordionController] that allows one section to be expanded at a time. +final class FRadioAccordionController extends FAccordionController { + /// Creates a [FRadioAccordionController]. + FRadioAccordionController({ + super.animationDuration, + super.min, + int super.max = 1, + }); + + @override + void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { + if (_expanded.length > _max!) { + super.addItem(index, controller, animation, false); + } else { + super.addItem(index, controller, animation, initiallyExpanded); + } + } + + @override + Future toggle(int index) async { + final toggle = []; + if (_expanded.length > _min) { + if (!_expanded.contains(index) && _expanded.length >= _max!) { + toggle.add(collapse(_expanded.first)); + _expanded.remove(_expanded.first); + } + } + toggle.add(super.toggle(index)); + await Future.wait(toggle); + } +} diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart new file mode 100644 index 000000000..5eaf2a0aa --- /dev/null +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -0,0 +1,253 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:forui/forui.dart'; +import 'package:forui/src/foundation/tappable.dart'; +import 'package:forui/src/foundation/util.dart'; + +import 'dart:math' as math; + +/// An item that represents a header in a [FAccordion]. +class FAccordionItem { + /// The title. + final Widget title; + + /// The child. + final Widget child; + + /// Whether the item is initially expanded. + final bool initiallyExpanded; + + /// Creates an [FAccordionItem]. + FAccordionItem({required this.title, required this.child, this.initiallyExpanded = false}); +} + +/// An interactive heading that reveals a section of content. +/// +/// See: +/// * https://forui.dev/docs/accordion for working examples. +class _Item extends StatefulWidget { + /// The accordion's style. Defaults to [FThemeData.accordionStyle]. + final FAccordionStyle style; + + /// The accordion's controller. + final FAccordionController controller; + + final FAccordionItem item; + + final int index; + + /// Creates an [_Item]. + const _Item({ + required this.style, + required this.index, + required this.item, + required this.controller, + }); + + @override + State<_Item> createState() => _ItemState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(DiagnosticsProperty('controller', controller)) + ..add(DiagnosticsProperty('item', item)) + ..add(IntProperty('index', index)); + } +} + +class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _expand; + bool _hovered = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.controller.animationDuration, + value: widget.item.initiallyExpanded ? 1.0 : 0.0, + vsync: this, + ); + _expand = Tween( + begin: 0, + end: 100, + ).animate( + CurvedAnimation( + curve: Curves.ease, + parent: _controller, + ), + ); + widget.controller.addItem(widget.index, _controller, _expand, widget.item.initiallyExpanded); + } + + @override + void didUpdateWidget(covariant _Item old) { + super.didUpdateWidget(old); + if (widget.controller != old.controller) { + _controller = AnimationController( + duration: widget.controller.animationDuration, + value: widget.item.initiallyExpanded ? 1.0 : 0.0, + vsync: this, + ); + _expand = Tween( + begin: 0, + end: 100, + ).animate( + CurvedAnimation( + curve: Curves.ease, + parent: _controller, + ), + ); + + old.controller.removeItem(old.index); + widget.controller.addItem(widget.index, _controller, _expand, widget.item.initiallyExpanded); + } + } + + @override + Widget build(BuildContext context) => AnimatedBuilder( + animation: widget.controller.controllers[widget.index].animation, + builder: (context, _) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FTappable( + onPress: () => widget.controller.toggle(widget.index), + child: MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + child: Container( + padding: widget.style.titlePadding, + child: Row( + children: [ + Expanded( + child: merge( + // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + style: widget.style.titleTextStyle + .copyWith(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), + child: widget.item.title, + ), + ), + Transform.rotate( + //TODO: Should I be getting the percentage value from the controller or from its local state? + angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, + + child: widget.style.icon, + ), + ], + ), + ), + ), + ), + // We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by + // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the + // child to layout at a smaller size while the amount of padding remains the same. + _Expandable( + //TODO: Should I be getting the percentage value from the controller or from its local state? + percentage: _expand.value / 100, + child: ClipRect( + //TODO: Should I be getting the percentage value from the controller or from its local state? + clipper: _Clipper(percentage: _expand.value / 100), + child: Padding( + padding: widget.style.contentPadding, + child: DefaultTextStyle(style: widget.style.childTextStyle, child: widget.item.child), + ), + ), + ), + FDivider( + style: context.theme.dividerStyles.horizontal + .copyWith(padding: EdgeInsets.zero, color: widget.style.dividerColor), + ), + ], + ), + ); +} + +class _Expandable extends SingleChildRenderObjectWidget { + final double _percentage; + + const _Expandable({ + required Widget child, + required double percentage, + }) : _percentage = percentage, + super(child: child); + + @override + RenderObject createRenderObject(BuildContext context) => _ExpandableBox(percentage: _percentage); + + @override + void updateRenderObject(BuildContext context, _ExpandableBox renderObject) { + renderObject.percentage = _percentage; + } +} + +class _ExpandableBox extends RenderBox with RenderObjectWithChildMixin { + double _percentage; + + _ExpandableBox({ + required double percentage, + }) : _percentage = percentage; + + @override + void performLayout() { + if (child case final child?) { + child.layout(constraints.normalize(), parentUsesSize: true); + size = Size(child.size.width, child.size.height * _percentage); + } else { + size = constraints.smallest; + } + } + + @override + void paint(PaintingContext context, Offset offset) { + if (child case final child?) { + context.paintChild(child, offset); + } + } + + @override + bool hitTest(BoxHitTestResult result, {required Offset position}) { + if (size.contains(position) && child!.hitTest(result, position: position)) { + result.add(BoxHitTestEntry(this, position)); + return true; + } + + return false; + } + + double get percentage => _percentage; + + set percentage(double value) { + if (_percentage == value) { + return; + } + + _percentage = value; + markNeedsLayout(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('percentage', percentage)); + } +} + +class _Clipper extends CustomClipper { + final double percentage; + + _Clipper({required this.percentage}); + + @override + Rect getClip(Size size) => Offset.zero & Size(size.width, size.height * percentage); + + @override + bool shouldReclip(covariant _Clipper oldClipper) => oldClipper.percentage != percentage; +} diff --git a/forui/lib/widgets/accordion.dart b/forui/lib/widgets/accordion.dart index 2bcf059f9..747917c19 100644 --- a/forui/lib/widgets/accordion.dart +++ b/forui/lib/widgets/accordion.dart @@ -1,8 +1,10 @@ /// {@category Widgets} /// -/// An interactive heading that reveals a section of content. +/// A vertically stacked set of interactive headings that each reveal a section of content. /// /// See https://forui.dev/docs/accordion for working examples. library forui.widgets.accordion; -export '../src/widgets/accordion.dart'; +export '../src/widgets/accordion/accordion.dart'; +export '../src/widgets/accordion/accordion_controller.dart'; +export '../src/widgets/accordion/accordion_item.dart'; From 9fdc4f6c20bbe19786a59c0bd92a907170b96fe3 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 22 Sep 2024 14:31:09 +0800 Subject: [PATCH 32/57] new changes to be made --- .../lib/src/widgets/accordion/accordion.dart | 7 ++++ .../accordion/accordion_controller.dart | 34 +++++++++++-------- .../src/widgets/accordion/accordion_item.dart | 4 +-- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index fc4e5f6a3..19f0213c4 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -54,6 +54,13 @@ class _FAccordionState extends State { void initState() { super.initState(); _controller = widget.controller ?? FRadioAccordionController(); + // TODO: check no.of widget.items and make sure they correspond with min and max + } + + @override + void didUpdateWidget(covariant FAccordion oldWidget) { + // TODO: check no.of widget.items and make sure they correspond with min and max + super.didUpdateWidget(oldWidget); } @override diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index da665464e..47598b8c4 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -1,15 +1,18 @@ import 'package:flutter/widgets.dart'; /// A controller that controls which sections are shown and hidden. -base class FAccordionController extends ChangeNotifier { +abstract base class FAccordionController extends ChangeNotifier { /// The duration of the expansion and collapse animations. final Duration animationDuration; + /// A list of controllers for each of the headers in the accordion. - final List<({AnimationController controller, Animation animation})> controllers; + final Map controllers; final Set _expanded; final int _min; final int? _max; + factory => _MultiselectAccordionController(); + /// Creates a [FAccordionController]. /// /// The [min] and [max] values are the minimum and maximum number of selections allowed. Defaults to no minimum or maximum. @@ -24,7 +27,7 @@ base class FAccordionController extends ChangeNotifier { this.animationDuration = const Duration(milliseconds: 500), }) : _min = min, _max = max, - controllers = [], + controllers = {}, _expanded = {}, assert(min >= 0, 'The min value must be greater than or equal to 0.'), assert(max == null || max >= 0, 'The max value must be greater than or equal to 0.'), @@ -33,17 +36,16 @@ base class FAccordionController extends ChangeNotifier { /// Adds an item to the accordion. // ignore: avoid_positional_boolean_parameters void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { - controllers.add((controller: controller, animation: animation)); + controllers[index] = (controller: controller, animation: animation); if (initiallyExpanded) { + //TODO: check min, max items _expanded.add(index); } } /// Removes an item from the accordion. void removeItem(int index) { - if (controllers.length <= index) { - controllers.removeAt(index); - } + controllers.removeWhere(index); _expanded.remove(index); } @@ -59,21 +61,21 @@ base class FAccordionController extends ChangeNotifier { _expanded.remove(index); await collapse(index); } else { - if (_max != null && _expanded.length >= _max) { - return; - } - _expanded.add(index); - await expand(index); - } - notifyListeners(); + // Expand Accordion + } } /// Shows the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. Future expand(int index) async { + + if (_max != null && _expanded.length >= _max) { + return; + } + _expanded.add(index); final controller = controllers[index].controller; await controller.forward(); notifyListeners(); @@ -112,10 +114,14 @@ final class FRadioAccordionController extends FAccordionController { final toggle = []; if (_expanded.length > _min) { if (!_expanded.contains(index) && _expanded.length >= _max!) { + toggle.add(collapse(_expanded.first)); _expanded.remove(_expanded.first); } } + + /// super.toggle + toggle.add(super.toggle(index)); await Future.wait(toggle); } diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 5eaf2a0aa..16ea7d51e 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -8,7 +8,7 @@ import 'package:forui/src/foundation/util.dart'; import 'dart:math' as math; /// An item that represents a header in a [FAccordion]. -class FAccordionItem { +class FAccordionData extends InheritedWidget{ /// The title. final Widget title; @@ -26,7 +26,7 @@ class FAccordionItem { /// /// See: /// * https://forui.dev/docs/accordion for working examples. -class _Item extends StatefulWidget { +class FAccordionItem extends StatefulWidget { /// The accordion's style. Defaults to [FThemeData.accordionStyle]. final FAccordionStyle style; From 11343077d32589e9617296e96cd3b5cd3d409d74 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Thu, 19 Sep 2024 17:23:53 +0000 Subject: [PATCH 33/57] Commit from GitHub Actions (Forui Presubmit) --- .../lib/src/widgets/accordion/accordion.dart | 25 ++++++++++--------- .../src/widgets/accordion/accordion_item.dart | 4 +-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index 19f0213c4..af303be93 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -1,9 +1,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; + import 'package:flutter_svg/svg.dart'; +import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; -import 'package:meta/meta.dart'; /// A vertically stacked set of interactive headings that each reveal a section of content. /// @@ -113,9 +114,9 @@ final class FAccordionStyle with Diagnosticable { /// Creates a [FDividerStyles] that inherits its properties from [colorScheme]. FAccordionStyle.inherit({required FColorScheme colorScheme, required FTypography typography}) : titleTextStyle = typography.base.copyWith( - fontWeight: FontWeight.w500, - color: colorScheme.foreground, - ), + fontWeight: FontWeight.w500, + color: colorScheme.foreground, + ), childTextStyle = typography.sm.copyWith( color: colorScheme.foreground, ), @@ -160,14 +161,14 @@ final class FAccordionStyle with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is FAccordionStyle && - runtimeType == other.runtimeType && - titleTextStyle == other.titleTextStyle && - childTextStyle == other.childTextStyle && - titlePadding == other.titlePadding && - contentPadding == other.contentPadding && - icon == other.icon && - dividerColor == other.dividerColor; + other is FAccordionStyle && + runtimeType == other.runtimeType && + titleTextStyle == other.titleTextStyle && + childTextStyle == other.childTextStyle && + titlePadding == other.titlePadding && + contentPadding == other.contentPadding && + icon == other.icon && + dividerColor == other.dividerColor; @override int get hashCode => diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 16ea7d51e..4e79b8ac7 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -5,8 +7,6 @@ import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; -import 'dart:math' as math; - /// An item that represents a header in a [FAccordion]. class FAccordionData extends InheritedWidget{ /// The title. From 9a43e9c6ae459e0216c9b040d8a8d23e53c90eb6 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 22 Sep 2024 17:34:35 +0800 Subject: [PATCH 34/57] Implemented FAccordionItemData, restructured controllers --- forui/example/lib/sandbox.dart | 18 +- .../lib/src/widgets/accordion/accordion.dart | 10 +- .../accordion/accordion_controller.dart | 135 +++++++--- .../src/widgets/accordion/accordion_item.dart | 242 ++++++++++-------- .../accordion/accordion_golden_test.dart | 12 +- samples/lib/widgets/accordion.dart | 14 +- 6 files changed, 261 insertions(+), 170 deletions(-) diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index 7db29e265..f0d469b64 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -24,10 +24,10 @@ class _SandboxState extends State { children: [ FAccordion( items: [ - FAccordionItem( - title: const Text('Title 1'), + const FAccordionItem( + title: Text('Title 1'), initiallyExpanded: true, - child: const Text( + child: Text( 'Yes. It adheres to the WAI-ARIA design pattern, wfihwe fdhfiwf dfhwiodf dfwhoif', ), ), @@ -42,16 +42,16 @@ class _SandboxState extends State { ), ), ), - FAccordionItem( - title: const Text('Title 3'), - child: const Text( + const FAccordionItem( + title: Text('Title 3'), + child: Text( 'Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left, ), ), - FAccordionItem( - title: const Text('Title 4'), - child: const Text( + const FAccordionItem( + title: Text('Title 4'), + child: Text( 'Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left, ), diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index af303be93..5350a461c 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -65,20 +65,16 @@ class _FAccordionState extends State { } @override - Widget build(BuildContext context) { - final style = widget.style ?? context.theme.accordionStyle; - return Column( + Widget build(BuildContext context) => Column( children: [ for (final (index, widget) in widget.items.indexed) - _Item( - style: style, + FAccordionItemData( index: index, - item: widget, controller: _controller, + child: widget, ), ], ); - } } /// The [FAccordion] style. diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index 47598b8c4..70790e34d 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -11,8 +11,6 @@ abstract base class FAccordionController extends ChangeNotifier { final int _min; final int? _max; - factory => _MultiselectAccordionController(); - /// Creates a [FAccordionController]. /// /// The [min] and [max] values are the minimum and maximum number of selections allowed. Defaults to no minimum or maximum. @@ -21,7 +19,9 @@ abstract base class FAccordionController extends ChangeNotifier { /// * Throws [AssertionError] if [min] < 0. /// * Throws [AssertionError] if [max] < 0. /// * Throws [AssertionError] if [min] > [max]. - FAccordionController({ + factory FAccordionController({int min, int? max, Duration animationDuration}) = _MultiSelectAccordionController; + + FAccordionController._({ int min = 0, int? max, this.animationDuration = const Duration(milliseconds: 500), @@ -35,58 +35,87 @@ abstract base class FAccordionController extends ChangeNotifier { /// Adds an item to the accordion. // ignore: avoid_positional_boolean_parameters + void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded); + + /// Removes an item from the accordion. + void removeItem(int index); + + /// Convenience method for toggling the current expanded status. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future toggle(int index); + + /// Shows the content in the accordion. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future expand(int index); + + /// Hides the content in the accordion. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future collapse(int index); +} + +final class _MultiSelectAccordionController extends FAccordionController { + _MultiSelectAccordionController({ + super.min, + super.max, + super.animationDuration, + }) : super._(); + + /// Adds an item to the accordion. + // ignore: avoid_positional_boolean_parameters + @override void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { controllers[index] = (controller: controller, animation: animation); - if (initiallyExpanded) { + if (initiallyExpanded && _expanded.length < _max!) { //TODO: check min, max items _expanded.add(index); } } /// Removes an item from the accordion. + @override void removeItem(int index) { - controllers.removeWhere(index); + controllers.remove(index); _expanded.remove(index); } /// Convenience method for toggling the current expanded status. /// /// This method should typically not be called while the widget tree is being rebuilt. - Future toggle(int index) async { - if (controllers[index].controller.value == 1) { - if (_expanded.length <= _min) { - return; - } - - _expanded.remove(index); - await collapse(index); - } else { - - - // Expand Accordion - } - } + @override + Future toggle(int index) async => controllers[index]?.animation.value == 100 ? collapse(index) : expand(index); /// Shows the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. + @override Future expand(int index) async { - if (_max != null && _expanded.length >= _max) { return; } + _expanded.add(index); - final controller = controllers[index].controller; - await controller.forward(); + + final controller = controllers[index]?.controller; + await controller?.forward(); notifyListeners(); } /// Hides the content in the accordion. /// /// This method should typically not be called while the widget tree is being rebuilt. + @override Future collapse(int index) async { - final controller = controllers[index].controller; - await controller.reverse(); + if (_expanded.length <= _min) { + return; + } + + _expanded.remove(index); + + final controller = controllers[index]?.controller; + await controller?.reverse(); notifyListeners(); } } @@ -98,31 +127,61 @@ final class FRadioAccordionController extends FAccordionController { super.animationDuration, super.min, int super.max = 1, - }); + }) : super._(); @override void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { - if (_expanded.length > _max!) { - super.addItem(index, controller, animation, false); - } else { - super.addItem(index, controller, animation, initiallyExpanded); + + controllers[index] = (controller: controller, animation: animation); + + if (initiallyExpanded) { + //TODO: check min, max items + _expanded.add(index); } } @override - Future toggle(int index) async { - final toggle = []; - if (_expanded.length > _min) { - if (!_expanded.contains(index) && _expanded.length >= _max!) { + void removeItem(int index) { + controllers.remove(index); + _expanded.remove(index); + } - toggle.add(collapse(_expanded.first)); - _expanded.remove(_expanded.first); + @override + Future toggle(int index) async => controllers[index]?.animation.value == 100 ? collapse(index) : expand(index); + + + @override + Future collapse(int index) async { + if (_expanded.length <= _min) { + return; + } + + _expanded.remove(index); + + final controller = controllers[index]?.controller; + await controller?.reverse(); + notifyListeners(); + } + + @override + Future expand(int index) async { + final expand = >[]; + if (_expanded.length > _min && _expanded.length >= _max!) { + + if(_expanded.contains(index)) { + return; } + expand.add(collapse(_expanded.first)); } - /// super.toggle + _expanded.add(index); + + final controller = controllers[index]?.controller; + + expand.add(controller!.forward() as Future); + await Future.wait(expand); + + notifyListeners(); - toggle.add(super.toggle(index)); - await Future.wait(toggle); } } diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 4e79b8ac7..33e632c7a 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -6,20 +6,45 @@ import 'package:flutter/widgets.dart'; import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; +import 'package:meta/meta.dart'; /// An item that represents a header in a [FAccordion]. -class FAccordionData extends InheritedWidget{ - /// The title. - final Widget title; +class FAccordionItemData extends InheritedWidget { + /// Returns the [FAccordionItemData] of the [FAccordionItem] in the given [context]. + /// + /// ## Contract + /// Throws [AssertionError] if there is no ancestor [FAccordionItem] in the given [context]. + @useResult + static FAccordionItemData of(BuildContext context) { + final data = context.dependOnInheritedWidgetOfExactType(); + assert(data != null, 'No FAccordionData found in context'); + return data!; + } - /// The child. - final Widget child; + /// The item's index. + final int index; - /// Whether the item is initially expanded. - final bool initiallyExpanded; + /// The accordion's controller. + final FAccordionController controller; - /// Creates an [FAccordionItem]. - FAccordionItem({required this.title, required this.child, this.initiallyExpanded = false}); + /// Creates an [FAccordionItemData]. + const FAccordionItemData({ + required this.index, + required this.controller, + required super.child, + super.key, + }); + + @override + bool updateShouldNotify(covariant FAccordionItemData old) => index != old.index || controller != old.controller; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('index', index)) + ..add(DiagnosticsProperty('controller', controller)); + } } /// An interactive heading that reveals a section of content. @@ -28,38 +53,41 @@ class FAccordionData extends InheritedWidget{ /// * https://forui.dev/docs/accordion for working examples. class FAccordionItem extends StatefulWidget { /// The accordion's style. Defaults to [FThemeData.accordionStyle]. - final FAccordionStyle style; + final FAccordionStyle? style; - /// The accordion's controller. - final FAccordionController controller; + /// The title. + final Widget title; - final FAccordionItem item; + /// Whether the item is initially expanded. + final bool initiallyExpanded; - final int index; + /// The child. + final Widget child; - /// Creates an [_Item]. - const _Item({ - required this.style, - required this.index, - required this.item, - required this.controller, + /// Creates an [FAccordionItem]. + const FAccordionItem({ + required this.title, + required this.child, + this.style, + this.initiallyExpanded = false, + super.key, }); @override - State<_Item> createState() => _ItemState(); + State createState() => _FAccordionItemState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('style', style)) - ..add(DiagnosticsProperty('controller', controller)) - ..add(DiagnosticsProperty('item', item)) - ..add(IntProperty('index', index)); + ..add(DiagnosticsProperty('title', title)) + ..add(DiagnosticsProperty('initiallyExpanded', initiallyExpanded)) + ..add(DiagnosticsProperty('child', child)); } } -class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { +class _FAccordionItemState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _expand; bool _hovered = false; @@ -67,9 +95,11 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { @override void initState() { super.initState(); + final data = context.getInheritedWidgetOfExactType(); + _controller = AnimationController( - duration: widget.controller.animationDuration, - value: widget.item.initiallyExpanded ? 1.0 : 0.0, + duration: data?.controller.animationDuration, + value: widget.initiallyExpanded ? 1.0 : 0.0, vsync: this, ); _expand = Tween( @@ -81,93 +111,99 @@ class _ItemState extends State<_Item> with SingleTickerProviderStateMixin { parent: _controller, ), ); - widget.controller.addItem(widget.index, _controller, _expand, widget.item.initiallyExpanded); + data?.controller.addItem(data.index, _controller, _expand, widget.initiallyExpanded); } + // + // + // @override + // void didUpdateWidget(covariant FAccordionItem old) { + // super.didUpdateWidget(old); + // + // if (widget.controller != old.controller) { + // _controller = AnimationController( + // duration: widget.controller.animationDuration, + // value: widget.item.initiallyExpanded ? 1.0 : 0.0, + // vsync: this, + // ); + // _expand = Tween( + // begin: 0, + // end: 100, + // ).animate( + // CurvedAnimation( + // curve: Curves.ease, + // parent: _controller, + // ), + // ); + // + // old.controller.removeItem(old.index); + // widget.controller.addItem(widget.index, _controller, _expand, widget.item.initiallyExpanded); + // } + // } @override - void didUpdateWidget(covariant _Item old) { - super.didUpdateWidget(old); - if (widget.controller != old.controller) { - _controller = AnimationController( - duration: widget.controller.animationDuration, - value: widget.item.initiallyExpanded ? 1.0 : 0.0, - vsync: this, - ); - _expand = Tween( - begin: 0, - end: 100, - ).animate( - CurvedAnimation( - curve: Curves.ease, - parent: _controller, - ), - ); - - old.controller.removeItem(old.index); - widget.controller.addItem(widget.index, _controller, _expand, widget.item.initiallyExpanded); - } - } - - @override - Widget build(BuildContext context) => AnimatedBuilder( - animation: widget.controller.controllers[widget.index].animation, - builder: (context, _) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FTappable( - onPress: () => widget.controller.toggle(widget.index), - child: MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: Container( - padding: widget.style.titlePadding, - child: Row( - children: [ - Expanded( - child: merge( - // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. - textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, - ), - style: widget.style.titleTextStyle - .copyWith(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), - child: widget.item.title, + Widget build(BuildContext context) { + final FAccordionItemData(:index, :controller) = FAccordionItemData.of(context); + + final style = widget.style ?? context.theme.accordionStyle; + return AnimatedBuilder( + animation: _expand, + builder: (context, _) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FTappable( + onPress: () => controller.toggle(index), + child: MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + child: Container( + padding: style.titlePadding, + child: Row( + children: [ + Expanded( + child: merge( + // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, ), + style: style.titleTextStyle + .copyWith(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), + child: widget.title, ), - Transform.rotate( - //TODO: Should I be getting the percentage value from the controller or from its local state? - angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, - - child: widget.style.icon, - ), - ], - ), + ), + Transform.rotate( + //TODO: Should I be getting the percentage value from the controller or from its local state? + angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, + + child: style.icon, + ), + ], ), ), ), - // We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by - // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the - // child to layout at a smaller size while the amount of padding remains the same. - _Expandable( + ), + // We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by + // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the + // child to layout at a smaller size while the amount of padding remains the same. + _Expandable( + //TODO: Should I be getting the percentage value from the controller or from its local state? + percentage: _expand.value / 100, + child: ClipRect( //TODO: Should I be getting the percentage value from the controller or from its local state? - percentage: _expand.value / 100, - child: ClipRect( - //TODO: Should I be getting the percentage value from the controller or from its local state? - clipper: _Clipper(percentage: _expand.value / 100), - child: Padding( - padding: widget.style.contentPadding, - child: DefaultTextStyle(style: widget.style.childTextStyle, child: widget.item.child), - ), + clipper: _Clipper(percentage: _expand.value / 100), + child: Padding( + padding: style.contentPadding, + child: DefaultTextStyle(style: style.childTextStyle, child: widget.child), ), ), - FDivider( - style: context.theme.dividerStyles.horizontal - .copyWith(padding: EdgeInsets.zero, color: widget.style.dividerColor), - ), - ], - ), - ); + ), + FDivider( + style: context.theme.dividerStyles.horizontal.copyWith(padding: EdgeInsets.zero, color: style.dividerColor), + ), + ], + ), + ); + } } class _Expandable extends SingleChildRenderObjectWidget { diff --git a/forui/test/src/widgets/accordion/accordion_golden_test.dart b/forui/test/src/widgets/accordion/accordion_golden_test.dart index ddcb72203..a0216ba5f 100644 --- a/forui/test/src/widgets/accordion/accordion_golden_test.dart +++ b/forui/test/src/widgets/accordion/accordion_golden_test.dart @@ -15,15 +15,15 @@ void main() { MaterialApp( home: TestScaffold( data: FThemes.zinc.light, - child: Column( + child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( items: [ FAccordionItem( - title: const Text('Title'), + title: Text('Title'), initiallyExpanded: true, - child: const ColoredBox( + child: ColoredBox( color: Colors.yellow, child: SizedBox.square( dimension: 50, @@ -46,15 +46,15 @@ void main() { MaterialApp( home: TestScaffold( data: FThemes.zinc.light, - child: Column( + child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( items: [ FAccordionItem( - title: const Text('Title'), + title: Text('Title'), initiallyExpanded: true, - child: const ColoredBox( + child: ColoredBox( color: Colors.yellow, child: SizedBox.square( dimension: 50, diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart index 83347bb86..f240e4864 100644 --- a/samples/lib/widgets/accordion.dart +++ b/samples/lib/widgets/accordion.dart @@ -25,21 +25,21 @@ class AccordionPage extends SampleScaffold { children: [ FAccordion( controller: controller, - items: [ + items: const [ FAccordionItem( - title: const Text('Is it accessible?'), - child: const Text('Yes. It adheres to the WAI-ARIA design pattern.'), + title: Text('Is it accessible?'), + child: Text('Yes. It adheres to the WAI-ARIA design pattern.'), ), FAccordionItem( - title: const Text('Is it Styled?'), + title: Text('Is it Styled?'), initiallyExpanded: true, - child: const Text( + child: Text( "Yes. It comes with default styles that matches the other components' aesthetics", ), ), FAccordionItem( - title: const Text('Is it Animated?'), - child: const Text( + title: Text('Is it Animated?'), + child: Text( 'Yes. It is animated by default, but you can disable it if you prefer', ), ), From 88c6f223f38153d911fa28f35dd9d1fde21a26b5 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 22 Sep 2024 09:35:53 +0000 Subject: [PATCH 35/57] Commit from GitHub Actions (Forui Samples Presubmit) --- samples/lib/widgets/accordion.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart index f240e4864..640fbe87b 100644 --- a/samples/lib/widgets/accordion.dart +++ b/samples/lib/widgets/accordion.dart @@ -27,19 +27,19 @@ class AccordionPage extends SampleScaffold { controller: controller, items: const [ FAccordionItem( - title: Text('Is it accessible?'), - child: Text('Yes. It adheres to the WAI-ARIA design pattern.'), + title: Text('Is it accessible?'), + child: Text('Yes. It adheres to the WAI-ARIA design pattern.'), ), FAccordionItem( - title: Text('Is it Styled?'), + title: Text('Is it Styled?'), initiallyExpanded: true, - child: Text( + child: Text( "Yes. It comes with default styles that matches the other components' aesthetics", ), ), FAccordionItem( - title: Text('Is it Animated?'), - child: Text( + title: Text('Is it Animated?'), + child: Text( 'Yes. It is animated by default, but you can disable it if you prefer', ), ), From e7413d95b717b28bb1bf6afa5ba10676f1ed850c Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Mon, 23 Sep 2024 01:55:01 +0800 Subject: [PATCH 36/57] Ready for next review --- forui/example/lib/sandbox.dart | 45 ++---------- .../lib/src/widgets/accordion/accordion.dart | 36 +++++++--- .../accordion/accordion_controller.dart | 68 +++++++++---------- .../src/widgets/accordion/accordion_item.dart | 43 ++++-------- forui/lib/widgets/accordion.dart | 2 +- .../src/widgets/accordion/accordion_test.dart | 13 ++-- 6 files changed, 85 insertions(+), 122 deletions(-) diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index f0d469b64..c4f8a20a7 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -25,9 +25,9 @@ class _SandboxState extends State { FAccordion( items: [ const FAccordionItem( - title: Text('Title 1'), + title: Text('Title 1'), initiallyExpanded: true, - child: Text( + child: Text( 'Yes. It adheres to the WAI-ARIA design pattern, wfihwe fdhfiwf dfhwiodf dfwhoif', ), ), @@ -43,15 +43,15 @@ class _SandboxState extends State { ), ), const FAccordionItem( - title: Text('Title 3'), - child: Text( + title: Text('Title 3'), + child: Text( 'Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left, ), ), const FAccordionItem( - title: Text('Title 4'), - child: Text( + title: Text('Title 4'), + child: Text( 'Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left, ), @@ -69,39 +69,6 @@ class _SandboxState extends State { FSelectGroupItem.checkbox(value: 3, label: const Text('Checkbox 3'), semanticLabel: 'Checkbox 3'), ], ), - FAccordion( - title: 'Is it Accessible?', - childHeight: 100, - initiallyExpanded: false, - onExpanded: () {}, - child: const Text('Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left,), - ), - FAccordion( - title: 'Is it Accessible?', - childHeight: 100, - initiallyExpanded: false, - onExpanded: () {}, - child: const Text('Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left,), - ), - FAccordion( - title: 'Is it Accessible?', - childHeight: 100, - initiallyExpanded: false, - onExpanded: () {}, - child: const Text('Yes. It adheres to the WAI-ARIA design pattern', textAlign: TextAlign.left,), - ), - SizedBox(height: 20), - FTooltip( - longPressExitDuration: const Duration(seconds: 5000), - tipBuilder: (context, style, _) => const Text('Add to library'), - child: FButton( - style: FButtonStyle.outline, - onPress: () {}, - label: const Text('Hover'), - ), - ), - const SizedBox(height: 20), - const FTextField.password(), ], ); } diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index 5350a461c..5b54e53ce 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:forui/src/widgets/accordion/accordion_item.dart'; import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; @@ -55,26 +56,39 @@ class _FAccordionState extends State { void initState() { super.initState(); _controller = widget.controller ?? FRadioAccordionController(); - // TODO: check no.of widget.items and make sure they correspond with min and max + + final expandedLength = widget.items.where((item) => item.initiallyExpanded).length; + + if (!_controller.validate(expandedLength)) { + throw StateError('number of expanded items must be within the min and max.'); + } } @override void didUpdateWidget(covariant FAccordion oldWidget) { - // TODO: check no.of widget.items and make sure they correspond with min and max super.didUpdateWidget(oldWidget); + + if (widget.controller != oldWidget.controller) { + _controller = widget.controller ?? FRadioAccordionController(); + final expandedLength = widget.items.where((item) => item.initiallyExpanded).length; + + if (!_controller.validate(expandedLength)) { + throw StateError('number of expanded items must be within the min and max.'); + } + } } @override Widget build(BuildContext context) => Column( - children: [ - for (final (index, widget) in widget.items.indexed) - FAccordionItemData( - index: index, - controller: _controller, - child: widget, - ), - ], - ); + children: [ + for (final (index, widget) in widget.items.indexed) + FAccordionItemData( + index: index, + controller: _controller, + child: widget, + ), + ], + ); } /// The [FAccordion] style. diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index 70790e34d..a4df85ebf 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -11,7 +11,10 @@ abstract base class FAccordionController extends ChangeNotifier { final int _min; final int? _max; - /// Creates a [FAccordionController]. + /// Creates a multi-select [FAccordionController]. + factory FAccordionController({int min, int? max, Duration animationDuration}) = _MultiSelectAccordionController; + + /// Creates a base [FAccordionController]. /// /// The [min] and [max] values are the minimum and maximum number of selections allowed. Defaults to no minimum or maximum. /// @@ -19,9 +22,7 @@ abstract base class FAccordionController extends ChangeNotifier { /// * Throws [AssertionError] if [min] < 0. /// * Throws [AssertionError] if [max] < 0. /// * Throws [AssertionError] if [min] > [max]. - factory FAccordionController({int min, int? max, Duration animationDuration}) = _MultiSelectAccordionController; - - FAccordionController._({ + FAccordionController.base({ int min = 0, int? max, this.animationDuration = const Duration(milliseconds: 500), @@ -38,7 +39,7 @@ abstract base class FAccordionController extends ChangeNotifier { void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded); /// Removes an item from the accordion. - void removeItem(int index); + bool removeItem(int index); /// Convenience method for toggling the current expanded status. /// @@ -54,6 +55,9 @@ abstract base class FAccordionController extends ChangeNotifier { /// /// This method should typically not be called while the widget tree is being rebuilt. Future collapse(int index); + + /// Validate if the number of expanded items is within the min and max. + bool validate(int length); } final class _MultiSelectAccordionController extends FAccordionController { @@ -61,35 +65,27 @@ final class _MultiSelectAccordionController extends FAccordionController { super.min, super.max, super.animationDuration, - }) : super._(); + }) : super.base(); - /// Adds an item to the accordion. - // ignore: avoid_positional_boolean_parameters @override void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { controllers[index] = (controller: controller, animation: animation); - if (initiallyExpanded && _expanded.length < _max!) { - //TODO: check min, max items + if (initiallyExpanded && validate(_expanded.length)) { _expanded.add(index); } } - /// Removes an item from the accordion. @override - void removeItem(int index) { - controllers.remove(index); + bool removeItem(int index) { + final removed = controllers.remove(index); _expanded.remove(index); + + return removed != null; } - /// Convenience method for toggling the current expanded status. - /// - /// This method should typically not be called while the widget tree is being rebuilt. @override Future toggle(int index) async => controllers[index]?.animation.value == 100 ? collapse(index) : expand(index); - /// Shows the content in the accordion. - /// - /// This method should typically not be called while the widget tree is being rebuilt. @override Future expand(int index) async { if (_max != null && _expanded.length >= _max) { @@ -103,9 +99,6 @@ final class _MultiSelectAccordionController extends FAccordionController { notifyListeners(); } - /// Hides the content in the accordion. - /// - /// This method should typically not be called while the widget tree is being rebuilt. @override Future collapse(int index) async { if (_expanded.length <= _min) { @@ -118,38 +111,40 @@ final class _MultiSelectAccordionController extends FAccordionController { await controller?.reverse(); notifyListeners(); } + + @override + bool validate(int length) => length >= _min && (_max == null || length <= _max); } -/// An [FAccordionController] that allows one section to be expanded at a time. +/// An [FAccordionController] that allows only one section to be expanded at a time. final class FRadioAccordionController extends FAccordionController { /// Creates a [FRadioAccordionController]. FRadioAccordionController({ super.animationDuration, super.min, int super.max = 1, - }) : super._(); + }) : super.base(); @override void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { - controllers[index] = (controller: controller, animation: animation); - if (initiallyExpanded) { - //TODO: check min, max items + if (initiallyExpanded && validate(_expanded.length)) { _expanded.add(index); } } @override - void removeItem(int index) { - controllers.remove(index); + bool removeItem(int index) { + final removed = controllers.remove(index); _expanded.remove(index); + + return removed != null; } @override Future toggle(int index) async => controllers[index]?.animation.value == 100 ? collapse(index) : expand(index); - @override Future collapse(int index) async { if (_expanded.length <= _min) { @@ -166,9 +161,9 @@ final class FRadioAccordionController extends FAccordionController { @override Future expand(int index) async { final expand = >[]; - if (_expanded.length > _min && _expanded.length >= _max!) { - if(_expanded.contains(index)) { + if (_expanded.length > _min && _max != null && _expanded.length >= _max) { + if (_expanded.contains(index)) { return; } expand.add(collapse(_expanded.first)); @@ -177,11 +172,14 @@ final class FRadioAccordionController extends FAccordionController { _expanded.add(index); final controller = controllers[index]?.controller; - - expand.add(controller!.forward() as Future); + if (controller != null) { + expand.add(controller.forward()); + } await Future.wait(expand); notifyListeners(); - } + + @override + bool validate(int length) => length >= _min && (_max == null || length <= _max); } diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 33e632c7a..ca28af9f3 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -8,6 +8,7 @@ import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; import 'package:meta/meta.dart'; +@internal /// An item that represents a header in a [FAccordion]. class FAccordionItemData extends InheritedWidget { /// Returns the [FAccordionItemData] of the [FAccordionItem] in the given [context]. @@ -93,12 +94,17 @@ class _FAccordionItemState extends State with SingleTickerProvid bool _hovered = false; @override - void initState() { - super.initState(); - final data = context.getInheritedWidgetOfExactType(); + void didChangeDependencies() { + super.didChangeDependencies(); + final data = FAccordionItemData.of(context); + + final removed = data.controller.removeItem(data.index); + if (removed) { + _controller.dispose(); + } _controller = AnimationController( - duration: data?.controller.animationDuration, + duration: data.controller.animationDuration, value: widget.initiallyExpanded ? 1.0 : 0.0, vsync: this, ); @@ -111,34 +117,9 @@ class _FAccordionItemState extends State with SingleTickerProvid parent: _controller, ), ); - data?.controller.addItem(data.index, _controller, _expand, widget.initiallyExpanded); + + data.controller.addItem(data.index, _controller, _expand, widget.initiallyExpanded); } - // - // - // @override - // void didUpdateWidget(covariant FAccordionItem old) { - // super.didUpdateWidget(old); - // - // if (widget.controller != old.controller) { - // _controller = AnimationController( - // duration: widget.controller.animationDuration, - // value: widget.item.initiallyExpanded ? 1.0 : 0.0, - // vsync: this, - // ); - // _expand = Tween( - // begin: 0, - // end: 100, - // ).animate( - // CurvedAnimation( - // curve: Curves.ease, - // parent: _controller, - // ), - // ); - // - // old.controller.removeItem(old.index); - // widget.controller.addItem(widget.index, _controller, _expand, widget.item.initiallyExpanded); - // } - // } @override Widget build(BuildContext context) { diff --git a/forui/lib/widgets/accordion.dart b/forui/lib/widgets/accordion.dart index 747917c19..19d6b371f 100644 --- a/forui/lib/widgets/accordion.dart +++ b/forui/lib/widgets/accordion.dart @@ -7,4 +7,4 @@ library forui.widgets.accordion; export '../src/widgets/accordion/accordion.dart'; export '../src/widgets/accordion/accordion_controller.dart'; -export '../src/widgets/accordion/accordion_item.dart'; +export '../src/widgets/accordion/accordion_item.dart' hide FAccordionItemData; diff --git a/forui/test/src/widgets/accordion/accordion_test.dart b/forui/test/src/widgets/accordion/accordion_test.dart index a7a8acefd..252e0fe9e 100644 --- a/forui/test/src/widgets/accordion/accordion_test.dart +++ b/forui/test/src/widgets/accordion/accordion_test.dart @@ -22,9 +22,12 @@ void main() { FAccordionItem( title: const Text('Title'), initiallyExpanded: true, - child: FButton( - onPress: () => taps++, - label: const Text('button'), + child: SizedBox.square( + dimension: 1, + child: GestureDetector( + onTap: () => taps++, + child: const Text('button'), + ), ), ), ], @@ -35,12 +38,12 @@ void main() { await tester.tap(find.text('Title')); await tester.pumpAndSettle(); - await tester.tap(find.byType(FButton), warnIfMissed: false); + await tester.tap(find.text('button'), warnIfMissed: false); expect(taps, 0); await tester.tap(find.text('Title')); await tester.pumpAndSettle(); - await tester.tap(find.byType(FButton)); + await tester.tap(find.text('button')); expect(taps, 1); }); }); From 9c3b8ca817809708f3c0093bdb0ced40f9cf020f Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 22 Sep 2024 17:56:24 +0000 Subject: [PATCH 37/57] Commit from GitHub Actions (Forui Presubmit) --- forui/lib/src/widgets/accordion/accordion.dart | 2 +- forui/lib/src/widgets/accordion/accordion_item.dart | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index 5b54e53ce..564ed2198 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -2,10 +2,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:forui/src/widgets/accordion/accordion_item.dart'; import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; +import 'package:forui/src/widgets/accordion/accordion_item.dart'; /// A vertically stacked set of interactive headings that each reveal a section of content. /// diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index ca28af9f3..bb12cb288 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -3,12 +3,14 @@ import 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; + import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; -import 'package:meta/meta.dart'; @internal + /// An item that represents a header in a [FAccordion]. class FAccordionItemData extends InheritedWidget { /// Returns the [FAccordionItemData] of the [FAccordionItem] in the given [context]. From 51cb60459d74855b7162480a7e6de5b068f6c5dd Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Tue, 24 Sep 2024 20:36:24 +0800 Subject: [PATCH 38/57] Apply suggestions from code review Co-authored-by: Matthias Ngeo --- forui/lib/src/widgets/accordion/accordion.dart | 4 +--- .../src/widgets/accordion/accordion_controller.dart | 2 +- forui/lib/src/widgets/accordion/accordion_item.dart | 13 ++----------- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index 564ed2198..96133430e 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -57,9 +57,7 @@ class _FAccordionState extends State { super.initState(); _controller = widget.controller ?? FRadioAccordionController(); - final expandedLength = widget.items.where((item) => item.initiallyExpanded).length; - - if (!_controller.validate(expandedLength)) { + if (!_controller.validate(widget.items.where((item) => item.initiallyExpanded).length)) { throw StateError('number of expanded items must be within the min and max.'); } } diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index a4df85ebf..1c4d37e91 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -16,7 +16,7 @@ abstract base class FAccordionController extends ChangeNotifier { /// Creates a base [FAccordionController]. /// - /// The [min] and [max] values are the minimum and maximum number of selections allowed. Defaults to no minimum or maximum. + /// The [min] and [max] values are the minimum and maximum number of selections allowed. Defaults to no minimum and maximum. /// /// # Contract: /// * Throws [AssertionError] if [min] < 0. diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index bb12cb288..8985e482d 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -11,12 +11,7 @@ import 'package:forui/src/foundation/util.dart'; @internal -/// An item that represents a header in a [FAccordion]. class FAccordionItemData extends InheritedWidget { - /// Returns the [FAccordionItemData] of the [FAccordionItem] in the given [context]. - /// - /// ## Contract - /// Throws [AssertionError] if there is no ancestor [FAccordionItem] in the given [context]. @useResult static FAccordionItemData of(BuildContext context) { final data = context.dependOnInheritedWidgetOfExactType(); @@ -24,13 +19,10 @@ class FAccordionItemData extends InheritedWidget { return data!; } - /// The item's index. final int index; - /// The accordion's controller. final FAccordionController controller; - /// Creates an [FAccordionItemData]. const FAccordionItemData({ required this.index, required this.controller, @@ -61,7 +53,7 @@ class FAccordionItem extends StatefulWidget { /// The title. final Widget title; - /// Whether the item is initially expanded. + /// True if the item is initially expanded. final bool initiallyExpanded; /// The child. @@ -100,8 +92,7 @@ class _FAccordionItemState extends State with SingleTickerProvid super.didChangeDependencies(); final data = FAccordionItemData.of(context); - final removed = data.controller.removeItem(data.index); - if (removed) { + if (data.controller.removeItem(data.index)) { _controller.dispose(); } From 9184191673489887510368a7b55449a221df08e0 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Tue, 24 Sep 2024 23:17:07 +0800 Subject: [PATCH 39/57] Fixed some pr issues --- forui/example/lib/sandbox.dart | 3 +- .../lib/src/widgets/accordion/accordion.dart | 75 +++++++++++++------ .../accordion/accordion_controller.dart | 61 ++++++++------- .../src/widgets/accordion/accordion_item.dart | 42 ++--------- .../accordion/accordion_golden_test.dart | 4 +- .../src/widgets/accordion/accordion_test.dart | 2 +- samples/lib/widgets/accordion.dart | 2 +- 7 files changed, 100 insertions(+), 89 deletions(-) diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index c4f8a20a7..c3188ae92 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -23,7 +23,8 @@ class _SandboxState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - items: [ + controller: FRadioAccordionController(min: 1, max: 3), + children: [ const FAccordionItem( title: Text('Title 1'), initiallyExpanded: true, diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index 96133430e..cc6878f51 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -5,7 +5,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; -import 'package:forui/src/widgets/accordion/accordion_item.dart'; /// A vertically stacked set of interactive headings that each reveal a section of content. /// @@ -13,24 +12,25 @@ import 'package:forui/src/widgets/accordion/accordion_item.dart'; /// /// See: /// * https://forui.dev/docs/FAccordion for working examples. +/// * [FAccordionController] for customizing the accordion's selection behavior. /// * [FAccordionStyle] for customizing a select group's appearance. class FAccordion extends StatefulWidget { /// The controller. /// /// See: - /// * [FRadioAccordionController] for a single radio like selection. + /// * [FRadioAccordionController] for a radio-like selection. /// * [FAccordionController] for default multiple selections. final FAccordionController? controller; /// The items. - final List items; + final List children; /// The style. Defaults to [FThemeData.accordionStyle]. final FAccordionStyle? style; /// Creates a [FAccordion]. const FAccordion({ - required this.items, + required this.children, this.controller, this.style, super.key, @@ -45,7 +45,7 @@ class FAccordion extends StatefulWidget { properties ..add(DiagnosticsProperty('controller', controller)) ..add(DiagnosticsProperty('style', style)) - ..add(IterableProperty('items', items)); + ..add(IterableProperty('items', children)); } } @@ -55,9 +55,9 @@ class _FAccordionState extends State { @override void initState() { super.initState(); - _controller = widget.controller ?? FRadioAccordionController(); + _controller = widget.controller ?? FAccordionController(); - if (!_controller.validate(widget.items.where((item) => item.initiallyExpanded).length)) { + if (!_controller.validate(widget.children.where((child) => child.initiallyExpanded).length)) { throw StateError('number of expanded items must be within the min and max.'); } } @@ -67,10 +67,9 @@ class _FAccordionState extends State { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { - _controller = widget.controller ?? FRadioAccordionController(); - final expandedLength = widget.items.where((item) => item.initiallyExpanded).length; + _controller = widget.controller ?? FAccordionController(); - if (!_controller.validate(expandedLength)) { + if (!_controller.validate(widget.children.where((child) => child.initiallyExpanded).length)) { throw StateError('number of expanded items must be within the min and max.'); } } @@ -79,11 +78,11 @@ class _FAccordionState extends State { @override Widget build(BuildContext context) => Column( children: [ - for (final (index, widget) in widget.items.indexed) + for (final (index, child) in widget.children.indexed) FAccordionItemData( index: index, controller: _controller, - child: widget, + child: child, ), ], ); @@ -97,11 +96,11 @@ final class FAccordionStyle with Diagnosticable { /// The child's default text style. final TextStyle childTextStyle; - /// The padding of the title. + /// The padding around the title. final EdgeInsets titlePadding; - /// The padding of the content. - final EdgeInsets contentPadding; + /// The padding around the content. + final EdgeInsets childPadding; /// The icon. final SvgPicture icon; @@ -114,7 +113,7 @@ final class FAccordionStyle with Diagnosticable { required this.titleTextStyle, required this.childTextStyle, required this.titlePadding, - required this.contentPadding, + required this.childPadding, required this.icon, required this.dividerColor, }); @@ -129,7 +128,7 @@ final class FAccordionStyle with Diagnosticable { color: colorScheme.foreground, ), titlePadding = const EdgeInsets.symmetric(vertical: 15), - contentPadding = const EdgeInsets.only(bottom: 15), + childPadding = const EdgeInsets.only(bottom: 15), icon = FAssets.icons.chevronRight( height: 20, colorFilter: ColorFilter.mode(colorScheme.primary, BlendMode.srcIn), @@ -142,7 +141,7 @@ final class FAccordionStyle with Diagnosticable { TextStyle? titleTextStyle, TextStyle? childTextStyle, EdgeInsets? titlePadding, - EdgeInsets? contentPadding, + EdgeInsets? childPadding, SvgPicture? icon, Color? dividerColor, }) => @@ -150,7 +149,7 @@ final class FAccordionStyle with Diagnosticable { titleTextStyle: titleTextStyle ?? this.titleTextStyle, childTextStyle: childTextStyle ?? this.childTextStyle, titlePadding: titlePadding ?? this.titlePadding, - contentPadding: contentPadding ?? this.contentPadding, + childPadding: childPadding ?? this.childPadding, icon: icon ?? this.icon, dividerColor: dividerColor ?? this.dividerColor, ); @@ -162,7 +161,7 @@ final class FAccordionStyle with Diagnosticable { ..add(DiagnosticsProperty('title', titleTextStyle)) ..add(DiagnosticsProperty('childTextStyle', childTextStyle)) ..add(DiagnosticsProperty('padding', titlePadding)) - ..add(DiagnosticsProperty('contentPadding', contentPadding)) + ..add(DiagnosticsProperty('contentPadding', childPadding)) ..add(ColorProperty('dividerColor', dividerColor)); } @@ -174,7 +173,7 @@ final class FAccordionStyle with Diagnosticable { titleTextStyle == other.titleTextStyle && childTextStyle == other.childTextStyle && titlePadding == other.titlePadding && - contentPadding == other.contentPadding && + childPadding == other.childPadding && icon == other.icon && dividerColor == other.dividerColor; @@ -183,7 +182,39 @@ final class FAccordionStyle with Diagnosticable { titleTextStyle.hashCode ^ childTextStyle.hashCode ^ titlePadding.hashCode ^ - contentPadding.hashCode ^ + childPadding.hashCode ^ icon.hashCode ^ dividerColor.hashCode; } + +@internal +class FAccordionItemData extends InheritedWidget { + @useResult + static FAccordionItemData of(BuildContext context) { + final data = context.dependOnInheritedWidgetOfExactType(); + assert(data != null, 'No FAccordionData found in context'); + return data!; + } + + final int index; + + final FAccordionController controller; + + const FAccordionItemData({ + required this.index, + required this.controller, + required super.child, + super.key, + }); + + @override + bool updateShouldNotify(covariant FAccordionItemData old) => index != old.index || controller != old.controller; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('index', index)) + ..add(DiagnosticsProperty('controller', controller)); + } +} diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index 1c4d37e91..2c086fb05 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -38,7 +38,7 @@ abstract base class FAccordionController extends ChangeNotifier { // ignore: avoid_positional_boolean_parameters void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded); - /// Removes an item from the accordion. + /// Removes an item from the accordion. Returns true if the item was removed. bool removeItem(int index); /// Convenience method for toggling the current expanded status. @@ -56,8 +56,11 @@ abstract base class FAccordionController extends ChangeNotifier { /// This method should typically not be called while the widget tree is being rebuilt. Future collapse(int index); - /// Validate if the number of expanded items is within the min and max. + /// Returns true if the number of expanded items is within the allowed range. bool validate(int length); + + /// The currently selected values. + Set get expanded => {..._expanded}; } final class _MultiSelectAccordionController extends FAccordionController { @@ -70,7 +73,10 @@ final class _MultiSelectAccordionController extends FAccordionController { @override void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { controllers[index] = (controller: controller, animation: animation); - if (initiallyExpanded && validate(_expanded.length)) { + if (initiallyExpanded) { + if (_max != null && _expanded.length >= _max) { + return; + } _expanded.add(index); } } @@ -88,27 +94,23 @@ final class _MultiSelectAccordionController extends FAccordionController { @override Future expand(int index) async { - if (_max != null && _expanded.length >= _max) { + if ((_max != null && _expanded.length >= _max) || _expanded.contains(index)) { return; } - _expanded.add(index); - - final controller = controllers[index]?.controller; - await controller?.forward(); + await controllers[index]?.controller.forward(); notifyListeners(); } @override Future collapse(int index) async { - if (_expanded.length <= _min) { + if (_expanded.length <= _min || !_expanded.contains(index)) { return; } _expanded.remove(index); - final controller = controllers[index]?.controller; - await controller?.reverse(); + await controllers[index]?.controller.reverse(); notifyListeners(); } @@ -129,7 +131,10 @@ final class FRadioAccordionController extends FAccordionController { void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { controllers[index] = (controller: controller, animation: animation); - if (initiallyExpanded && validate(_expanded.length)) { + if (initiallyExpanded) { + if (_max != null && _expanded.length >= _max) { + return; + } _expanded.add(index); } } @@ -147,39 +152,43 @@ final class FRadioAccordionController extends FAccordionController { @override Future collapse(int index) async { - if (_expanded.length <= _min) { - return; - } - - _expanded.remove(index); - - final controller = controllers[index]?.controller; - await controller?.reverse(); + await _collapse(index); notifyListeners(); } @override Future expand(int index) async { - final expand = >[]; + final futures = >[]; if (_expanded.length > _min && _max != null && _expanded.length >= _max) { if (_expanded.contains(index)) { return; } - expand.add(collapse(_expanded.first)); + + futures.add(_collapse(_expanded.first)); } _expanded.add(index); - final controller = controllers[index]?.controller; - if (controller != null) { - expand.add(controller.forward()); + final future = controllers[index]?.controller?.forward(); + if (future != null) { + futures.add(future); } - await Future.wait(expand); + await Future.wait(futures); notifyListeners(); } + Future _collapse(int index) async { + if (_expanded.length <= _min || !_expanded.contains(index)) { + return; + } + + _expanded.remove(index); + + await controllers[index]?.controller.reverse(); + } + @override bool validate(int length) => length >= _min && (_max == null || length <= _max); } diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 8985e482d..635ea2479 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -9,39 +9,6 @@ import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; -@internal - -class FAccordionItemData extends InheritedWidget { - @useResult - static FAccordionItemData of(BuildContext context) { - final data = context.dependOnInheritedWidgetOfExactType(); - assert(data != null, 'No FAccordionData found in context'); - return data!; - } - - final int index; - - final FAccordionController controller; - - const FAccordionItemData({ - required this.index, - required this.controller, - required super.child, - super.key, - }); - - @override - bool updateShouldNotify(covariant FAccordionItemData old) => index != old.index || controller != old.controller; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(IntProperty('index', index)) - ..add(DiagnosticsProperty('controller', controller)); - } -} - /// An interactive heading that reveals a section of content. /// /// See: @@ -110,14 +77,12 @@ class _FAccordionItemState extends State with SingleTickerProvid parent: _controller, ), ); - data.controller.addItem(data.index, _controller, _expand, widget.initiallyExpanded); } @override Widget build(BuildContext context) { final FAccordionItemData(:index, :controller) = FAccordionItemData.of(context); - final style = widget.style ?? context.theme.accordionStyle; return AnimatedBuilder( animation: _expand, @@ -166,7 +131,7 @@ class _FAccordionItemState extends State with SingleTickerProvid //TODO: Should I be getting the percentage value from the controller or from its local state? clipper: _Clipper(percentage: _expand.value / 100), child: Padding( - padding: style.contentPadding, + padding: style.childPadding, child: DefaultTextStyle(style: style.childTextStyle, child: widget.child), ), ), @@ -178,6 +143,11 @@ class _FAccordionItemState extends State with SingleTickerProvid ), ); } + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } } class _Expandable extends SingleChildRenderObjectWidget { diff --git a/forui/test/src/widgets/accordion/accordion_golden_test.dart b/forui/test/src/widgets/accordion/accordion_golden_test.dart index a0216ba5f..0dd058154 100644 --- a/forui/test/src/widgets/accordion/accordion_golden_test.dart +++ b/forui/test/src/widgets/accordion/accordion_golden_test.dart @@ -19,7 +19,7 @@ void main() { mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - items: [ + children: [ FAccordionItem( title: Text('Title'), initiallyExpanded: true, @@ -50,7 +50,7 @@ void main() { mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - items: [ + children: [ FAccordionItem( title: Text('Title'), initiallyExpanded: true, diff --git a/forui/test/src/widgets/accordion/accordion_test.dart b/forui/test/src/widgets/accordion/accordion_test.dart index 252e0fe9e..c2aad67e3 100644 --- a/forui/test/src/widgets/accordion/accordion_test.dart +++ b/forui/test/src/widgets/accordion/accordion_test.dart @@ -18,7 +18,7 @@ void main() { home: TestScaffold( data: FThemes.zinc.light, child: FAccordion( - items: [ + children: [ FAccordionItem( title: const Text('Title'), initiallyExpanded: true, diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart index 640fbe87b..c65ee31ea 100644 --- a/samples/lib/widgets/accordion.dart +++ b/samples/lib/widgets/accordion.dart @@ -25,7 +25,7 @@ class AccordionPage extends SampleScaffold { children: [ FAccordion( controller: controller, - items: const [ + children: const [ FAccordionItem( title: Text('Is it accessible?'), child: Text('Yes. It adheres to the WAI-ARIA design pattern.'), From 9d65ee4ddb6fb992130ab8700d23b1b750588080 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Tue, 24 Sep 2024 12:37:58 +0000 Subject: [PATCH 40/57] Commit from GitHub Actions (Forui Presubmit) --- .../src/widgets/accordion/accordion_item.dart | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 635ea2479..44be5e29b 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -9,6 +9,38 @@ import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; +@internal +class FAccordionItemData extends InheritedWidget { + @useResult + static FAccordionItemData of(BuildContext context) { + final data = context.dependOnInheritedWidgetOfExactType(); + assert(data != null, 'No FAccordionData found in context'); + return data!; + } + + final int index; + + final FAccordionController controller; + + const FAccordionItemData({ + required this.index, + required this.controller, + required super.child, + super.key, + }); + + @override + bool updateShouldNotify(covariant FAccordionItemData old) => index != old.index || controller != old.controller; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('index', index)) + ..add(DiagnosticsProperty('controller', controller)); + } +} + /// An interactive heading that reveals a section of content. /// /// See: From 74c7623ec6ef14c1437e23eb8d4e684adc621f99 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Tue, 24 Sep 2024 23:19:46 +0800 Subject: [PATCH 41/57] resolved merge conflicts --- forui/lib/src/widgets/accordion/accordion_controller.dart | 2 +- forui/lib/src/widgets/accordion/accordion_item.dart | 3 +-- forui/lib/widgets/accordion.dart | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index 2c086fb05..34faab9ec 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -170,7 +170,7 @@ final class FRadioAccordionController extends FAccordionController { _expanded.add(index); - final future = controllers[index]?.controller?.forward(); + final future = controllers[index]?.controller.forward(); if (future != null) { futures.add(future); } diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 44be5e29b..0930c7195 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -3,8 +3,6 @@ import 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart'; - import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; @@ -175,6 +173,7 @@ class _FAccordionItemState extends State with SingleTickerProvid ), ); } + @override void dispose() { _controller.dispose(); diff --git a/forui/lib/widgets/accordion.dart b/forui/lib/widgets/accordion.dart index 19d6b371f..a156b637d 100644 --- a/forui/lib/widgets/accordion.dart +++ b/forui/lib/widgets/accordion.dart @@ -5,6 +5,6 @@ /// See https://forui.dev/docs/accordion for working examples. library forui.widgets.accordion; -export '../src/widgets/accordion/accordion.dart'; +export '../src/widgets/accordion/accordion.dart' hide FAccordionItemData; export '../src/widgets/accordion/accordion_controller.dart'; -export '../src/widgets/accordion/accordion_item.dart' hide FAccordionItemData; +export '../src/widgets/accordion/accordion_item.dart'; From 5392c484678236d2f477677c6e819c83fc4e90b9 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Wed, 25 Sep 2024 21:44:08 +0800 Subject: [PATCH 42/57] Tests still need rework --- forui/example/lib/sandbox.dart | 3 +- .../lib/src/widgets/accordion/accordion.dart | 6 +- .../accordion/accordion_controller.dart | 153 +++-------- .../src/widgets/accordion/accordion_item.dart | 55 ++-- .../accordion/accordion_controller_test.dart | 253 ++++++++++++++++++ samples/lib/widgets/accordion.dart | 4 +- 6 files changed, 324 insertions(+), 150 deletions(-) create mode 100644 forui/test/src/widgets/accordion/accordion_controller_test.dart diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index c3188ae92..d7f996633 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -20,10 +20,9 @@ class _SandboxState extends State { @override Widget build(BuildContext context) => Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - controller: FRadioAccordionController(min: 1, max: 3), + controller: FAccordionController(max: 2), children: [ const FAccordionItem( title: Text('Title 1'), diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index cc6878f51..304628dc8 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -18,8 +18,8 @@ class FAccordion extends StatefulWidget { /// The controller. /// /// See: - /// * [FRadioAccordionController] for a radio-like selection. /// * [FAccordionController] for default multiple selections. + /// * [FAccordionController.radio] for a radio-like selection. final FAccordionController? controller; /// The items. @@ -50,7 +50,7 @@ class FAccordion extends StatefulWidget { } class _FAccordionState extends State { - late final FAccordionController _controller; + late FAccordionController _controller; @override void initState() { @@ -58,7 +58,7 @@ class _FAccordionState extends State { _controller = widget.controller ?? FAccordionController(); if (!_controller.validate(widget.children.where((child) => child.initiallyExpanded).length)) { - throw StateError('number of expanded items must be within the min and max.'); + throw StateError('number of expanded items must be within the allowed range.'); } } diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index 34faab9ec..0500a3983 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; /// A controller that controls which sections are shown and hidden. -abstract base class FAccordionController extends ChangeNotifier { +class FAccordionController extends ChangeNotifier { /// The duration of the expansion and collapse animations. final Duration animationDuration; @@ -11,8 +11,11 @@ abstract base class FAccordionController extends ChangeNotifier { final int _min; final int? _max; - /// Creates a multi-select [FAccordionController]. - factory FAccordionController({int min, int? max, Duration animationDuration}) = _MultiSelectAccordionController; + /// An [FAccordionController] that allows only one section to be expanded at a time. + factory FAccordionController.radio({Duration? animationDuration}) => FAccordionController( + max: 1, + animationDuration: animationDuration ?? const Duration(milliseconds: 100), + ); /// Creates a base [FAccordionController]. /// @@ -22,10 +25,10 @@ abstract base class FAccordionController extends ChangeNotifier { /// * Throws [AssertionError] if [min] < 0. /// * Throws [AssertionError] if [max] < 0. /// * Throws [AssertionError] if [min] > [max]. - FAccordionController.base({ + FAccordionController({ int min = 0, int? max, - this.animationDuration = const Duration(milliseconds: 500), + this.animationDuration = const Duration(milliseconds: 100), }) : _min = min, _max = max, controllers = {}, @@ -35,44 +38,9 @@ abstract base class FAccordionController extends ChangeNotifier { assert(max == null || min <= max, 'The max value must be greater than or equal to the min value.'); /// Adds an item to the accordion. - // ignore: avoid_positional_boolean_parameters - void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded); - - /// Removes an item from the accordion. Returns true if the item was removed. - bool removeItem(int index); - - /// Convenience method for toggling the current expanded status. - /// - /// This method should typically not be called while the widget tree is being rebuilt. - Future toggle(int index); - - /// Shows the content in the accordion. - /// - /// This method should typically not be called while the widget tree is being rebuilt. - Future expand(int index); - - /// Hides the content in the accordion. - /// - /// This method should typically not be called while the widget tree is being rebuilt. - Future collapse(int index); - - /// Returns true if the number of expanded items is within the allowed range. - bool validate(int length); - - /// The currently selected values. - Set get expanded => {..._expanded}; -} - -final class _MultiSelectAccordionController extends FAccordionController { - _MultiSelectAccordionController({ - super.min, - super.max, - super.animationDuration, - }) : super.base(); - - @override - void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { + void addItem(int index, AnimationController controller, Animation animation, {required bool initiallyExpanded}) { controllers[index] = (controller: controller, animation: animation); + if (initiallyExpanded) { if (_max != null && _expanded.length >= _max) { return; @@ -81,7 +49,7 @@ final class _MultiSelectAccordionController extends FAccordionController { } } - @override + /// Removes an item from the accordion. Returns true if the item was removed. bool removeItem(int index) { final removed = controllers.remove(index); _expanded.remove(index); @@ -89,82 +57,28 @@ final class _MultiSelectAccordionController extends FAccordionController { return removed != null; } - @override - Future toggle(int index) async => controllers[index]?.animation.value == 100 ? collapse(index) : expand(index); - - @override - Future expand(int index) async { - if ((_max != null && _expanded.length >= _max) || _expanded.contains(index)) { - return; - } - _expanded.add(index); - await controllers[index]?.controller.forward(); - notifyListeners(); - } + /// Convenience method for toggling the current expanded status. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future toggle(int index) async { + final value = controllers[index]?.animation.value; - @override - Future collapse(int index) async { - if (_expanded.length <= _min || !_expanded.contains(index)) { + if (value == null) { return; } - _expanded.remove(index); - - await controllers[index]?.controller.reverse(); - notifyListeners(); - } - - @override - bool validate(int length) => length >= _min && (_max == null || length <= _max); -} - -/// An [FAccordionController] that allows only one section to be expanded at a time. -final class FRadioAccordionController extends FAccordionController { - /// Creates a [FRadioAccordionController]. - FRadioAccordionController({ - super.animationDuration, - super.min, - int super.max = 1, - }) : super.base(); - - @override - void addItem(int index, AnimationController controller, Animation animation, bool initiallyExpanded) { - controllers[index] = (controller: controller, animation: animation); - - if (initiallyExpanded) { - if (_max != null && _expanded.length >= _max) { - return; - } - _expanded.add(index); - } - } - - @override - bool removeItem(int index) { - final removed = controllers.remove(index); - _expanded.remove(index); - - return removed != null; - } - - @override - Future toggle(int index) async => controllers[index]?.animation.value == 100 ? collapse(index) : expand(index); - - @override - Future collapse(int index) async { - await _collapse(index); - notifyListeners(); + value == 100 ? await collapse(index) : await expand(index); } - @override + /// Shows the content in the accordion. + /// + /// This method should typically not be called while the widget tree is being rebuilt. Future expand(int index) async { + if (_expanded.contains(index)) { + return; + } final futures = >[]; - if (_expanded.length > _min && _max != null && _expanded.length >= _max) { - if (_expanded.contains(index)) { - return; - } - futures.add(_collapse(_expanded.first)); } @@ -179,16 +93,29 @@ final class FRadioAccordionController extends FAccordionController { notifyListeners(); } - Future _collapse(int index) async { + Future _collapse(int index) async { if (_expanded.length <= _min || !_expanded.contains(index)) { - return; + return false; } _expanded.remove(index); await controllers[index]?.controller.reverse(); + return true; } - @override + /// Hides the content in the accordion. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future collapse(int index) async { + if (await _collapse(index)) { + notifyListeners(); + } + } + + /// Returns true if the number of expanded items is within the allowed range. bool validate(int length) => length >= _min && (_max == null || length <= _max); + + /// The currently selected values. + Set get expanded => {..._expanded}; } diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 0930c7195..a0ff629da 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; +import 'package:forui/src/widgets/accordion/accordion.dart'; @internal class FAccordionItemData extends InheritedWidget { @@ -80,9 +81,8 @@ class FAccordionItem extends StatefulWidget { } class _FAccordionItemState extends State with SingleTickerProviderStateMixin { - late AnimationController _controller; + late final AnimationController _controller; late Animation _expand; - bool _hovered = false; @override void didChangeDependencies() { @@ -107,7 +107,7 @@ class _FAccordionItemState extends State with SingleTickerProvid parent: _controller, ), ); - data.controller.addItem(data.index, _controller, _expand, widget.initiallyExpanded); + data.controller.addItem(data.index, _controller, _expand, initiallyExpanded: widget.initiallyExpanded); } @override @@ -120,45 +120,40 @@ class _FAccordionItemState extends State with SingleTickerProvid crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FTappable( + behavior: HitTestBehavior.translucent, onPress: () => controller.toggle(index), - child: MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: Container( - padding: style.titlePadding, - child: Row( - children: [ - Expanded( - child: merge( - // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. - textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, - ), - style: style.titleTextStyle - .copyWith(decoration: _hovered ? TextDecoration.underline : TextDecoration.none), - child: widget.title, + builder: (context, state, child) => Container( + padding: style.titlePadding, + child: Row( + children: [ + Expanded( + child: merge( + // TODO: replace with DefaultTextStyle.merge when textHeightBehavior has been added. + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, ), + style: style.titleTextStyle.copyWith( + decoration: state.hovered || state.longPressed ? TextDecoration.underline : TextDecoration.none, + ), + child: widget.title, ), - Transform.rotate( - //TODO: Should I be getting the percentage value from the controller or from its local state? - angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, - - child: style.icon, - ), - ], - ), + ), + child!, + ], ), ), + child: Transform.rotate( + angle: (_expand.value / 100 * -180 + 90) * math.pi / 180.0, + child: style.icon, + ), ), // We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by // RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the // child to layout at a smaller size while the amount of padding remains the same. _Expandable( - //TODO: Should I be getting the percentage value from the controller or from its local state? percentage: _expand.value / 100, child: ClipRect( - //TODO: Should I be getting the percentage value from the controller or from its local state? clipper: _Clipper(percentage: _expand.value / 100), child: Padding( padding: style.childPadding, diff --git a/forui/test/src/widgets/accordion/accordion_controller_test.dart b/forui/test/src/widgets/accordion/accordion_controller_test.dart new file mode 100644 index 000000000..2dcef13ab --- /dev/null +++ b/forui/test/src/widgets/accordion/accordion_controller_test.dart @@ -0,0 +1,253 @@ +import 'package:flutter/animation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:forui/forui.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'accordion_controller_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +@GenerateNiceMocks([MockSpec()]) +void main() { + group('FAccordionController', () { + late FAccordionController controller; + late AnimationController animationController; + late Animation animation; + int count = 0; + + setUp(() { + count = 0; + animationController = MockAnimationController(); + animation = Tween(begin: 0, end: 100).animate(animationController); + when(animationController.forward()).thenAnswer((_) { + when(animationController.value).thenReturn(1.0); + return TickerFuture.complete(); + }); + when(animationController.reverse()).thenAnswer((_) { + when(animationController.value).thenReturn(0.0); + return TickerFuture.complete(); + }); + + controller = FAccordionController(min: 1, max: 3) + ..addListener(() { + count++; + }); + }); + + tearDown(() { + animationController.dispose(); + controller.dispose(); + }); + + test('addItem(...)', () { + controller.addItem(0, animationController, animation, initiallyExpanded: false); + expect(controller.expanded.length, 0); + controller.addItem(0, animationController, animation, initiallyExpanded: true); + expect(controller.expanded.length, 1); + + controller.addItem(0, animationController, animation, initiallyExpanded: true); + expect(controller.expanded.length, 1); + + controller + ..addItem(1, animationController, animation, initiallyExpanded: true) + ..addItem(2, animationController, animation, initiallyExpanded: true) + ..addItem(3, animationController, animation, initiallyExpanded: true); + expect(controller.expanded.length, 3); + expect(controller.expanded.last, 2); + + controller.addItem(3, animationController, animation, initiallyExpanded: false); + expect(controller.expanded.length, 3); + expect(controller.controllers.length, 4); + }); + + test('removeItem(...)', () { + controller + ..addItem(0, animationController, animation, initiallyExpanded: true) + ..addItem(1, animationController, animation, initiallyExpanded: false) + ..addItem(2, animationController, animation, initiallyExpanded: true) + ..addItem(3, animationController, animation, initiallyExpanded: true); + expect(controller.removeItem(0), true); + expect(controller.removeItem(4), false); + expect(controller.removeItem(1), true); + expect(controller.expanded.contains(1), false); + expect(controller.removeItem(2), true); + }); + + test('toggle(...)', () async { + await animationController.forward(); + controller.addItem(0, animationController, animation, initiallyExpanded: true); + await controller.toggle(0); + expect(controller.controllers[0]?.animation.value, 100); + controller.addItem(1, animationController, animation, initiallyExpanded: true); + await controller.toggle(0); + expect(controller.controllers[0]?.animation.value, 0); + }); + + test('expand(...)', () async { + controller + ..addItem(0, animationController, animation, initiallyExpanded: false) + ..addItem(1, animationController, animation, initiallyExpanded: false) + ..addItem(2, animationController, animation, initiallyExpanded: false) + ..addItem(3, animationController, animation, initiallyExpanded: true); + await controller.expand(0); + expect(controller.expanded, {3, 0}); + await controller.expand(0); + expect(count, 1); + await controller.expand(1); + await controller.expand(2); + expect(controller.expanded, {0, 1, 2}); + }); + + test('collapse(...)', () async { + controller + ..addItem(0, animationController, animation, initiallyExpanded: false) + ..addItem(1, animationController, animation, initiallyExpanded: false) + ..addItem(2, animationController, animation, initiallyExpanded: false) + ..addItem(3, animationController, animation, initiallyExpanded: true); + await controller.collapse(3); + expect(controller.expanded.contains(3), true); + expect(count, 0); + await controller.expand(0); + await controller.collapse(3); + await controller.collapse(2); + expect(count, 2); + await controller.collapse(0); + expect(controller.expanded, {0}); + }); + + // TODO: needs rework + test('validate(...)', () { + expect(controller.validate(controller.expanded.length), false); + + controller.addItem(0, animationController, animation, initiallyExpanded: true); + expect(controller.validate(controller.expanded.length), true); + + controller + ..addItem(1, animationController, animation, initiallyExpanded: true) + ..addItem(2, animationController, animation, initiallyExpanded: true); + expect(controller.validate(controller.expanded.length), true); + + controller.addItem(3, animationController, animation, initiallyExpanded: true); + expect(controller.validate(controller.expanded.length), true); + }); + }); + + group('FAccordionController.radio', () { + late FAccordionController controller; + final List animationControllers = []; + final List> animations = []; + int count = 0; + + setUp(() { + count = 0; + animations.clear(); + animationControllers.clear(); + + for (int i = 0; i < 4; i++) { + animationControllers.add(MockAnimationController()); + animations.add(Tween(begin: 0, end: 100).animate(animationControllers[i])); + when(animationControllers[i].forward()).thenAnswer((_) { + when(animationControllers[i].value).thenReturn(1.0); + return TickerFuture.complete(); + }); + when(animationControllers[i].reverse()).thenAnswer((_) { + when(animationControllers[i].value).thenReturn(0.0); + return TickerFuture.complete(); + }); + } + + controller = FAccordionController.radio() + ..addListener(() { + count++; + }); + }); + + tearDown(() { + for (int i = 0; i < 4; i++) { + animationControllers[i].dispose(); + } + controller.dispose(); + }); + + test('addItem(...)', () { + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + expect(controller.expanded.length, 0); + expect(controller.controllers.length, 1); + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + expect(controller.expanded.length, 1); + expect(controller.controllers.length, 1); + + controller + ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: true) + ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) + ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); + expect(controller.expanded.length, 1); + + // addItem() doesn't know if the total length of initialExpanded items is within the allowed range + expect(controller.expanded, {3}); + }); + + test('removeItem(...)', () { + controller + ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: true) + ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) + ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) + ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); + expect(controller.removeItem(0), true); + expect(controller.removeItem(4), false); + expect(controller.removeItem(1), true); + expect(controller.expanded.contains(1), false); + expect(controller.removeItem(2), true); + + // removeItem() doesn't know if the total length of initialExpanded items is within the allowed range + + }); + + test('toggle(...)', () async { + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + await controller.toggle(0); + expect(controller.controllers[0]?.animation.value, 100); + await controller.toggle(1); + expect(count, 1); + + controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: false); + + await controller.toggle(1); + expect(controller.controllers[0]?.animation.value, 0); + }); + + test('expand(...)', () async { + controller + ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: false) + ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) + ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: false) + ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); + await controller.expand(0); + expect(controller.expanded, {0}); + await controller.expand(0); + expect(count, 1); + await controller.expand(1); + await controller.expand(2); + expect(controller.expanded, {2}); + }); + + test('collapse(...)', () async { + controller + ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: false) + ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) + ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: false) + ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); + await controller.collapse(3); + expect(controller.expanded.isEmpty, true); + await controller.expand(0); + await controller.collapse(3); + await controller.collapse(2); + expect(count, 2); + expect(controller.expanded, {0}); + }); + + // TODO: needs rework + test('validate(...)', () {}); + }); +} diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart index c65ee31ea..0a2dca2c1 100644 --- a/samples/lib/widgets/accordion.dart +++ b/samples/lib/widgets/accordion.dart @@ -7,7 +7,7 @@ import 'package:forui_samples/sample_scaffold.dart'; final controllers = { 'default': FAccordionController(), - 'radio': FRadioAccordionController(), + 'radio': FAccordionController.radio(), }; @RoutePage() @@ -17,7 +17,7 @@ class AccordionPage extends SampleScaffold { AccordionPage({ @queryParam super.theme, @queryParam String controller = 'default', - }) : controller = controllers[controller] ?? FRadioAccordionController(); + }) : controller = controllers[controller] ?? FAccordionController(); @override Widget child(BuildContext context) => Column( From 5fb73035f0e86860d6a84b842d3ed7c6b912ee50 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Wed, 25 Sep 2024 13:45:38 +0000 Subject: [PATCH 43/57] Commit from GitHub Actions (Forui Presubmit) --- .../src/widgets/accordion/accordion_controller_test.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/forui/test/src/widgets/accordion/accordion_controller_test.dart b/forui/test/src/widgets/accordion/accordion_controller_test.dart index 2dcef13ab..c1d1a493d 100644 --- a/forui/test/src/widgets/accordion/accordion_controller_test.dart +++ b/forui/test/src/widgets/accordion/accordion_controller_test.dart @@ -1,10 +1,10 @@ import 'package:flutter/animation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:forui/forui.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:forui/forui.dart'; import 'accordion_controller_test.mocks.dart'; @GenerateNiceMocks([MockSpec()]) @@ -201,7 +201,6 @@ void main() { expect(controller.removeItem(2), true); // removeItem() doesn't know if the total length of initialExpanded items is within the allowed range - }); test('toggle(...)', () async { From b9f4a83dd4473ac3715d68307214592634b2c35d Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Fri, 27 Sep 2024 13:10:09 +0800 Subject: [PATCH 44/57] Accordion ready for review, tests implemented --- docs/pages/docs/accordion.mdx | 47 ++++++- .../accordion/accordion_controller.dart | 7 +- .../src/widgets/accordion/accordion_item.dart | 10 +- .../accordion/accordion_controller_test.dart | 118 +++++++++--------- samples/lib/widgets/accordion.dart | 49 ++++---- 5 files changed, 138 insertions(+), 93 deletions(-) diff --git a/docs/pages/docs/accordion.mdx b/docs/pages/docs/accordion.mdx index 33a1e6890..c8090b4ca 100644 --- a/docs/pages/docs/accordion.mdx +++ b/docs/pages/docs/accordion.mdx @@ -12,7 +12,7 @@ A vertically stacked set of interactive headings that each reveal a section of c - + ```dart @@ -20,6 +20,7 @@ A vertically stacked set of interactive headings that each reveal a section of c mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( + controller: FAccordionController(max: 2), items: [ FAccordionItem( title: const Text('Is it accessible?'), @@ -52,7 +53,7 @@ A vertically stacked set of interactive headings that each reveal a section of c ```dart FAccordion( - controller: FAccordionController(), // or FRadioAccordionController() + controller: FAccordionController(min: 1, max: 2), // or FAccordionController.radio() items: [ FAccordionItem( title: const Text('Is it accessible?'), @@ -70,12 +71,50 @@ FAccordion( - ```dart + ```dart {5} + const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FAccordion( + controller: FAccordionController.radio(), + items: [ + FAccordionItem( + title: const Text('Is it accessible?'), + child: const Text('Yes. It adheres to the WAI-ARIA design pattern.'), + ), + FAccordionItem( + title: const Text('Is it Styled?'), + initiallyExpanded: true, + child: const Text( + "Yes. It comes with default styles that matches the other components' aesthetics", + ), + ), + FAccordionItem( + title: const Text('Is it Animated?'), + child: const Text( + 'Yes. It is animated by default, but you can disable it if you prefer', + ), + ), + ], + ), + ], + ); + ``` + + + +### With Multi-select Behaviour + + + + + + ```dart {5} const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FAccordion( - controller: FRadioAccordionController(), + controller: FAccordionController(), items: [ FAccordionItem( title: const Text('Is it accessible?'), diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index 0500a3983..d03462d82 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -14,7 +14,7 @@ class FAccordionController extends ChangeNotifier { /// An [FAccordionController] that allows only one section to be expanded at a time. factory FAccordionController.radio({Duration? animationDuration}) => FAccordionController( max: 1, - animationDuration: animationDuration ?? const Duration(milliseconds: 100), + animationDuration: animationDuration ?? const Duration(milliseconds: 200), ); /// Creates a base [FAccordionController]. @@ -39,6 +39,10 @@ class FAccordionController extends ChangeNotifier { /// Adds an item to the accordion. void addItem(int index, AnimationController controller, Animation animation, {required bool initiallyExpanded}) { + controller + ..value = initiallyExpanded ? 1 : 0 + ..duration = animationDuration; + controllers[index] = (controller: controller, animation: animation); if (initiallyExpanded) { @@ -53,7 +57,6 @@ class FAccordionController extends ChangeNotifier { bool removeItem(int index) { final removed = controllers.remove(index); _expanded.remove(index); - return removed != null; } diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index a0ff629da..ff2d7fc0a 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -80,8 +80,8 @@ class FAccordionItem extends StatefulWidget { } } -class _FAccordionItemState extends State with SingleTickerProviderStateMixin { - late final AnimationController _controller; +class _FAccordionItemState extends State with TickerProviderStateMixin { + late AnimationController _controller; late Animation _expand; @override @@ -93,11 +93,7 @@ class _FAccordionItemState extends State with SingleTickerProvid _controller.dispose(); } - _controller = AnimationController( - duration: data.controller.animationDuration, - value: widget.initiallyExpanded ? 1.0 : 0.0, - vsync: this, - ); + _controller = AnimationController(vsync: this); _expand = Tween( begin: 0, end: 100, diff --git a/forui/test/src/widgets/accordion/accordion_controller_test.dart b/forui/test/src/widgets/accordion/accordion_controller_test.dart index c1d1a493d..8a1781d99 100644 --- a/forui/test/src/widgets/accordion/accordion_controller_test.dart +++ b/forui/test/src/widgets/accordion/accordion_controller_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/animation.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -12,22 +13,27 @@ import 'accordion_controller_test.mocks.dart'; void main() { group('FAccordionController', () { late FAccordionController controller; - late AnimationController animationController; - late Animation animation; + final List animationControllers = []; + final List> animations = []; int count = 0; setUp(() { count = 0; - animationController = MockAnimationController(); - animation = Tween(begin: 0, end: 100).animate(animationController); - when(animationController.forward()).thenAnswer((_) { - when(animationController.value).thenReturn(1.0); - return TickerFuture.complete(); - }); - when(animationController.reverse()).thenAnswer((_) { - when(animationController.value).thenReturn(0.0); - return TickerFuture.complete(); - }); + animations.clear(); + animationControllers.clear(); + + for (int i = 0; i < 4; i++) { + animationControllers.add(MockAnimationController()); + animations.add(Tween(begin: 0, end: 100).animate(animationControllers[i])); + when(animationControllers[i].forward()).thenAnswer((_) { + when(animationControllers[i].value).thenReturn(1.0); + return TickerFuture.complete(); + }); + when(animationControllers[i].reverse()).thenAnswer((_) { + when(animationControllers[i].value).thenReturn(0.0); + return TickerFuture.complete(); + }); + } controller = FAccordionController(min: 1, max: 3) ..addListener(() { @@ -36,37 +42,39 @@ void main() { }); tearDown(() { - animationController.dispose(); + for (int i = 0; i < 4; i++) { + animationControllers[i].dispose(); + } controller.dispose(); }); test('addItem(...)', () { - controller.addItem(0, animationController, animation, initiallyExpanded: false); + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); expect(controller.expanded.length, 0); - controller.addItem(0, animationController, animation, initiallyExpanded: true); + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); expect(controller.expanded.length, 1); - controller.addItem(0, animationController, animation, initiallyExpanded: true); + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); expect(controller.expanded.length, 1); controller - ..addItem(1, animationController, animation, initiallyExpanded: true) - ..addItem(2, animationController, animation, initiallyExpanded: true) - ..addItem(3, animationController, animation, initiallyExpanded: true); + ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: true) + ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) + ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); expect(controller.expanded.length, 3); expect(controller.expanded.last, 2); - controller.addItem(3, animationController, animation, initiallyExpanded: false); + controller.addItem(3, animationControllers[3], animations[3], initiallyExpanded: false); expect(controller.expanded.length, 3); expect(controller.controllers.length, 4); }); test('removeItem(...)', () { controller - ..addItem(0, animationController, animation, initiallyExpanded: true) - ..addItem(1, animationController, animation, initiallyExpanded: false) - ..addItem(2, animationController, animation, initiallyExpanded: true) - ..addItem(3, animationController, animation, initiallyExpanded: true); + ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: true) + ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) + ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) + ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); expect(controller.removeItem(0), true); expect(controller.removeItem(4), false); expect(controller.removeItem(1), true); @@ -75,21 +83,28 @@ void main() { }); test('toggle(...)', () async { - await animationController.forward(); - controller.addItem(0, animationController, animation, initiallyExpanded: true); + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); await controller.toggle(0); expect(controller.controllers[0]?.animation.value, 100); - controller.addItem(1, animationController, animation, initiallyExpanded: true); - await controller.toggle(0); + await controller.toggle(1); + expect(count, 1); + + controller + ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) + ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) + ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: false); + await controller.toggle(1); + expect(controller.controllers[1]?.animation.value, 100); + await controller.toggle(3); expect(controller.controllers[0]?.animation.value, 0); }); test('expand(...)', () async { controller - ..addItem(0, animationController, animation, initiallyExpanded: false) - ..addItem(1, animationController, animation, initiallyExpanded: false) - ..addItem(2, animationController, animation, initiallyExpanded: false) - ..addItem(3, animationController, animation, initiallyExpanded: true); + ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: false) + ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) + ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: false) + ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); await controller.expand(0); expect(controller.expanded, {3, 0}); await controller.expand(0); @@ -101,10 +116,10 @@ void main() { test('collapse(...)', () async { controller - ..addItem(0, animationController, animation, initiallyExpanded: false) - ..addItem(1, animationController, animation, initiallyExpanded: false) - ..addItem(2, animationController, animation, initiallyExpanded: false) - ..addItem(3, animationController, animation, initiallyExpanded: true); + ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: false) + ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) + ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: false) + ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); await controller.collapse(3); expect(controller.expanded.contains(3), true); expect(count, 0); @@ -116,20 +131,12 @@ void main() { expect(controller.expanded, {0}); }); - // TODO: needs rework test('validate(...)', () { - expect(controller.validate(controller.expanded.length), false); - - controller.addItem(0, animationController, animation, initiallyExpanded: true); - expect(controller.validate(controller.expanded.length), true); - - controller - ..addItem(1, animationController, animation, initiallyExpanded: true) - ..addItem(2, animationController, animation, initiallyExpanded: true); - expect(controller.validate(controller.expanded.length), true); - - controller.addItem(3, animationController, animation, initiallyExpanded: true); - expect(controller.validate(controller.expanded.length), true); + expect(controller.validate(0), false); + expect(controller.validate(1), true); + expect(controller.validate(2), true); + expect(controller.validate(3), true); + expect(controller.validate(4), false); }); }); @@ -183,9 +190,6 @@ void main() { ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); expect(controller.expanded.length, 1); - - // addItem() doesn't know if the total length of initialExpanded items is within the allowed range - expect(controller.expanded, {3}); }); test('removeItem(...)', () { @@ -199,8 +203,6 @@ void main() { expect(controller.removeItem(1), true); expect(controller.expanded.contains(1), false); expect(controller.removeItem(2), true); - - // removeItem() doesn't know if the total length of initialExpanded items is within the allowed range }); test('toggle(...)', () async { @@ -246,7 +248,11 @@ void main() { expect(controller.expanded, {0}); }); - // TODO: needs rework - test('validate(...)', () {}); + test('validate(...)', () { + expect(controller.validate(0), true); + expect(controller.validate(1), true); + expect(controller.validate(2), false); + expect(controller.validate(3), false); + }); }); } diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart index 0a2dca2c1..7bbbfd5b7 100644 --- a/samples/lib/widgets/accordion.dart +++ b/samples/lib/widgets/accordion.dart @@ -7,6 +7,7 @@ import 'package:forui_samples/sample_scaffold.dart'; final controllers = { 'default': FAccordionController(), + 'default-max': FAccordionController(max: 2), 'radio': FAccordionController.radio(), }; @@ -21,30 +22,30 @@ class AccordionPage extends SampleScaffold { @override Widget child(BuildContext context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FAccordion( - controller: controller, - children: const [ - FAccordionItem( - title: Text('Is it accessible?'), - child: Text('Yes. It adheres to the WAI-ARIA design pattern.'), - ), - FAccordionItem( - title: Text('Is it Styled?'), - initiallyExpanded: true, - child: Text( - "Yes. It comes with default styles that matches the other components' aesthetics", - ), - ), - FAccordionItem( - title: Text('Is it Animated?'), - child: Text( - 'Yes. It is animated by default, but you can disable it if you prefer', - ), - ), - ], + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FAccordion( + controller: controller, + children: const [ + FAccordionItem( + title: Text('Is it accessible?'), + child: Text('Yes. It adheres to the WAI-ARIA design pattern.'), + ), + FAccordionItem( + title: Text('Is it Styled?'), + initiallyExpanded: true, + child: Text( + "Yes. It comes with default styles that matches the other components' aesthetics", + ), + ), + FAccordionItem( + title: Text('Is it Animated?'), + child: Text( + 'Yes. It is animated by default, but you can disable it if you prefer', + ), ), ], - ); + ), + ], + ); } From a17189c139c1b78ee06f1b9e56ab2ea734ca8359 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Fri, 27 Sep 2024 05:11:39 +0000 Subject: [PATCH 45/57] Commit from GitHub Actions (Forui Samples Presubmit) --- samples/lib/widgets/accordion.dart | 48 +++++++++++++++--------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/samples/lib/widgets/accordion.dart b/samples/lib/widgets/accordion.dart index 7bbbfd5b7..3567e32c6 100644 --- a/samples/lib/widgets/accordion.dart +++ b/samples/lib/widgets/accordion.dart @@ -22,30 +22,30 @@ class AccordionPage extends SampleScaffold { @override Widget child(BuildContext context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FAccordion( - controller: controller, - children: const [ - FAccordionItem( - title: Text('Is it accessible?'), - child: Text('Yes. It adheres to the WAI-ARIA design pattern.'), - ), - FAccordionItem( - title: Text('Is it Styled?'), - initiallyExpanded: true, - child: Text( - "Yes. It comes with default styles that matches the other components' aesthetics", - ), - ), - FAccordionItem( - title: Text('Is it Animated?'), - child: Text( - 'Yes. It is animated by default, but you can disable it if you prefer', - ), + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FAccordion( + controller: controller, + children: const [ + FAccordionItem( + title: Text('Is it accessible?'), + child: Text('Yes. It adheres to the WAI-ARIA design pattern.'), + ), + FAccordionItem( + title: Text('Is it Styled?'), + initiallyExpanded: true, + child: Text( + "Yes. It comes with default styles that matches the other components' aesthetics", + ), + ), + FAccordionItem( + title: Text('Is it Animated?'), + child: Text( + 'Yes. It is animated by default, but you can disable it if you prefer', + ), + ), + ], ), ], - ), - ], - ); + ); } From a1aa6f162314d0d12c7be34a2868c3bf1d19e830 Mon Sep 17 00:00:00 2001 From: Pante Date: Fri, 27 Sep 2024 14:35:44 +0000 Subject: [PATCH 46/57] Commit from GitHub Actions (Forui Samples Presubmit) --- samples/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/pubspec.lock b/samples/pubspec.lock index 2ccf1e8d2..24be77b54 100644 --- a/samples/pubspec.lock +++ b/samples/pubspec.lock @@ -272,7 +272,7 @@ packages: path: "../forui" relative: true source: path - version: "0.5.0" + version: "0.5.1" forui_assets: dependency: "direct overridden" description: From 8e3cc8a3604a03135c84b863071412e24466e109 Mon Sep 17 00:00:00 2001 From: Pante Date: Fri, 27 Sep 2024 14:35:51 +0000 Subject: [PATCH 47/57] Commit from GitHub Actions (Forui Presubmit) --- forui/example/pubspec.lock | 2 +- forui/lib/src/widgets/accordion/accordion_item.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/forui/example/pubspec.lock b/forui/example/pubspec.lock index dd43f20f3..3569e2b1e 100644 --- a/forui/example/pubspec.lock +++ b/forui/example/pubspec.lock @@ -243,7 +243,7 @@ packages: path: ".." relative: true source: path - version: "0.5.0" + version: "0.5.1" forui_assets: dependency: transitive description: diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index ff2d7fc0a..16ea61862 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -6,7 +6,6 @@ import 'package:flutter/widgets.dart'; import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; -import 'package:forui/src/widgets/accordion/accordion.dart'; @internal class FAccordionItemData extends InheritedWidget { From 01ebe958b8765653b003d5c560afbaaee1fae04b Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Mon, 30 Sep 2024 00:33:29 +0800 Subject: [PATCH 48/57] Apply suggestions from code review Co-authored-by: Matthias Ngeo --- forui/lib/src/widgets/accordion/accordion.dart | 10 +++++----- .../src/widgets/accordion/accordion_controller.dart | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index 304628dc8..1554d217e 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -8,12 +8,12 @@ import 'package:forui/forui.dart'; /// A vertically stacked set of interactive headings that each reveal a section of content. /// -/// Typically used to group multiple [FAccordionItem]s. /// /// See: /// * https://forui.dev/docs/FAccordion for working examples. /// * [FAccordionController] for customizing the accordion's selection behavior. -/// * [FAccordionStyle] for customizing a select group's appearance. +/// * [FAccordionItem] for adding items to an accordion. +/// * [FAccordionStyle] for customizing an accordion's appearance. class FAccordion extends StatefulWidget { /// The controller. /// @@ -88,9 +88,9 @@ class _FAccordionState extends State { ); } -/// The [FAccordion] style. +/// The [FAccordion]'s style. final class FAccordionStyle with Diagnosticable { - /// The title's text style. + /// The title's default text style. final TextStyle titleTextStyle; /// The child's default text style. @@ -192,7 +192,7 @@ class FAccordionItemData extends InheritedWidget { @useResult static FAccordionItemData of(BuildContext context) { final data = context.dependOnInheritedWidgetOfExactType(); - assert(data != null, 'No FAccordionData found in context'); + assert(data != null, 'No FAccordionItemData found in context'); return data!; } diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index d03462d82..b56ecffbe 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -2,10 +2,11 @@ import 'package:flutter/widgets.dart'; /// A controller that controls which sections are shown and hidden. class FAccordionController extends ChangeNotifier { - /// The duration of the expansion and collapse animations. + /// The duration of the expanding and collapsing animations. final Duration animationDuration; - /// A list of controllers for each of the headers in the accordion. + /// The animation controllers for each of the sections in the accordion. + @internal final Map controllers; final Set _expanded; final int _min; @@ -17,7 +18,7 @@ class FAccordionController extends ChangeNotifier { animationDuration: animationDuration ?? const Duration(milliseconds: 200), ); - /// Creates a base [FAccordionController]. + /// Creates a [FAccordionController]. /// /// The [min] and [max] values are the minimum and maximum number of selections allowed. Defaults to no minimum and maximum. /// @@ -53,14 +54,14 @@ class FAccordionController extends ChangeNotifier { } } - /// Removes an item from the accordion. Returns true if the item was removed. + /// Removes the item at the given [index] from the accordion. Returns true if the item was removed. bool removeItem(int index) { final removed = controllers.remove(index); _expanded.remove(index); return removed != null; } - /// Convenience method for toggling the current expanded status. + /// Convenience method for toggling the current expansion status. /// /// This method should typically not be called while the widget tree is being rebuilt. Future toggle(int index) async { From 0944ed555f5a701462b7aea3e8e1e7aa9cc5c299 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Mon, 30 Sep 2024 00:35:19 +0800 Subject: [PATCH 49/57] Apply suggestions from code review Co-authored-by: Matthias Ngeo --- forui/lib/src/widgets/accordion/accordion_controller.dart | 7 ++++--- forui/lib/src/widgets/accordion/accordion_item.dart | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index b56ecffbe..40161bc3c 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -74,15 +74,16 @@ class FAccordionController extends ChangeNotifier { value == 100 ? await collapse(index) : await expand(index); } - /// Shows the content in the accordion. + /// Expands the item at the given [index]. /// /// This method should typically not be called while the widget tree is being rebuilt. Future expand(int index) async { if (_expanded.contains(index)) { return; } + final futures = >[]; - if (_expanded.length > _min && _max != null && _expanded.length >= _max) { + if (_max != null && _expanded.length >= _max) { futures.add(_collapse(_expanded.first)); } @@ -108,7 +109,7 @@ class FAccordionController extends ChangeNotifier { return true; } - /// Hides the content in the accordion. + /// Collapses the item at the given [index]. /// /// This method should typically not be called while the widget tree is being rebuilt. Future collapse(int index) async { diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 16ea61862..9984ab25a 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -73,7 +73,6 @@ class FAccordionItem extends StatefulWidget { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('style', style)) - ..add(DiagnosticsProperty('title', title)) ..add(DiagnosticsProperty('initiallyExpanded', initiallyExpanded)) ..add(DiagnosticsProperty('child', child)); } From 78a736edf68253f143333d937701c054b72100c2 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Sun, 29 Sep 2024 16:36:39 +0000 Subject: [PATCH 50/57] Commit from GitHub Actions (Forui Presubmit) --- forui/lib/src/widgets/accordion/accordion_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index 40161bc3c..25248a482 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -81,7 +81,7 @@ class FAccordionController extends ChangeNotifier { if (_expanded.contains(index)) { return; } - + final futures = >[]; if (_max != null && _expanded.length >= _max) { futures.add(_collapse(_expanded.first)); From d39092f37507697f5897dbf6b709edbf3c7d24a9 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Mon, 30 Sep 2024 01:05:27 +0800 Subject: [PATCH 51/57] left to edit controller tests --- .../lib/src/widgets/accordion/accordion.dart | 2 +- .../accordion/accordion_controller.dart | 21 +++-- .../src/widgets/accordion/accordion_item.dart | 55 +++----------- .../accordion/accordion_controller_test.dart | 66 +++++++--------- .../accordion/accordion_golden_test.dart | 76 +++++++++---------- .../src/widgets/accordion/accordion_test.dart | 3 - 6 files changed, 84 insertions(+), 139 deletions(-) diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index 1554d217e..ebea24616 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -103,7 +103,7 @@ final class FAccordionStyle with Diagnosticable { final EdgeInsets childPadding; /// The icon. - final SvgPicture icon; + final Widget icon; /// The divider's color. final Color dividerColor; diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index 40161bc3c..3a8b1f121 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -6,7 +6,6 @@ class FAccordionController extends ChangeNotifier { final Duration animationDuration; /// The animation controllers for each of the sections in the accordion. - @internal final Map controllers; final Set _expanded; final int _min; @@ -81,7 +80,7 @@ class FAccordionController extends ChangeNotifier { if (_expanded.contains(index)) { return; } - + final futures = >[]; if (_max != null && _expanded.length >= _max) { futures.add(_collapse(_expanded.first)); @@ -98,6 +97,15 @@ class FAccordionController extends ChangeNotifier { notifyListeners(); } + /// Collapses the item at the given [index]. + /// + /// This method should typically not be called while the widget tree is being rebuilt. + Future collapse(int index) async { + if (await _collapse(index)) { + notifyListeners(); + } + } + Future _collapse(int index) async { if (_expanded.length <= _min || !_expanded.contains(index)) { return false; @@ -109,15 +117,6 @@ class FAccordionController extends ChangeNotifier { return true; } - /// Collapses the item at the given [index]. - /// - /// This method should typically not be called while the widget tree is being rebuilt. - Future collapse(int index) async { - if (await _collapse(index)) { - notifyListeners(); - } - } - /// Returns true if the number of expanded items is within the allowed range. bool validate(int length) => length >= _min && (_max == null || length <= _max); diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 9984ab25a..9eb4129de 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -6,38 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; import 'package:forui/src/foundation/util.dart'; - -@internal -class FAccordionItemData extends InheritedWidget { - @useResult - static FAccordionItemData of(BuildContext context) { - final data = context.dependOnInheritedWidgetOfExactType(); - assert(data != null, 'No FAccordionData found in context'); - return data!; - } - - final int index; - - final FAccordionController controller; - - const FAccordionItemData({ - required this.index, - required this.controller, - required super.child, - super.key, - }); - - @override - bool updateShouldNotify(covariant FAccordionItemData old) => index != old.index || controller != old.controller; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(IntProperty('index', index)) - ..add(DiagnosticsProperty('controller', controller)); - } -} +import 'package:forui/src/widgets/accordion/accordion.dart'; /// An interactive heading that reveals a section of content. /// @@ -73,8 +42,7 @@ class FAccordionItem extends StatefulWidget { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('style', style)) - ..add(DiagnosticsProperty('initiallyExpanded', initiallyExpanded)) - ..add(DiagnosticsProperty('child', child)); + ..add(DiagnosticsProperty('initiallyExpanded', initiallyExpanded)); } } @@ -148,7 +116,7 @@ class _FAccordionItemState extends State with TickerProviderStat _Expandable( percentage: _expand.value / 100, child: ClipRect( - clipper: _Clipper(percentage: _expand.value / 100), + clipper: _Clipper(_expand.value / 100), child: Padding( padding: style.childPadding, child: DefaultTextStyle(style: style.childTextStyle, child: widget.child), @@ -174,26 +142,21 @@ class _Expandable extends SingleChildRenderObjectWidget { final double _percentage; const _Expandable({ - required Widget child, + required super.child, required double percentage, - }) : _percentage = percentage, - super(child: child); + }) : _percentage = percentage; @override - RenderObject createRenderObject(BuildContext context) => _ExpandableBox(percentage: _percentage); + RenderObject createRenderObject(BuildContext context) => _ExpandableBox(_percentage); @override - void updateRenderObject(BuildContext context, _ExpandableBox renderObject) { - renderObject.percentage = _percentage; - } + void updateRenderObject(BuildContext context, _ExpandableBox renderObject) => renderObject..percentage = _percentage; } class _ExpandableBox extends RenderBox with RenderObjectWithChildMixin { double _percentage; - _ExpandableBox({ - required double percentage, - }) : _percentage = percentage; + _ExpandableBox(double percentage) : _percentage = percentage; @override void performLayout() { @@ -243,7 +206,7 @@ class _ExpandableBox extends RenderBox with RenderObjectWithChildMixin { final double percentage; - _Clipper({required this.percentage}); + _Clipper(this.percentage); @override Rect getClip(Size size) => Offset.zero & Size(size.width, size.height * percentage); diff --git a/forui/test/src/widgets/accordion/accordion_controller_test.dart b/forui/test/src/widgets/accordion/accordion_controller_test.dart index 8a1781d99..5ece5605b 100644 --- a/forui/test/src/widgets/accordion/accordion_controller_test.dart +++ b/forui/test/src/widgets/accordion/accordion_controller_test.dart @@ -10,6 +10,30 @@ import 'accordion_controller_test.mocks.dart'; @GenerateNiceMocks([MockSpec()]) @GenerateNiceMocks([MockSpec()]) +void _setup(List animationControllers, List> animations) { + animations.clear(); + animationControllers.clear(); + + for (int i = 0; i < 4; i++) { + animationControllers.add(MockAnimationController()); + animations.add(Tween(begin: 0, end: 100).animate(animationControllers[i])); + when(animationControllers[i].forward()).thenAnswer((_) { + when(animationControllers[i].value).thenReturn(1.0); + return TickerFuture.complete(); + }); + when(animationControllers[i].reverse()).thenAnswer((_) { + when(animationControllers[i].value).thenReturn(0.0); + return TickerFuture.complete(); + }); + } +} + +void _tearDown(List animationControllers) { + for (int i = 0; i < 4; i++) { + animationControllers[i].dispose(); + } +} + void main() { group('FAccordionController', () { late FAccordionController controller; @@ -19,22 +43,7 @@ void main() { setUp(() { count = 0; - animations.clear(); - animationControllers.clear(); - - for (int i = 0; i < 4; i++) { - animationControllers.add(MockAnimationController()); - animations.add(Tween(begin: 0, end: 100).animate(animationControllers[i])); - when(animationControllers[i].forward()).thenAnswer((_) { - when(animationControllers[i].value).thenReturn(1.0); - return TickerFuture.complete(); - }); - when(animationControllers[i].reverse()).thenAnswer((_) { - when(animationControllers[i].value).thenReturn(0.0); - return TickerFuture.complete(); - }); - } - + _setup(animationControllers, animations); controller = FAccordionController(min: 1, max: 3) ..addListener(() { count++; @@ -42,9 +51,7 @@ void main() { }); tearDown(() { - for (int i = 0; i < 4; i++) { - animationControllers[i].dispose(); - } + _tearDown(animationControllers); controller.dispose(); }); @@ -148,22 +155,7 @@ void main() { setUp(() { count = 0; - animations.clear(); - animationControllers.clear(); - - for (int i = 0; i < 4; i++) { - animationControllers.add(MockAnimationController()); - animations.add(Tween(begin: 0, end: 100).animate(animationControllers[i])); - when(animationControllers[i].forward()).thenAnswer((_) { - when(animationControllers[i].value).thenReturn(1.0); - return TickerFuture.complete(); - }); - when(animationControllers[i].reverse()).thenAnswer((_) { - when(animationControllers[i].value).thenReturn(0.0); - return TickerFuture.complete(); - }); - } - + _setup(animationControllers, animations); controller = FAccordionController.radio() ..addListener(() { count++; @@ -171,9 +163,7 @@ void main() { }); tearDown(() { - for (int i = 0; i < 4; i++) { - animationControllers[i].dispose(); - } + _tearDown(animationControllers); controller.dispose(); }); diff --git a/forui/test/src/widgets/accordion/accordion_golden_test.dart b/forui/test/src/widgets/accordion/accordion_golden_test.dart index 0dd058154..04cf071c5 100644 --- a/forui/test/src/widgets/accordion/accordion_golden_test.dart +++ b/forui/test/src/widgets/accordion/accordion_golden_test.dart @@ -12,28 +12,26 @@ void main() { group('FAccordion', () { testWidgets('shown', (tester) async { await tester.pumpWidget( - MaterialApp( - home: TestScaffold( - data: FThemes.zinc.light, - child: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FAccordion( - children: [ - FAccordionItem( - title: Text('Title'), - initiallyExpanded: true, - child: ColoredBox( - color: Colors.yellow, - child: SizedBox.square( - dimension: 50, - ), + TestScaffold.app( + data: FThemes.zinc.light, + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FAccordion( + children: [ + FAccordionItem( + title: Text('Title'), + initiallyExpanded: true, + child: ColoredBox( + color: Colors.yellow, + child: SizedBox.square( + dimension: 50, ), ), - ], - ), - ], - ), + ), + ], + ), + ], ), ), ); @@ -43,28 +41,26 @@ void main() { testWidgets('hidden', (tester) async { await tester.pumpWidget( - MaterialApp( - home: TestScaffold( - data: FThemes.zinc.light, - child: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FAccordion( - children: [ - FAccordionItem( - title: Text('Title'), - initiallyExpanded: true, - child: ColoredBox( - color: Colors.yellow, - child: SizedBox.square( - dimension: 50, - ), + TestScaffold.app( + data: FThemes.zinc.light, + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FAccordion( + children: [ + FAccordionItem( + title: Text('Title'), + initiallyExpanded: true, + child: ColoredBox( + color: Colors.yellow, + child: SizedBox.square( + dimension: 50, ), ), - ], - ), - ], - ), + ), + ], + ), + ], ), ), ); diff --git a/forui/test/src/widgets/accordion/accordion_test.dart b/forui/test/src/widgets/accordion/accordion_test.dart index c2aad67e3..ec2499681 100644 --- a/forui/test/src/widgets/accordion/accordion_test.dart +++ b/forui/test/src/widgets/accordion/accordion_test.dart @@ -1,6 +1,3 @@ -@Tags(['golden']) -library; - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; From 01b79c30d77874fd5e4fe47b435f5ef6d17ecdb9 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Wed, 2 Oct 2024 01:44:00 +0800 Subject: [PATCH 52/57] Ready for review --- .../accordion/accordion_controller.dart | 29 +- .../src/widgets/accordion/accordion_item.dart | 4 +- .../accordion/accordion_controller_test.dart | 439 +++++++++++------- 3 files changed, 302 insertions(+), 170 deletions(-) diff --git a/forui/lib/src/widgets/accordion/accordion_controller.dart b/forui/lib/src/widgets/accordion/accordion_controller.dart index 3a8b1f121..72fc37f64 100644 --- a/forui/lib/src/widgets/accordion/accordion_controller.dart +++ b/forui/lib/src/widgets/accordion/accordion_controller.dart @@ -11,7 +11,7 @@ class FAccordionController extends ChangeNotifier { final int _min; final int? _max; - /// An [FAccordionController] that allows only one section to be expanded at a time. + /// Creates an [FAccordionController] that allows only one section to be expanded at a time. factory FAccordionController.radio({Duration? animationDuration}) => FAccordionController( max: 1, animationDuration: animationDuration ?? const Duration(milliseconds: 200), @@ -19,7 +19,8 @@ class FAccordionController extends ChangeNotifier { /// Creates a [FAccordionController]. /// - /// The [min] and [max] values are the minimum and maximum number of selections allowed. Defaults to no minimum and maximum. + /// The [min], inclusive, and [max], inclusive, values are the minimum and maximum number of selections allowed. + /// Defaults to no minimum and maximum. /// /// # Contract: /// * Throws [AssertionError] if [min] < 0. @@ -28,7 +29,7 @@ class FAccordionController extends ChangeNotifier { FAccordionController({ int min = 0, int? max, - this.animationDuration = const Duration(milliseconds: 100), + this.animationDuration = const Duration(milliseconds: 200), }) : _min = min, _max = max, controllers = {}, @@ -38,7 +39,12 @@ class FAccordionController extends ChangeNotifier { assert(max == null || min <= max, 'The max value must be greater than or equal to the min value.'); /// Adds an item to the accordion. - void addItem(int index, AnimationController controller, Animation animation, {required bool initiallyExpanded}) { + Future addItem( + int index, + AnimationController controller, + Animation animation, { + required bool initiallyExpanded, + }) async { controller ..value = initiallyExpanded ? 1 : 0 ..duration = animationDuration; @@ -47,7 +53,9 @@ class FAccordionController extends ChangeNotifier { if (initiallyExpanded) { if (_max != null && _expanded.length >= _max) { - return; + if (!await _collapse(expanded.first)) { + return; + } } _expanded.add(index); } @@ -55,6 +63,9 @@ class FAccordionController extends ChangeNotifier { /// Removes the item at the given [index] from the accordion. Returns true if the item was removed. bool removeItem(int index) { + if (_expanded.length <= _min && _expanded.contains(index)) { + return false; + } final removed = controllers.remove(index); _expanded.remove(index); return removed != null; @@ -77,7 +88,7 @@ class FAccordionController extends ChangeNotifier { /// /// This method should typically not be called while the widget tree is being rebuilt. Future expand(int index) async { - if (_expanded.contains(index)) { + if (_expanded.contains(index) || controllers[index] == null) { return; } @@ -122,4 +133,10 @@ class FAccordionController extends ChangeNotifier { /// The currently selected values. Set get expanded => {..._expanded}; + + /// Removes all objects from the expanded and controller list; + void clear() { + _expanded.clear(); + controllers.clear(); + } } diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 9eb4129de..bc7c08b97 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -51,7 +51,7 @@ class _FAccordionItemState extends State with TickerProviderStat late Animation _expand; @override - void didChangeDependencies() { + Future didChangeDependencies() async { super.didChangeDependencies(); final data = FAccordionItemData.of(context); @@ -69,7 +69,7 @@ class _FAccordionItemState extends State with TickerProviderStat parent: _controller, ), ); - data.controller.addItem(data.index, _controller, _expand, initiallyExpanded: widget.initiallyExpanded); + await data.controller.addItem(data.index, _controller, _expand, initiallyExpanded: widget.initiallyExpanded); } @override diff --git a/forui/test/src/widgets/accordion/accordion_controller_test.dart b/forui/test/src/widgets/accordion/accordion_controller_test.dart index 5ece5605b..44113c31f 100644 --- a/forui/test/src/widgets/accordion/accordion_controller_test.dart +++ b/forui/test/src/widgets/accordion/accordion_controller_test.dart @@ -10,17 +10,18 @@ import 'accordion_controller_test.mocks.dart'; @GenerateNiceMocks([MockSpec()]) @GenerateNiceMocks([MockSpec()]) -void _setup(List animationControllers, List> animations) { +void _setup(List animationControllers, List> animations, int length) { animations.clear(); animationControllers.clear(); - for (int i = 0; i < 4; i++) { + for (int i = 0; i < length; i++) { animationControllers.add(MockAnimationController()); animations.add(Tween(begin: 0, end: 100).animate(animationControllers[i])); when(animationControllers[i].forward()).thenAnswer((_) { when(animationControllers[i].value).thenReturn(1.0); return TickerFuture.complete(); }); + when(animationControllers[i].reverse()).thenAnswer((_) { when(animationControllers[i].value).thenReturn(0.0); return TickerFuture.complete(); @@ -28,8 +29,8 @@ void _setup(List animationControllers, List> } } -void _tearDown(List animationControllers) { - for (int i = 0; i < 4; i++) { +void _tearDown(List animationControllers, length) { + for (int i = 0; i < length; i++) { animationControllers[i].dispose(); } } @@ -40,110 +41,178 @@ void main() { final List animationControllers = []; final List> animations = []; int count = 0; + int length = 3; setUp(() { count = 0; - _setup(animationControllers, animations); - controller = FAccordionController(min: 1, max: 3) + length = 3; + _setup(animationControllers, animations, length); + controller = FAccordionController(min: 1, max: 2) ..addListener(() { count++; }); }); tearDown(() { - _tearDown(animationControllers); + _tearDown(animationControllers, length); controller.dispose(); }); - test('addItem(...)', () { - controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); - expect(controller.expanded.length, 0); - controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); - expect(controller.expanded.length, 1); - - controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); - expect(controller.expanded.length, 1); - - controller - ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: true) - ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) - ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); - expect(controller.expanded.length, 3); - expect(controller.expanded.last, 2); - - controller.addItem(3, animationControllers[3], animations[3], initiallyExpanded: false); - expect(controller.expanded.length, 3); - expect(controller.controllers.length, 4); - }); - - test('removeItem(...)', () { - controller - ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: true) - ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) - ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) - ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); - expect(controller.removeItem(0), true); - expect(controller.removeItem(4), false); - expect(controller.removeItem(1), true); - expect(controller.expanded.contains(1), false); - expect(controller.removeItem(2), true); - }); - - test('toggle(...)', () async { - controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); - await controller.toggle(0); - expect(controller.controllers[0]?.animation.value, 100); - await controller.toggle(1); - expect(count, 1); - - controller - ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) - ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) - ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: false); - await controller.toggle(1); - expect(controller.controllers[1]?.animation.value, 100); - await controller.toggle(3); - expect(controller.controllers[0]?.animation.value, 0); - }); - - test('expand(...)', () async { - controller - ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: false) - ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) - ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: false) - ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); - await controller.expand(0); - expect(controller.expanded, {3, 0}); - await controller.expand(0); - expect(count, 1); - await controller.expand(1); - await controller.expand(2); - expect(controller.expanded, {0, 1, 2}); - }); - - test('collapse(...)', () async { - controller - ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: false) - ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) - ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: false) - ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); - await controller.collapse(3); - expect(controller.expanded.contains(3), true); - expect(count, 0); - await controller.expand(0); - await controller.collapse(3); - await controller.collapse(2); - expect(count, 2); - await controller.collapse(0); - expect(controller.expanded, {0}); + group('addItem(...)', () { + test('sets animation controller value based on initiallyExpanded', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + verify(animationControllers[0].value = 0); + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + verify(animationControllers[0].value = 1); + }); + + test('adds to expanded list', () { + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + expect(controller.expanded.length, 0); + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + expect(controller.expanded.length, 1); + expect(controller.controllers.length, 1); + }); + + test('aware of max limit', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: true); + await controller.addItem(2, animationControllers[2], animations[2], initiallyExpanded: true); + expect(controller.expanded, {1, 2}); + }); + }); + + group('removeItem(...)', () { + setUp(() { + length = 1; + _setup(animationControllers, animations, length); + controller = FAccordionController(min: 1, max: 2) + ..addListener(() { + count++; + }); + }); + + tearDown(() { + _tearDown(animationControllers, length); + }); + test('removes from the expanded list', () { + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + expect(controller.removeItem(0), true); + expect(controller.removeItem(0), false); + }); + test('aware of min limit', () { + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + expect(controller.removeItem(0), false); + }); + }); + + group('toggle(...)', () { + test('expands an item', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + expect(controller.controllers[0]?.animation.value, 0); + await controller.toggle(0); + expect(controller.controllers[0]?.animation.value, 100); + }); + + test('collapses an item', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: true); + await animationControllers[0].forward(); + expect(controller.controllers[0]?.animation.value, 100); + await controller.toggle(0); + expect(controller.controllers[0]?.animation.value, 0); + }); + + test('invalid index', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + await controller.toggle(1); + expect(count, 0); + await controller.toggle(0); + expect(count, 1); + }); + + test('aware of max limit', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: true); + await controller.addItem(2, animationControllers[2], animations[2], initiallyExpanded: false); + await animationControllers[0].forward(); + await animationControllers[1].forward(); + expect(controller.controllers[0]?.animation.value, 100); + expect(controller.controllers[1]?.animation.value, 100); + + await controller.toggle(2); + expect(controller.controllers[0]?.animation.value, 0); + }); + + test('aware of min limit', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: false); + await controller.addItem(2, animationControllers[2], animations[2], initiallyExpanded: true); + await animationControllers[2].forward(); + expect(controller.controllers[2]?.animation.value, 100); + + await controller.toggle(2); + expect(controller.controllers[2]?.animation.value, 100); + }); + }); + + group('expand(...)', () { + test('does not call notifyListener on invalid index', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + await controller.expand(0); + expect(controller.expanded, {0}); + await controller.expand(0); + expect(count, 1); + await controller.expand(1); + expect(count, 1); + }); + + test('aware of max limit', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: true); + await controller.addItem(2, animationControllers[2], animations[2], initiallyExpanded: false); + await controller.expand(2); + expect(controller.expanded, {1, 2}); + }); + }); + + group('collapse(...)', () { + setUp(() { + length = 2; + _setup(animationControllers, animations, length); + controller = FAccordionController(min: 1, max: 2) + ..addListener(() { + count++; + }); + }); + + tearDown(() { + _tearDown(animationControllers, length); + }); + + test('does not call notifyListener on invalid index', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: true); + await controller.collapse(0); + expect(controller.expanded, {1}); + await controller.collapse(0); + expect(count, 1); + await controller.collapse(2); + expect(count, 1); + }); + + test('aware of min limit', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.collapse(0); + expect(controller.expanded, {0}); + }); }); test('validate(...)', () { expect(controller.validate(0), false); expect(controller.validate(1), true); expect(controller.validate(2), true); - expect(controller.validate(3), true); - expect(controller.validate(4), false); + expect(controller.validate(3), false); }); }); @@ -152,10 +221,12 @@ void main() { final List animationControllers = []; final List> animations = []; int count = 0; + int length = 2; setUp(() { count = 0; - _setup(animationControllers, animations); + length = 2; + _setup(animationControllers, animations, length); controller = FAccordionController.radio() ..addListener(() { count++; @@ -163,86 +234,130 @@ void main() { }); tearDown(() { - _tearDown(animationControllers); + _tearDown(animationControllers, length); controller.dispose(); }); - test('addItem(...)', () { - controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); - expect(controller.expanded.length, 0); - expect(controller.controllers.length, 1); - controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); - expect(controller.expanded.length, 1); - expect(controller.controllers.length, 1); - - controller - ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: true) - ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) - ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); - expect(controller.expanded.length, 1); - }); - - test('removeItem(...)', () { - controller - ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: true) - ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) - ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: true) - ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); - expect(controller.removeItem(0), true); - expect(controller.removeItem(4), false); - expect(controller.removeItem(1), true); - expect(controller.expanded.contains(1), false); - expect(controller.removeItem(2), true); - }); - - test('toggle(...)', () async { - controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); - await controller.toggle(0); - expect(controller.controllers[0]?.animation.value, 100); - await controller.toggle(1); - expect(count, 1); - - controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: false); - - await controller.toggle(1); - expect(controller.controllers[0]?.animation.value, 0); - }); - - test('expand(...)', () async { - controller - ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: false) - ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) - ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: false) - ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); - await controller.expand(0); - expect(controller.expanded, {0}); - await controller.expand(0); - expect(count, 1); - await controller.expand(1); - await controller.expand(2); - expect(controller.expanded, {2}); - }); - - test('collapse(...)', () async { - controller - ..addItem(0, animationControllers[0], animations[0], initiallyExpanded: false) - ..addItem(1, animationControllers[1], animations[1], initiallyExpanded: false) - ..addItem(2, animationControllers[2], animations[2], initiallyExpanded: false) - ..addItem(3, animationControllers[3], animations[3], initiallyExpanded: true); - await controller.collapse(3); - expect(controller.expanded.isEmpty, true); - await controller.expand(0); - await controller.collapse(3); - await controller.collapse(2); - expect(count, 2); - expect(controller.expanded, {0}); + group('addItem(...)', () { + test('adds to expanded list', () { + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + expect(controller.expanded.length, 0); + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + expect(controller.expanded.length, 1); + expect(controller.controllers.length, 1); + }); + test('aware of max limit', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: true); + expect(controller.expanded, {1}); + }); + }); + + group('removeItem(...)', () { + setUp(() { + length = 1; + _setup(animationControllers, animations, length); + controller = FAccordionController.radio() + ..addListener(() { + count++; + }); + }); + + tearDown(() { + _tearDown(animationControllers, length); + }); + + test('removes from the expanded list', () { + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + expect(controller.removeItem(0), true); + expect(controller.removeItem(0), false); + }); + test('aware of min limit', () { + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + expect(controller.removeItem(0), true); + }); + }); + + group('toggle(...)', () { + test('expands an item', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + expect(controller.controllers[0]?.animation.value, 0); + await controller.toggle(0); + expect(controller.controllers[0]?.animation.value, 100); + }); + + test('collapses an item', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await animationControllers[0].forward(); + expect(controller.controllers[0]?.animation.value, 100); + await controller.toggle(0); + expect(controller.controllers[0]?.animation.value, 0); + }); + + test('invalid index', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + await controller.toggle(1); + expect(count, 0); + await controller.toggle(0); + expect(count, 1); + }); + + test('aware of max limit', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: false); + await animationControllers[0].forward(); + expect(controller.controllers[0]?.animation.value, 100); + + await controller.toggle(1); + expect(controller.controllers[0]?.animation.value, 0); + }); + + test('aware of min limit', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await animationControllers[0].forward(); + expect(controller.controllers[0]?.animation.value, 100); + + await controller.toggle(0); + expect(controller.controllers[0]?.animation.value, 0); + }); + }); + + group('expand(...)', () { + test('does not call notifyListener on invalid index', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.expand(0); + expect(count, 0); + + await controller.expand(1); + expect(count, 0); + }); + + test('aware of max limit', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: false); + await controller.expand(1); + expect(controller.expanded, {1}); + }); + }); + + group('collapse(...)', () { + test('does not call notifyListener on invalid index', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); + await controller.collapse(0); + expect(count, 0); + }); + + test('aware of min limit', () async { + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); + await controller.collapse(0); + expect(controller.expanded.isEmpty, true); + }); }); test('validate(...)', () { expect(controller.validate(0), true); expect(controller.validate(1), true); expect(controller.validate(2), false); - expect(controller.validate(3), false); }); }); } From c4bbbf45a0ea7e46e9d457efa9af9f5c86ae1bb1 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Wed, 2 Oct 2024 01:47:22 +0800 Subject: [PATCH 53/57] Update accordion_item.dart --- forui/lib/src/widgets/accordion/accordion_item.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index bc7c08b97..9ab2694b2 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -96,7 +96,7 @@ class _FAccordionItemState extends State with TickerProviderStat applyHeightToLastDescent: false, ), style: style.titleTextStyle.copyWith( - decoration: state.hovered || state.longPressed ? TextDecoration.underline : TextDecoration.none, + decoration: state.hovered || state.shortPressed ? TextDecoration.underline : TextDecoration.none, ), child: widget.title, ), From 8ae0af7cea08ab7cce62faad8e9a16fefc2cd802 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Tue, 1 Oct 2024 17:48:55 +0000 Subject: [PATCH 54/57] Commit from GitHub Actions (Forui Presubmit) --- forui/lib/src/widgets/accordion/accordion_item.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 9ab2694b2..1aa671b38 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -96,7 +96,8 @@ class _FAccordionItemState extends State with TickerProviderStat applyHeightToLastDescent: false, ), style: style.titleTextStyle.copyWith( - decoration: state.hovered || state.shortPressed ? TextDecoration.underline : TextDecoration.none, + decoration: + state.hovered || state.shortPressed ? TextDecoration.underline : TextDecoration.none, ), child: widget.title, ), From 92fa7ba93a787df12fdcf9d5da51950c9f97b112 Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Wed, 2 Oct 2024 15:19:33 +0800 Subject: [PATCH 55/57] Fixed next pr issues --- .../lib/src/widgets/accordion/accordion.dart | 18 ++--- .../src/widgets/accordion/accordion_item.dart | 4 +- forui/test/golden/accordion/hidden.png | Bin 4021 -> 24785 bytes forui/test/golden/accordion/shown.png | Bin 4082 -> 24828 bytes .../accordion/accordion_controller_test.dart | 68 ++++++++++++------ 5 files changed, 57 insertions(+), 33 deletions(-) diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index ebea24616..78b5d1f40 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -10,7 +10,7 @@ import 'package:forui/forui.dart'; /// /// /// See: -/// * https://forui.dev/docs/FAccordion for working examples. +/// * https://forui.dev/docs/accordion for working examples. /// * [FAccordionController] for customizing the accordion's selection behavior. /// * [FAccordionItem] for adding items to an accordion. /// * [FAccordionStyle] for customizing an accordion's appearance. @@ -106,7 +106,7 @@ final class FAccordionStyle with Diagnosticable { final Widget icon; /// The divider's color. - final Color dividerColor; + final FDividerStyle divider; /// Creates a [FAccordionStyle]. FAccordionStyle({ @@ -115,7 +115,7 @@ final class FAccordionStyle with Diagnosticable { required this.titlePadding, required this.childPadding, required this.icon, - required this.dividerColor, + required this.divider, }); /// Creates a [FDividerStyles] that inherits its properties from [colorScheme]. @@ -133,7 +133,7 @@ final class FAccordionStyle with Diagnosticable { height: 20, colorFilter: ColorFilter.mode(colorScheme.primary, BlendMode.srcIn), ), - dividerColor = colorScheme.border; + divider = FDividerStyle(color: colorScheme.border, padding: EdgeInsets.zero); /// Returns a copy of this [FAccordionStyle] with the given properties replaced. @useResult @@ -143,7 +143,7 @@ final class FAccordionStyle with Diagnosticable { EdgeInsets? titlePadding, EdgeInsets? childPadding, SvgPicture? icon, - Color? dividerColor, + FDividerStyle? divider, }) => FAccordionStyle( titleTextStyle: titleTextStyle ?? this.titleTextStyle, @@ -151,7 +151,7 @@ final class FAccordionStyle with Diagnosticable { titlePadding: titlePadding ?? this.titlePadding, childPadding: childPadding ?? this.childPadding, icon: icon ?? this.icon, - dividerColor: dividerColor ?? this.dividerColor, + divider: divider ?? this.divider, ); @override @@ -162,7 +162,7 @@ final class FAccordionStyle with Diagnosticable { ..add(DiagnosticsProperty('childTextStyle', childTextStyle)) ..add(DiagnosticsProperty('padding', titlePadding)) ..add(DiagnosticsProperty('contentPadding', childPadding)) - ..add(ColorProperty('dividerColor', dividerColor)); + ..add(DiagnosticsProperty('divider', divider)); } @override @@ -175,7 +175,7 @@ final class FAccordionStyle with Diagnosticable { titlePadding == other.titlePadding && childPadding == other.childPadding && icon == other.icon && - dividerColor == other.dividerColor; + divider == other.divider; @override int get hashCode => @@ -184,7 +184,7 @@ final class FAccordionStyle with Diagnosticable { titlePadding.hashCode ^ childPadding.hashCode ^ icon.hashCode ^ - dividerColor.hashCode; + divider.hashCode; } @internal diff --git a/forui/lib/src/widgets/accordion/accordion_item.dart b/forui/lib/src/widgets/accordion/accordion_item.dart index 1aa671b38..1af6a90b0 100644 --- a/forui/lib/src/widgets/accordion/accordion_item.dart +++ b/forui/lib/src/widgets/accordion/accordion_item.dart @@ -42,7 +42,7 @@ class FAccordionItem extends StatefulWidget { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('style', style)) - ..add(DiagnosticsProperty('initiallyExpanded', initiallyExpanded)); + ..add(FlagProperty('initiallyExpanded', value: initiallyExpanded, ifTrue: 'Initially expanded')); } } @@ -125,7 +125,7 @@ class _FAccordionItemState extends State with TickerProviderStat ), ), FDivider( - style: context.theme.dividerStyles.horizontal.copyWith(padding: EdgeInsets.zero, color: style.dividerColor), + style: style.divider, ), ], ), diff --git a/forui/test/golden/accordion/hidden.png b/forui/test/golden/accordion/hidden.png index 8bbaf629f5fced8fe64db43f610aed24171dbce3..666042c4eb6e0f09201b90f0509a82fe795366aa 100644 GIT binary patch literal 24785 zcmeHQcUTi?w;y{fh!HD_D5$t10xCuNDx%mBH6+qO1tbC@RRVA3qC1eiD!d9X80Z7zXzZ-tnCADX8Y~2Jc$CYP}G^L z0I(jgH2?KvaMA$B*u$}Rd70p=t>;;_&5fP=I)AZtpx@0{^0xEgjbEcI&j)QiqjQS8 zQ3bd66)Hm=*&(-oGrlx{ODYW3-n?0jvhMPR zHmp#(5}?Q5<5qL8^GlTl{Z3fpnlg#7YNa4Qs6A?4e?R7W`HlH>>DtE{=oJBKWix-? zKVX~Vn(qM%f8Iv4D!5owqo0{1%Aculn<(O5F}X1}1ht6~jbh5BERA6t%FqS;^FUo&quWu`UtRlex!3+@}c zgtplW({D2$&F&GA_mfy_VLsXPqVSZdr;A`uvDGMi=Hye1$&3|JhhvXrqA00TPjz8W zt*@Z)ngYyq6vZCWpqO5RQde*iUZvM;gz)htU!~8D$__EdeVob;Yp+yy*%l~2%6&nO z35n77uue)_N?HdRc%Jsjsn^@o)z|n~tutJax!o})>eZ`B|NkXD-}wc%_=vNPTW+SK2Suq zr&pob`drF7v`rXRmFlR$qs^w&52&t@RcTtqZQ%E@)aJ=*F zgiEC1AB#_{trhf@@2MOgwUdlT_cm1JXqMVXi#&4-DHPv;a1-xr>iHsA)v70F9XSt0 z!U}993A>;B2?o@&o41!NNThr2jhOROy;;B4au=^ktj^|ga*6^;YV>n-`|k3U9pQ{& z0n5!aJG=VhPWL2LYJ?04K==9cqP6lR6DDy*!?ymO!66q6CrSd;B_?`m<%&DiP&0n2 za=rz&%<1mFOfK_dt@T^)G_}P_7vtzNA@qXHpP<<<`p%)*2@p)q3sts>H^_pB@VZKW z7YkVrTwm_02B~;=85KPUW%Yr)%CHTqJhM{oH12j+o7oJ75SxcQK}RS_r(2U@1J9&JN_ z#f37*4MJrfu>tLpdmVBu@&Jjo9j0psC=A8rJR1~l?7Q==O`5)la(gG+Yf-tm`WL^P z@i#~&SNFAX?DD&7KG=V7JoP@DI}3zGCuZ|y#M{J zSyvWUQIx|y6xiEgj=S!6cjsm*6Jz*3AGYw;c2zdt_9HPa{4T3Z=3mWzq#zqCmp4}A zN3w%D4q*Nj6!)3g?P{G!UwqW6jg%_~VH$H2ML7jCh}$7qdC$xO%7!Bd&og z0TEurf+%{;W+<`Oz}rpGt>O}wcuQGn13DYZx5BXXG$&zJKz=mGpeQi5d%OW{vl?>i z*a;P?SOIb?Hj2(W0#mJ4PS%8UEi)rwte~`QA)zSYFkMT~HpTvho5p2y8FBS$LCLda z3V6Kl-+#aQWpEb>g&%{$Yh15F6)c6Is<=vL?uTs2 zZ7f%mhoIVZ8-@P{6LwaizJUuz(Jf%Y8tcmyVV0kPmPIgO6%}e1Ot|(cJrXVqT3!rU zeg;~;042!->|JM=u8S&EBWUsZ@`F78L*fqU{ta00#BYxure;;~k0z5tZX18U6lT0V zb+$ z`nvFZ81;b4$2`M~-Hs+*J(_t!s2|42DOAMv_EzV?6Wggh*^{QXZux$_W%cMUC98Rv znbn2!4?$?3wl?nG`j*OoN|>e zv4Ttu+(Y#fp>ZBnziV87C0ys&Rbenxj-Bl@$j4-32JK~ zl6+*EuEIQXxbVi-C2--J=R)AZ7UY#KS&j@Fuw^b@2r)<@_GevbUl&V<94~vRl$;!^ zTspeFWPBjURo8`c$|3f(si^bmNIlvn)2_y9mULTO#;;Y!x^{3FvbN31f^EX1J}LSa z%^`$hT7>E82{=277l~yR%Hq!6AXTJgGnvR2l5yYDzE1;tQ6o#8E<^qHc#o1QwU~*8 z_CJ7pJah8U1YdMqNrieobU6g$15bMDG2P^Qz;g|D;C=xRqTVCAl|TYXDS9v6ZX12O}NN{HEZqasbZM@ESpxoqEACuLfcFrPIIsz> z$>>oBoDJxg!&PAIU#1%v4FeM}eD@1nc)*nb7jC?g1s66;1-o~h=|Pk<*JXKe4Gk9N zmAMO7K57~aWMJec-!-SMWA`<)?a{KW2{c0k2w(}dRK!N8C(QnY zI&?kkm~jI-&H)EtXl_K*0{{XJSZ_$>Og0_TRv-unDS#j#qyU0|kOBw-LJE^0Afy0- zfRF+R!oQ9b#F~u2sSld8UUq~mW<;w_7Utv?VJZ874M>zj@zx)w+B} zb=a+n7ax&Vr|bRBS$!e)vNP6q_PLC6y7Z`eU9%l>Zr8CC!i}=d202Db-^Xu{E)NCI zJ!6uzI-yH=u%MF=UfMfSH*WY^I?_Udeeb;i_FeY@Xv$*=>^p5_x!J!hGPc1Cz0a6> zw;VG0&bCv>i;Ii%35L?909pd7lOkB&`S{v9y?AHm0J%j}+H!Tt_JzNR0#+y1%OE$38kLzvOI z;ZJEDJbf4G$TskxZ72c2SH~#_&c-F`dQo-o>q@%_;8OcUJyX4NfuB2PK*g%XrOm;$ z<#`qN-5nkzP)_HaYhv0K1q+T8cQiFMY1`#`Vv>ysh3y2shr{!W>2v4K_2d+)`Gi=> zoXZ)sjnL6A=*@7tzqKqxP&Jqz$~#FUmfoUBmvnC5sjF*~T`CF6b^euAtI2;nI>hj4 z2px%g4h_T>jK*BQ-lAY-gz0^qnoTC(QX^XPODDXua8F$=O4~e>volk}GqyVvp2T33 z!@emCzot%%-p(K8ywr2^AWBUEPW@T=5`k?f1`{CYuCgta+_TK_#^*M)h=s&Jjgmrx z(LO8rql7HOp4rquhC`MO{w*~#GgC2^`Pch*?{Hy)s+BDgQv-w6r|eIkJaiK4%3`IGE}){0rh_8#Y+QZ@%ag^7CHkJ|VqwQS=V>+%4{BbMjZhc#3xz_K zfV=mE1uiWuEq|?xL&1fI>?}JXN5m5e9*S;fT^7)WE@HWqn>TO%LhN{U?zDzuQ3083 zGX5<`aoxJ1;2zI{T;fFA<UhlLa{rvGj}u=`;(R$OI~&Tn^OCjt&JIJ9KLpd+iDl#GHOgpan>PHF=SIdks3k>)pPeD?RRrB9R$ z9eKf$#)lOFf4?+zXj>ySRfpl2%*SVNDfgpiu#-DQ0(nE4lKu0GXT!d{%J!^?bf5%NHw#2Ebss`|ypU{RBq;`AAcCmCf^^&bK*x!;(!ZNtL7aT?qhDe+^lKnKG-zlgr zGScKzjYPWM+2jCdCT7YE*frMG15c|Z!M6Bs!^7;1BWu*^OG46RgOwi!GuF7&2Zmy1 zQv24dljO&}Cu1lX#SN<0joxbGbrS_8klMocsQ$o8kgFClO1h2M-uZ?!<;nP!c7JJj z)>8OJ>9%xj*+hSTKjq=WwA}FVCLM#2OK*L^j0>AjShZAERt9ua(sg$E`&)EVmO3A? zqOf=gu`w~tJ@u~x&W`)_n$RQ?35_bF*sqppPanl8PdL|{pn+Vl>!ab$OJ7(R+Ird8 zS`Ue$=AvPj_YI$#+;=OR2HSxBn50TTSlN)7@t6F zkI!8f_RY@W)I6#8Jvk*MB?jT49yR?acW&zo(~V{KBQ4V2Xfr~;U-WwSdBE$vymak{ zEU{S3d-rq%KQ4CXbyr1WFv7A~^39Engq#&l@c=#qybRpHtb%(<=9rvRE@8L4iO5+_ zKJ~F)yJ*yw{(zT$Hyj7Oi!{7{k@s|E!LBzdXnzN{>;{%-4(f7a@Jo)X)L3Q~)6N!li zZ^#_)mWE`;=K94-2KHE*{gafG6xz^oS#YQPOv=PPddfpni3M4JeQ&2b%|Y1WnOitq zFSkawxh7uSuw=Iuy|6v>YqgH9uI@*InpFy!Pn^)~2U}06)T@0A>(HhwRjFn8KiRZU zJUYsa-IZ7mhGKjU}JR=){@FhXlB8Na+HT$a;>kT*RUYjK)6BDD+D#a&trO?uWvUhr%k)?e9_G)mAJ!PDea{xm@-qc>wZy zhP;U)MF1d0@DC{h7vpDtN@oG4c6o#Vk=p-<)IL)0QwSjSj@0}A*A2gHf{w4AdL8&< zEUTB!(3id%Am8Txc&A6^-hSYKe7*k@2c#g7ftRmDXSwgFIc<;g G@BarYx|9DcpTqRy((#<+P~Oh&SzHm=rqJ>FJl+;nN1YU?(-)?m7-xS@DK@zM{} zS<%gO4CRt#X-#L42o6OQm`-gw7c-H1Hc!bla+p@hP9_yz)ix{nplZcLQ#6PI;JEdm56; zIw3YUn6KmU_;3K4L$vL2q-b~f)o+?2T7{UoDSr8&bT;6AvL!J$wtfNa3aax;!Eo)=^?e- zBPe`+Ne??h&n(AR`aXe*aZ#b&YPFilymmeDJePW4R?cDbIio9wDo#pLZZ3^*d;R(7 z&a=LrO~^Dv%Vk6>TLT)o6Ev=4CV}RQAlN(>%Pf^tPX79iS7$r_Dv!t0S}yA9He-ZD zU8JyVK{PK$7sVBN6rKe_f#92oJ1&rFsx2mrIXQVm+IUG34!$v-nPdFGVPEcD?13OU z?%gzxw%-6@)Eu_5aWf= z(ul!P`+-8iN^MvXI8G>E^r{pF!?AK8eB?2(J(lhp+L%o7Du)uCv|Y2*tW)SGRGxkS z3@h#DGA?|9Kb!i4etY@bWx53Koqf@TJ;jiJ)Lz5DVM5WBAA`^#!r>>8nU7&XDr<5KzYA-}ZEkj!P*qh$HonsL6#!5{d+O#6Z6Rsj zq(nf4$O8cHE!GucPcYE0h{!Y;e(wKkP`JCvmxDFeAVok>QLqdUr3Fzy0wPikb;c+tO-HeXqN3s? zfdqj>DFG1^Q7MW9Lf0rr0z||Z2n5~>V{$M5zO}yfu377yo3#+`$=Unt^XzBuvv2MO zFYLFonLkHm4gdi2cki+~2mrIO05A)MnhB0PZjej|zx@<+(8dDDty$3nUQ7?N*!>#{ zd_|&;#RGr_u-j_KZ{caZZ6-c$-Ad&HgGYRiuU%8uxwG?^1Fp2|4;H-bymM*Ch24H( z>yGIi=B?7iSiZPdh28Yjv#ajc)Rh{_^$LGnie(sL)8_54N*FxedL;ByiRvwj-cyC<2s^@g zcgzxKFJVdzS6gFhGfkGW-C|6}M&H;BynFv_xlohx2(m-_;BGW$(7Tp(xjs9qzNhkO zPfvKy=oz%5B}~8LY&559h-5=(SOK%iu^WXWPma!mjXJSWxa^5h2h*RH$P7-}pN%4C zPLArsMi0cHaJmByDJZfFq(SBWOO(zccTqg;6s}lIy%b(?A45yWN&m!yiiqjC-Urj+D~$h24-CshYQ-ALU1;Y%1qZA#r9;IG-N_N1Vr`3Eef%^L5Ky5{7*9jmhM|kZ9ABREl2-jI}!H zIs5#eoiv0t6GR`L1P%1+P~UB2&LPr#En{c=P;V~qSMu<$bkbW>LCRP}kS+Qpq5ani z<&Nm$?*k29W>hNs(^l^^ElR8$2|)kJladvR%yHA?lJ7@?eZ%pmjK`TFI%B2=Yb#W? zu0{RyLlyQ47&4w8da`+}Pp=NVJ(;yuLG^Sp?PmzRaI3%2oToh}(3~3(OxU@a>m@7Y zn~3#~r!^%(tA}b-99;{kXgY^V=!3j^TTyd8T@w;Ey+8}U3JHHVCp`Cp31=mtaDjQ7 z(Hu!>L~4!WI>;@nUav4yQ~WLr*h>%%5CbmK7D6$4DL6Qk4hppq4R&H?L+X~sqR4H~ zv`rdl#|YTIP*#{ei1x87(H?~N%jJuPj{j1MncU0U4^bHj}{ZC#YD>w2;3ufO(qlNT+QrxYv!Poy!W zqk|+H?v-hA3eU;v(LRef8)1eYwej}mWvj;|3IkfT`77R6zPlPkC_eRvoSCe9?}7+n zj!r!OXh{&k8HzZ7buTRW3+uzt14NpFoqY?Ta6W`-;$;;1Fl-qwO-k1Stc)(^GpsPx zP#DHu1hWK`@M7m&puJoJIrbuWxf;4uS>TbnTTX32Z)L?g7`DDv1cxP%9q$rRWLUO) zxDxHS3^Hr~Ax(;85oA`<1sZ=JO!e9dk}jlcu{puP9&(#D0Y#36=~{qxEDbJNEtRLs z*!cLz%!0*>aJayaAFDnG8#$!ZGiHhVpS#<8HPTWkGFQcGsT=VAUL()ua6EVGUnv*;m-Ac?Wiz=I2! zKSSVOQIz+1>FU0tMusM;F-I`k(TlD@G@Ju&FB!U20k;S9;5GsZHwta9PD7J2un>ak zSv-wp1FbcGRfUQo1l6{yDBL%gFjkZD1}=PoW(^bGpi!X$Te$#Sc?c$~sY!Vc6Mhv> zI}aBIS5|;lE&x|P1v$wFj9n~D*J(}4c4)Fj#U6gJt)xSCegoEg$=ic_DLK!DcIhPi zRg>W}Q6?KRe_h0Ia@22Ec>TJv$M*Ws)4smPItt|{571QCLJ-v(bXYAeXgSgG@mNXl zSmgGQp)af!Lw&eEws(QbM}4DAyzJ8A?XnMv45Nzki&UJtyV-QOV>>rb#H87kD}jSo z?C;)FwV#!p%`Td43!&||n^0&BZG*M}&8dMlS)Oc#X@OV`6fgAfhq3zGPJ+k~60S2q zbDqJ3GvaAu_RvzjH&KEZ=xy_hD3~4dZug;Kgj%|bsI?x_yopA)#z3yXYBw+SfO2H) zofFOkD6E0=qDNy;8MZpDsZ$eccfH_C_+dX^YqaCxIU)oQT9gB;FuXLP_cC;1C!!|z$ zb_kCIWEh^df)Gk<6y3jn1LI8gCp6ecHt?pelquTeIN`{rW70s+z`W3Ilz5^0IVj#9 zTBvGKN?A_O`3H~=W=|AN@JR<%H7S0PN)U{oDg)4%%;!l}J0kY|46B2dps9lBpKZ{@yig(xM!gx}YCe3O)QlhB7)3 zVz7&#_i~C2v$uegc<@R0n`|cvE4ZP%B^PKcCAbON)pnD07EJl-=5-Y+VUbGcH$DBK z^UyMUZ$HS@-SE7!`N&=~vCAm0RLoMDqNojY%;V7a7L^i7Q&}J6m;D7<`S}BJVI4^~ z(pHQxX2>gY^8wOD7-!72enZ1a$66W&C!NaiG&o~@(gJtF7<>CNh&PPxER~Qc!v0sM z2$vj~BCG@!rZ8d7(oneYk>fX}2&a2Y5l%fnRaj+P1(ZL59Q|EWbm=aKtpxlDSKO%_ z0goYo*t;68Y5Uy{cs8J&i`ImNf4P2W0t`&Rxak+TaPQG#xNu!u4qVtg6O3Lpvpp!8 zugmGlH8s{&m4$Pa@7DK)7CR_T+-pw0_TSQ_x}fDp6L74LjF-I!#2$oT1pb5~1lSFQ zRO~7!Cd~hYB2)ubW?X{GIbatI%~gnc06@S2`GzoOBIuB?0zp8M0tf<<6hIJ=qyU0| zB!vkOkfZ>DfFuPFgnuq6NOU8@7TgMpVh4^29-@-6uHR-Hchj14uAs9-d&@8N{q)1l z>FZJpH|iH!5_1JBS@&`e@638YESME{U)9gg@QyLpGA?V$&r0g{nswf|1mF_FBG~9`X)a`nfRx*uCZ$_rrIn2d+(XM=g-qQb#)v`yO`-NED7?+ zVaPhhfK!$Xm*UXABMuF$BnGwFR@a^R+u5^c6TE6o4BW0NK3u+L{J7j2!mvivbHnh?7)L=ow}*nRM>84bJ?ppKL|hX3{67Bfa@5Dk`MD z7IoLJ8FF|WTR;uI{GmV9`HQithMOMsQtjp|C`e8#$)?bbu~WwBc4^^xBvQtiuInM4 z}zLwfr9Oi~1_89`7}S$VAbb($Zs_H`Pis>+j0 zPnmD~*xO+nvHBK~=)6rJ5RiZW{r8>u=gysze5<#oj%Q?M1~}uQqnRp6)-92G$p$VM z%)=B@$-^-bmE7pvA?@!AX><+y)lJ^ofXDjzm3T%reEs^0`cBz>l##4v^5{dvlTRRu zjYUyDJXTAK*L7X@BaYa2KHGeKd<1v7%&3Toz2D_>^SgaPc(cMH$A-&{YmB8P21R1) z_A}iU>Awa&XpbCxy-!HI5zbVL9*R3E?>j^7-Mg3b?Hk#)C~SP}y;1OIu2Nfy7V7$m zlP7a0*rs;o;~Q!sl|)idA9I*L-#_P2ww*&h*Co|NMviiWr%g>mfAYvA(sj36xp{d5 zN^MsF+_J8&F5m8&%niSkNFKd^FV)*}+MFO~s_mxDo6kJ+1pDHoXZqSk29q1TyzDr6 zp1PDKraqYtYI#1+U9LjmHk4Pf%`3))j(cwXVaY02+qx-mYj!@oo$DxAD z5?M|*DQZu~itD3vOSid>57#Ou^b1l$l(BtH0~qkDNz~z8n?Y_XR(B7s?WxaR(pHZH zgNnt@6fSj7H;p3E17Zp!63LQk3r^vl`iJYF7sh(sclm6b(H1zLKW2O?VvdwQH! z?3DL5xWC;O*>L*X=gUMj8$*5l_Ii^!w4!%@`Z&BKl|C$>j_1ytIg|Ny`0{0`o)O;X zjUNcdMwiER^-*%<@z*MyHjtNrog78MJ#QoYZ4~YHZg&SG-qtOvKBnJ&>}z92ucD0M zoZ?Rt)0HNt4}5q|=Ss!nx5;{vkr7;DZhC^3yXjkb11K{Ik9IGlSc6R?{I|n{F#$w> z?xXvHkbdR}_<3>Gn|QI@mjYK^9{E~oGEINIpeNv_S3Wm%w6m+4DCp$|xl%;~Y<2M| zZ4!&^MPUXpXThzXGA1_cHA=X1H-=F*XY3`)$Wmm_+HhD}!rWfIs@!{||l zK_WE+&sUy6f-5w{f7{iD7o5ire{yQh1*4L}WG01t>8y993Q!hu!D^S1p%kOQ56_bu z@k;K6iOFiD>9>B52m5r_M9Sddj$TGf@~#FM{n`<l3rkQoF1-3r@=3EjJP; z&7*a7cRTs@%~JDGq`5`-KWL8-mae95HZX9qv~|(w zd*hMDK!9EL#})fx-r4TZmfwqhyFnb^VWWA#d4ub|IYswv7w*}A>EqRndiYb$)$3dq zx3kGv zxtU017STPU{QL5ii{AL^6T#lTvTv2m)kFp>%O-_&Y|G)#6q%FC$%Vpclq{R;p%nj< zCzA^c?b&U`Vd`c4fQX0)5AWRE+~m~MR8?;^8w1@wHcU^AzlhARmXj_ zJ3oLq{Jp8QHL^bgJVBEZly$d@inFDeWn+@T``uDTeYSnd{rg%eW-(HcL2YZq@Ynl! z^zT(cwQ}E%#VGWPfP_;b{&n8RszbGH`A>#lS=73?xv2>R4EDLX%9$!L{@?*pCyb}c zEgWwaQ#Dq2Ci2)RDI!q|D3zDD84OX2T9XiddZ2(K=DoftemiJzGIA|bmRMq=dJ9oB zmf7Zar4eDQ;lb{VC*C+5?tQmic732!!tjieexG{|qobq41CM6jn6!h<=NHe-oF!Yf z$z)d7;BX{04pYGO)2A~SnHZv7w&#|cz0F*3$1RHlaf<3{wzf8`V56u{TV-NEF|Tl* zypJ6SeTEyHBM;};HmExsh==ubU4}hog7s)wL9rdX3u@e{w%Hu;K;s+uqF5Css5$x;Inc}UKX#R!s0<+oAD2?KRvBL}a%T-Ere z>k-w7`m|I{p1rYVr>Ff34|k*#l;vyJcXL+lq4LN9eZ@r=+p}B6m!L7usdL z05Z#v(*!w9esJL*Pn*a+qAV(>WCR2j1eU)MKqB zd=*q&@BLX`z9GXuq7lL5Zy*ptAcOz_BpQ%tK#q(bnE;7~A3z{y19CPXXM-#&AklzC z0}>5LG=K;osQ^g@NGkZZrh@&&p_5NtHofS;D-Yvc7RpVo5op28h@nsu z6arWXV}>7qG6lhcZ5@}Ea;s7-7D}tsQjl9~ozRw+9rh;}vR~FO`M&4--khBGoacSd z`##^z^YPJ0TUT2E05azL$V31vyZ|uA*;qqMbMdu22xbL|(Gh?#>%I;@EDIuHF51AW z+=f^UfaBem$ncA$cej=ks__a`<*VF`;-86)*{+B`ni!TBzUp@^srv19B%(dB-67E) z`P^Up!uFHlfce|i`0>Q<-y@IP6H(VI@7X?^sr`tW7v5eIfkq7uw#=UY)fgC8AuMb~ zIJnPhx0#%+DU7m}DJG%a;3BGgO0Gh|$XBcLb?<@+2S&r`R8H|sM>GXpye-ip zsYn1MwF3+Wqi!KhZ}`y(_1)WPX95)!TuiOdzpsVZ-4iFb;M1nf$rB7m!tm$fG6~a4<7hCc#=uenSJ@{6@+|c z+>s00)eQ#>Iw>hBXOD?A*oo9~UmC)5U#c_C&CL}p;2)`srABRfov>5p;`>uYtp5MYTaKs^hc`IYNyau0LDt?$ThZR7WYA7>qrCzZUdDAxWvUKP|Ci2LX z4P>YIL4EOOxJIKnT}w;pS<0&>|3f1TIkSrGplU3PQSyUgc#l>@UGkK(D+1u zDOpcft43+mh4h2bsA$;ZwvW59tv2|lC?SX8aL4F0>ai18-D0fm0%>U=`|;nRHZDx$ zI)mP^+Y@``b(4Cfh9g~rBH;->BM03wGwGX($D@Sng_{zluqPR=iU0agNRZO_XV;*E zvz74i`H@Kragg!7AsAx;{lx59!N^Y79E0KY4g;W)3LyFZ=Kmw(^h{=-nC=1Te&N1v z4&g}Tn^>o4q_BhmFmNqn5)!&)dHpj;@62C5IFnX1>ExqHGSe@{VPX6rbz!3JzoKr{ Z^M^BE94MkD6JV@B%(?hTM#QCS{{ZqPZH@o{ diff --git a/forui/test/src/widgets/accordion/accordion_controller_test.dart b/forui/test/src/widgets/accordion/accordion_controller_test.dart index 44113c31f..6818850f6 100644 --- a/forui/test/src/widgets/accordion/accordion_controller_test.dart +++ b/forui/test/src/widgets/accordion/accordion_controller_test.dart @@ -8,11 +8,9 @@ import 'package:mockito/mockito.dart'; import 'package:forui/forui.dart'; import 'accordion_controller_test.mocks.dart'; -@GenerateNiceMocks([MockSpec()]) -@GenerateNiceMocks([MockSpec()]) -void _setup(List animationControllers, List> animations, int length) { - animations.clear(); - animationControllers.clear(); +(List, List) _setup(int length) { + final animationControllers = []; + final animations = []; for (int i = 0; i < length; i++) { animationControllers.add(MockAnimationController()); @@ -27,26 +25,32 @@ void _setup(List animationControllers, List> return TickerFuture.complete(); }); } + + return (animationControllers, animations); } -void _tearDown(List animationControllers, length) { - for (int i = 0; i < length; i++) { - animationControllers[i].dispose(); +void _tearDown(List animationControllers) { + for (final controller in animationControllers) { + controller.dispose(); } } +@GenerateNiceMocks([MockSpec()]) +@GenerateNiceMocks([MockSpec()]) void main() { group('FAccordionController', () { late FAccordionController controller; - final List animationControllers = []; - final List> animations = []; + List animationControllers = []; + List> animations = []; int count = 0; int length = 3; setUp(() { count = 0; length = 3; - _setup(animationControllers, animations, length); + final record = _setup(length); + animationControllers = List.from(record.$1); + animations = List.from(record.$2); controller = FAccordionController(min: 1, max: 2) ..addListener(() { count++; @@ -54,7 +58,7 @@ void main() { }); tearDown(() { - _tearDown(animationControllers, length); + _tearDown(animationControllers); controller.dispose(); }); @@ -62,6 +66,7 @@ void main() { test('sets animation controller value based on initiallyExpanded', () async { await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); verify(animationControllers[0].value = 0); + await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); verify(animationControllers[0].value = 1); }); @@ -69,6 +74,7 @@ void main() { test('adds to expanded list', () { controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); expect(controller.expanded.length, 0); + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); expect(controller.expanded.length, 1); expect(controller.controllers.length, 1); @@ -85,7 +91,9 @@ void main() { group('removeItem(...)', () { setUp(() { length = 1; - _setup(animationControllers, animations, length); + final record = _setup(length); + animationControllers = List.from(record.$1); + animations = List.from(record.$2); controller = FAccordionController(min: 1, max: 2) ..addListener(() { count++; @@ -93,13 +101,14 @@ void main() { }); tearDown(() { - _tearDown(animationControllers, length); + _tearDown(animationControllers); }); test('removes from the expanded list', () { controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); expect(controller.removeItem(0), true); expect(controller.removeItem(0), false); }); + test('aware of min limit', () { controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); expect(controller.removeItem(0), false); @@ -110,6 +119,7 @@ void main() { test('expands an item', () async { await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); expect(controller.controllers[0]?.animation.value, 0); + await controller.toggle(0); expect(controller.controllers[0]?.animation.value, 100); }); @@ -119,6 +129,7 @@ void main() { await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: true); await animationControllers[0].forward(); expect(controller.controllers[0]?.animation.value, 100); + await controller.toggle(0); expect(controller.controllers[0]?.animation.value, 0); }); @@ -127,6 +138,7 @@ void main() { await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); await controller.toggle(1); expect(count, 0); + await controller.toggle(0); expect(count, 1); }); @@ -161,6 +173,7 @@ void main() { await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); await controller.expand(0); expect(controller.expanded, {0}); + await controller.expand(0); expect(count, 1); await controller.expand(1); @@ -179,7 +192,9 @@ void main() { group('collapse(...)', () { setUp(() { length = 2; - _setup(animationControllers, animations, length); + final record = _setup(length); + animationControllers = List.from(record.$1); + animations = List.from(record.$2); controller = FAccordionController(min: 1, max: 2) ..addListener(() { count++; @@ -187,7 +202,7 @@ void main() { }); tearDown(() { - _tearDown(animationControllers, length); + _tearDown(animationControllers); }); test('does not call notifyListener on invalid index', () async { @@ -195,6 +210,7 @@ void main() { await controller.addItem(1, animationControllers[1], animations[1], initiallyExpanded: true); await controller.collapse(0); expect(controller.expanded, {1}); + await controller.collapse(0); expect(count, 1); await controller.collapse(2); @@ -218,15 +234,17 @@ void main() { group('FAccordionController.radio', () { late FAccordionController controller; - final List animationControllers = []; - final List> animations = []; + List animationControllers = []; + List> animations = []; int count = 0; int length = 2; setUp(() { count = 0; length = 2; - _setup(animationControllers, animations, length); + final record = _setup(length); + animationControllers = List.from(record.$1); + animations = List.from(record.$2); controller = FAccordionController.radio() ..addListener(() { count++; @@ -234,7 +252,7 @@ void main() { }); tearDown(() { - _tearDown(animationControllers, length); + _tearDown(animationControllers); controller.dispose(); }); @@ -242,6 +260,7 @@ void main() { test('adds to expanded list', () { controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); expect(controller.expanded.length, 0); + controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); expect(controller.expanded.length, 1); expect(controller.controllers.length, 1); @@ -256,7 +275,9 @@ void main() { group('removeItem(...)', () { setUp(() { length = 1; - _setup(animationControllers, animations, length); + final record = _setup(length); + animationControllers = List.from(record.$1); + animations = List.from(record.$2); controller = FAccordionController.radio() ..addListener(() { count++; @@ -264,7 +285,7 @@ void main() { }); tearDown(() { - _tearDown(animationControllers, length); + _tearDown(animationControllers); }); test('removes from the expanded list', () { @@ -282,6 +303,7 @@ void main() { test('expands an item', () async { await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); expect(controller.controllers[0]?.animation.value, 0); + await controller.toggle(0); expect(controller.controllers[0]?.animation.value, 100); }); @@ -290,6 +312,7 @@ void main() { await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: true); await animationControllers[0].forward(); expect(controller.controllers[0]?.animation.value, 100); + await controller.toggle(0); expect(controller.controllers[0]?.animation.value, 0); }); @@ -297,6 +320,7 @@ void main() { test('invalid index', () async { await controller.addItem(0, animationControllers[0], animations[0], initiallyExpanded: false); await controller.toggle(1); + expect(count, 0); await controller.toggle(0); expect(count, 1); From 05eca3b83fbad2e41fe6531965a22c532129e24c Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Wed, 2 Oct 2024 15:20:11 +0800 Subject: [PATCH 56/57] Apply suggestions from code review Co-authored-by: Matthias Ngeo --- docs/pages/docs/accordion.mdx | 4 ++-- forui/lib/src/widgets/accordion/accordion.dart | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/pages/docs/accordion.mdx b/docs/pages/docs/accordion.mdx index c8090b4ca..180687452 100644 --- a/docs/pages/docs/accordion.mdx +++ b/docs/pages/docs/accordion.mdx @@ -27,8 +27,8 @@ A vertically stacked set of interactive headings that each reveal a section of c child: const Text('Yes. It adheres to the WAI-ARIA design pattern.'), ), FAccordionItem( - title: const Text('Is it Styled?'), initiallyExpanded: true, + title: const Text('Is it Styled?'), child: const Text( "Yes. It comes with default styles that matches the other components' aesthetics", ), @@ -121,8 +121,8 @@ FAccordion( child: const Text('Yes. It adheres to the WAI-ARIA design pattern.'), ), FAccordionItem( - title: const Text('Is it Styled?'), initiallyExpanded: true, + title: const Text('Is it Styled?'), child: const Text( "Yes. It comes with default styles that matches the other components' aesthetics", ), diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index 78b5d1f40..41fbbe17c 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -22,11 +22,11 @@ class FAccordion extends StatefulWidget { /// * [FAccordionController.radio] for a radio-like selection. final FAccordionController? controller; - /// The items. - final List children; - /// The style. Defaults to [FThemeData.accordionStyle]. final FAccordionStyle? style; + + /// The items. + final List children; /// Creates a [FAccordion]. const FAccordion({ From 7d14f1a33d4529aaa2d775e2ed19883d96f2c80f Mon Sep 17 00:00:00 2001 From: Daviiddoo Date: Wed, 2 Oct 2024 07:22:17 +0000 Subject: [PATCH 57/57] Commit from GitHub Actions (Forui Presubmit) --- forui/lib/src/widgets/accordion/accordion.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forui/lib/src/widgets/accordion/accordion.dart b/forui/lib/src/widgets/accordion/accordion.dart index 41fbbe17c..da89d97db 100644 --- a/forui/lib/src/widgets/accordion/accordion.dart +++ b/forui/lib/src/widgets/accordion/accordion.dart @@ -24,7 +24,7 @@ class FAccordion extends StatefulWidget { /// The style. Defaults to [FThemeData.accordionStyle]. final FAccordionStyle? style; - + /// The items. final List children;