diff --git a/.gitignore b/.gitignore index f60b1a0..431f241 100644 --- a/.gitignore +++ b/.gitignore @@ -271,3 +271,4 @@ example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig example/macos/Flutter/ephemeral/flutter_export_environment.sh example/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows example/windows/flutter/ephemeral/.plugin_symlinks/risto_widgets +/auto_git diff --git a/example/lib/main.dart b/example/lib/main.dart index 7c32802..404f696 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,13 @@ import 'package:flutter/material.dart'; import 'package:risto_widgets/risto_widgets.dart'; +// Import the pages +import 'pages/action_button_page.dart'; +import 'pages/custom_sheet_page.dart'; +import 'pages/expandable_page.dart'; +import 'pages/increment_decrement_page.dart'; +import 'pages/list_tile_button_page.dart'; + void main() { runApp(const MyApp()); } @@ -28,6 +35,41 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { + final List _navigationItems = []; + + @override + void initState() { + super.initState(); + + _navigationItems.addAll([ + NavigationItem( + page: const IncrementDecrementPage(), + icon: const Icon(Icons.home), + label: 'Increment/Decrement', + ), + NavigationItem( + page: const ActionButtonPage(), + icon: const Icon(Icons.search), + label: 'Action Buttons', + ), + NavigationItem( + page: const ListTileButtonPage(), + icon: const Icon(Icons.list), + label: 'List Tile Buttons', + ), + NavigationItem( + page: const ExpandablePage(), + icon: const Icon(Icons.expand), + label: 'Expandable', + ), + NavigationItem( + page: const CustomSheetPage(), + icon: const Icon(Icons.open_in_new), + label: 'Custom Sheets', + ), + ]); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -42,347 +84,8 @@ class _HomePageState extends State { unselectedItemColor: Colors.blue, elevation: 8.0, itemPadding: const EdgeInsets.all(5), - items: [ - NavigationItem( - page: _buildIncrementDecrementPage(context), - icon: const Icon( - Icons.home, - color: Colors.blue, - ), - label: 'Increment/Decrement', - ), - NavigationItem( - page: _buildActionButtonPage(context), - icon: const Icon(Icons.search), - label: 'Action Buttons', - ), - NavigationItem( - page: _buildListTileButtonPage(context), - icon: const Icon(Icons.list), - label: 'List Tile Buttons', - ), - NavigationItem( - page: _buildExpandablePage(context), - icon: const Icon(Icons.expand), - label: 'Expandable', - ), - NavigationItem( - page: _buildCustomSheetPage(context), - icon: const Icon(Icons.open_in_new), - label: 'Custom Sheets', - ), - ], + items: _navigationItems, ), ); } - - // Increment/Decrement Page - Widget _buildIncrementDecrementPage(BuildContext context) { - return StatefulBuilder( - builder: (context, setState) { - int quantity1 = 1; - int quantity2 = 5; - int quantity3 = 8; - - return PaddedChildrenList( - children: [ - Text( - 'Increment/Decrement Widget', - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 16), - - // Flat Example - StatefulBuilder( - builder: (context, setState) { - return IncrementDecrementWidget.flat( - quantity: quantity1, - maxQuantity: 10, - minValue: 1, - onIncrement: () { - setState(() { - if (quantity1 < 10) quantity1++; - }); - }, - onDecrement: () { - setState(() { - if (quantity1 > 1) quantity1--; - }); - }, - backgroundColor: Colors.grey[200], - iconColor: Colors.blue, - ); - }, - ), - - const SizedBox(height: 16), - - // Raised Example - StatefulBuilder( - builder: (context, setState) { - return IncrementDecrementWidget.raised( - quantity: quantity2, - maxQuantity: 15, - minValue: 0, - onIncrement: () { - setState(() { - if (quantity2 < 15) quantity2++; - }); - }, - onDecrement: () { - setState(() { - if (quantity2 > 0) quantity2--; - }); - }, - backgroundColor: Colors.lightGreen[100], - iconColor: Colors.green, - ); - }, - ), - - const SizedBox(height: 16), - - // Minimal Example - StatefulBuilder( - builder: (context, setState) { - return IncrementDecrementWidget.minimal( - quantity: quantity3, - maxQuantity: 20, - minValue: 5, - onIncrement: () { - setState(() { - if (quantity3 < 20) quantity3++; - }); - }, - onDecrement: () { - setState(() { - if (quantity3 > 5) quantity3--; - }); - }, - iconColor: Colors.red, - ); - }, - ), - ], - ); - }, - ); - } - - // Action Buttons Page - Widget _buildActionButtonPage(BuildContext context) { - int counter = 0; - - return StatefulBuilder( - builder: (context, setState) { - return PaddedChildrenList( - children: [ - Text( - 'Custom Action Buttons', - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 16), - // Raised Button Example - CustomActionButton.raised( - onPressed: () { - setState(() { - counter++; - }); - }, - backgroundColor: Colors.blue, - borderColor: Colors.blueAccent, - elevation: 4.0, - borderRadius: 8.0, - child: Text('Raised Button ($counter)', - style: const TextStyle(color: Colors.white)), - ), - const SizedBox(height: 16), - // Flat Button Example - CustomActionButton.flat( - onPressed: () { - setState(() { - counter++; - }); - }, - backgroundColor: Colors.green, - borderColor: Colors.transparent, - borderRadius: 8.0, - child: Text('Flat Button ($counter)', - style: const TextStyle(color: Colors.white)), - ), - const SizedBox(height: 16), - // Minimal Button Example - CustomActionButton.minimal( - onPressed: () { - setState(() { - counter++; - }); - }, - child: Text('Minimal Button ($counter)', - style: const TextStyle(color: Colors.black)), - ), - ], - ); - }, - ); - } - - // List Tile Buttons Page - Widget _buildListTileButtonPage(BuildContext context) { - return PaddedChildrenList( - children: [ - Text( - 'List Tile Button', - style: Theme.of(context).textTheme.titleLarge, - ), - IconListTileButton( - title: const Text('List Tile Button'), - icon: Icons.list, - subtitle: const Text('Subtitle'), - onPressed: () {}, - padding: const EdgeInsets.symmetric(vertical: 8.0), - backgroundColor: Colors.white, - borderColor: Colors.blue, - iconColor: Colors.blue, - trailing: const Icon(Icons.arrow_forward), - size: 80, - ), - ListTileButton( - body: const Text('List Tile Button'), - subtitle: const Text('Subtitle'), - onPressed: () {}, - padding: const EdgeInsets.symmetric(vertical: 8.0), - backgroundColor: Colors.white, - borderColor: Colors.blue, - trailing: const Icon(Icons.arrow_forward), - ), - const SizedBox(height: 20), - Text( - 'Double List Tile Buttons', - style: Theme.of(context).textTheme.titleLarge, - ), - DoubleListTileButtons( - firstButton: ListTileButton( - body: const Center(child: Text('First Button')), - onPressed: () {}, - backgroundColor: Colors.red, - ), - secondButton: ListTileButton( - body: const Center(child: Text('Second Button')), - onPressed: () {}, - backgroundColor: Colors.green, - ), - padding: const EdgeInsets.symmetric(vertical: 8.0), - space: 16.0, - ), - ], - ); - } - - // Expandable Widget Page - Widget _buildExpandablePage(BuildContext context) { - return PaddedChildrenList( - children: [ - Text( - 'Expandable List Tile Button', - style: Theme.of(context).textTheme.titleLarge, - ), - // Add some space between title and first item - const SizedBox(height: 20), - - // Standard Expandable List Tile Button - ExpandableListTileButton.listTile( - backgroundColor: Colors.blueGrey[600], - expandedColor: Colors.blue[300], - expanded: Container( - width: double.infinity, - padding: const EdgeInsets.all(16.0), - child: const Text('Expanded content goes here'), - ), - buttonBody: const Text( - 'Expandable ListTile Button', - style: TextStyle(color: Colors.white), - ), - trailing: const Icon(Icons.arrow_drop_down, color: Colors.white), - ), - const SizedBox(height: 20), - // Space between items - - // Icon List Tile Button with Expandable - ExpandableListTileButton.iconListTile( - backgroundColor: Colors.blueGrey[600], - expandedColor: Colors.blue[300], - expanded: Container( - width: double.infinity, - padding: const EdgeInsets.all(16.0), - child: const Text('Expanded content goes here'), - ), - buttonBody: const Text( - 'Expandable IconListTile Button', - style: TextStyle(color: Colors.white), - ), - icon: Icons.account_circle, - iconColor: Colors.white, - trailing: const Icon(Icons.arrow_drop_down, color: Colors.white), - ), - const SizedBox(height: 20), - // Space between items - - // Custom Header with Expandable Content - ExpandableListTileButton.custom( - backgroundColor: Colors.blueGrey[600], - expandedColor: Colors.blue[300], - expanded: Container( - width: double.infinity, - padding: const EdgeInsets.all(16.0), - child: const Text('Expanded content goes here'), - ), - customHeader: (tapAction) => CustomActionButton( - backgroundColor: Colors.blueGrey[600], - margin: EdgeInsets.zero, - onPressed: () => tapAction.call(), - child: const Center( - child: Text( - 'Expandable CustomHeader Button', - style: TextStyle(color: Colors.white), - ), - ), - ), - ), - // Space after the last item - ], - ); - } - - // Custom Sheets Page - Widget _buildCustomSheetPage(BuildContext context) { - return PaddedChildrenList( - children: [ - Text( - 'Custom Sheet Example', - style: Theme.of(context).textTheme.titleLarge, - ), - ElevatedButton( - onPressed: () { - OpenCustomSheet.openConfirmSheet( - context, - body: const Text('Are you sure?'), - onClose: (value) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(value == true ? 'Confirmed' : 'Closed'), - )); - }, - backgroundColor: Colors.white, - handleColor: Colors.grey, - buttonColor: Colors.blue, - buttonTextColor: Colors.white, - padding: const EdgeInsets.all(16.0), - buttonSpacing: 16.0, - ); - }, - child: const Text('Open Custom Sheet'), - ), - ], - ); - } } diff --git a/example/lib/pages/action_button_page.dart b/example/lib/pages/action_button_page.dart new file mode 100644 index 0000000..72f4a21 --- /dev/null +++ b/example/lib/pages/action_button_page.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:risto_widgets/risto_widgets.dart'; + +class ActionButtonPage extends StatefulWidget { + const ActionButtonPage({super.key}); + + @override + State createState() => _ActionButtonPageState(); +} + +class _ActionButtonPageState extends State { + int counter = 0; + + @override + Widget build(BuildContext context) { + return PaddedChildrenList( + children: [ + Text( + 'Custom Action Buttons', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + // Elevated Button Example + CustomActionButton.elevated( + onPressed: () { + setState(() { + counter++; + }); + }, + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + elevation: 4.0, + borderRadius: 8.0, + child: Text('Elevated Button ($counter)'), + ), + const SizedBox(height: 16), + // Flat Button Example + CustomActionButton.flat( + onPressed: () { + setState(() { + counter++; + }); + }, + backgroundColor: Colors.green, + splashColor: Colors.white.withOpacity(0.2), + borderRadius: 8.0, + child: Text('Flat Button ($counter)', + style: const TextStyle(color: Colors.white)), + ), + const SizedBox(height: 16), + // Minimal Button Example + CustomActionButton.minimal( + onPressed: () { + setState(() { + counter++; + }); + }, + child: Text('Minimal Button ($counter)', + style: const TextStyle(color: Colors.black)), + ), + const SizedBox(height: 16), + // Long Press Button Example + CustomActionButton.longPress( + onPressed: () { + setState(() { + counter++; + }); + }, + onLongPress: () { + setState(() { + counter += 2; + }); + }, + backgroundColor: Colors.red, + foregroundColor: Colors.white, + elevation: 4.0, + borderRadius: 8.0, + child: Text('Long Press Button ($counter)'), + ), + const SizedBox(height: 16), + + CustomActionButton( + elevation: 4.0, + child: Text( + 'Disabled Press Button ($counter)', + style: const TextStyle(color: Colors.white), + ), + ), + ], + ); + } +} diff --git a/example/lib/pages/custom_sheet_page.dart b/example/lib/pages/custom_sheet_page.dart new file mode 100644 index 0000000..f4c02c6 --- /dev/null +++ b/example/lib/pages/custom_sheet_page.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:risto_widgets/risto_widgets.dart'; + +class CustomSheetPage extends StatelessWidget { + const CustomSheetPage({super.key}); + + @override + Widget build(BuildContext context) { + return PaddedChildrenList( + children: [ + Text( + 'Custom Sheet Example', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + OpenCustomSheet.openConfirmSheet( + context, + body: const Text('Are you sure you want to proceed?'), + onClose: (value) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + value == true ? 'Action Confirmed' : 'Action Cancelled'), + )); + }, + backgroundColor: Colors.white, + handleColor: Colors.grey, + firstButtonColor: Colors.red, + secondButtonColor: Colors.green, + firstButtonTextColor: Colors.white, + secondButtonTextColor: Colors.white, + buttonSpacing: 8.0, + ).show(context); + }, + child: const Text('Open Confirm Sheet'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + OpenCustomSheet.scrollableSheet( + context, + expand: false, + initialChildSize: 0.5, + minChildSize: 0.25, + maxChildSize: 1.0, + body: ({scrollController}) { + return ListView.builder( + controller: scrollController, + itemCount: 50, + itemBuilder: (context, index) => ListTile( + title: Text('Item $index'), + ), + ); + }, + ).show(context); + }, + child: const Text('Open Scrollable Sheet'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + OpenCustomSheet( + scrollable: false, + expand: false, + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 0.8, + barrierColor: Colors.black.withOpacity(0.5), + body: ({scrollController}) => Container( + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + padding: const EdgeInsets.only( + top: 16, + bottom: 32, + left: 16, + right: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Enter Details', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + const TextField( + decoration: InputDecoration( + labelText: 'Name', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + const TextField( + decoration: InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 24), + CustomActionButton.flat( + onPressed: () => Navigator.pop(context, 'Submitted'), + backgroundColor: Colors.green, + margin: EdgeInsets.zero, + child: const Text( + 'Submit', + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + ), + ).show(context); + }, + child: const Text('Open Custom Form Sheet'), + ), + ], + ); + } +} diff --git a/example/lib/pages/expandable_page.dart b/example/lib/pages/expandable_page.dart new file mode 100644 index 0000000..6efc95e --- /dev/null +++ b/example/lib/pages/expandable_page.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:risto_widgets/risto_widgets.dart'; + +class ExpandablePage extends StatelessWidget { + const ExpandablePage({super.key}); + + @override + Widget build(BuildContext context) { + return PaddedChildrenList( + children: [ + Text( + 'Expandable List Tile Button', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 20), + + // Standard Expandable List Tile Button + ExpandableListTileButton.listTile( + backgroundColor: Colors.blueGrey[600], + expandedColor: Colors.blue[300], + expanded: Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + child: const Text('Expanded content goes here'), + ), + title: const Text( + 'Expandable ListTile Button', + style: TextStyle(color: Colors.white), + ), + subtitle: const Text( + 'Subtitle Text', + style: TextStyle(color: Colors.white70), + ), + ), + const SizedBox(height: 20), + + // Icon List Tile Button with Expandable + ExpandableListTileButton.iconListTile( + backgroundColor: Colors.blueGrey[600], + expandedColor: Colors.blue[300], + expanded: Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + child: const Text('Expanded content goes here'), + ), + title: const Text( + 'Expandable IconListTile Button', + style: TextStyle(color: Colors.white), + ), + subtitle: const Text( + 'Subtitle Text', + style: TextStyle(color: Colors.white70), + ), + icon: Icons.account_circle, + iconColor: Colors.white, + sizeFactor: 2, + ), + const SizedBox(height: 20), + + // Custom Header with Expandable Content + ExpandableListTileButton.custom( + backgroundColor: Colors.blueGrey[600], + expandedColor: Colors.blue[300], + expanded: Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + child: const Text('Expanded content goes here'), + ), + customHeader: (tapAction, isExpanded) => CustomActionButton( + backgroundColor: Colors.blueGrey[600], + margin: EdgeInsets.zero, + onPressed: () => tapAction.call(), + child: const Center( + child: Text( + 'Expandable CustomHeader Button', + style: TextStyle(color: Colors.white), + ), + ), + ), + ), + ], + ); + } +} diff --git a/example/lib/pages/increment_decrement_page.dart b/example/lib/pages/increment_decrement_page.dart new file mode 100644 index 0000000..bd0caef --- /dev/null +++ b/example/lib/pages/increment_decrement_page.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:risto_widgets/risto_widgets.dart'; + +class IncrementDecrementPage extends StatefulWidget { + const IncrementDecrementPage({super.key}); + + @override + State createState() => _IncrementDecrementPageState(); +} + +class _IncrementDecrementPageState extends State { + int quantity1 = 1; + int quantity2 = 5; + int quantity3 = 8; + int quantity4 = 2; + int quantity5 = 3; // For asynchronous callback example + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Increment/Decrement Examples'), + ), + body: PaddedChildrenList( + children: [ + // Flat Example with synchronous callback returning int? + IncrementDecrementWidget.flat( + quantity: quantity1, + maxQuantity: 10, + minValue: 1, + onChanged: (newQuantity) { + setState(() { + quantity1 = newQuantity; + }); + return newQuantity; // Returning int? is acceptable + }, + backgroundColor: Colors.grey[200], + iconColor: Colors.blue, + ), + + const SizedBox(height: 16), + + // Raised Example with synchronous callback returning int? + IncrementDecrementWidget.raised( + quantity: quantity2, + maxQuantity: 15, + minValue: 0, + onChanged: (newQuantity) { + setState(() { + quantity2 = newQuantity; + }); + return newQuantity; // Returning int? is acceptable + }, + backgroundColor: Colors.lightGreen[100], + iconColor: Colors.green, + ), + + const SizedBox(height: 16), + + // Minimal Example with synchronous callback returning int? + IncrementDecrementWidget.minimal( + quantity: quantity3, + maxQuantity: 20, + minValue: 5, + onChanged: (newQuantity) { + setState(() { + quantity3 = newQuantity; + }); + return newQuantity; // Returning int? is acceptable + }, + iconColor: Colors.red, + ), + + const SizedBox(height: 16), + + // Squared Buttons Example with synchronous callback returning void + IncrementDecrementWidget.squared( + quantity: quantity4, + alignment: MainAxisAlignment.center, + onChanged: (newQuantity) { + setState(() { + quantity4 = newQuantity; + }); + // No return statement, implicitly returns null (void) + }, + backgroundColor: Colors.orange[100], + iconColor: Colors.orange, + buttonSize: 60.0, + borderRadius: 8.0, + incrementIcon: const Icon(Icons.add), + decrementIcon: const Icon(Icons.remove), + ), + + const SizedBox(height: 16), + + // Asynchronous Callback Example + IncrementDecrementWidget.raised( + quantity: quantity5, + maxQuantity: 10, + minValue: 1, + onChanged: (newQuantity) async { + // Simulate an asynchronous operation, e.g., server validation + await Future.delayed(const Duration(milliseconds: 100)); + // Optionally modify the quantity + int updatedQuantity = newQuantity; // Example modification + // Ensure that the updated quantity does not exceed maxQuantity + if (updatedQuantity > 10) { + updatedQuantity = 10; + } + setState(() { + quantity5 = updatedQuantity; + }); + return updatedQuantity; // Returning Future + }, + backgroundColor: Colors.purple[100], + iconColor: Colors.purple, + elevation: 4.0, + buttonHeight: 50.0, + borderRadius: 12.0, + incrementIcon: const Icon(Icons.add), + decrementIcon: const Icon(Icons.remove), + ), + + const SizedBox(height: 16), + + // Displaying current quantities for clarity + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Quantity1 (Flat): $quantity1'), + Text('Quantity2 (Raised): $quantity2'), + Text('Quantity3 (Minimal): $quantity3'), + Text('Quantity4 (Squared): $quantity4'), + Text('Quantity5 (Async Raised): $quantity5'), + ], + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/list_tile_button_page.dart b/example/lib/pages/list_tile_button_page.dart new file mode 100644 index 0000000..faafa1a --- /dev/null +++ b/example/lib/pages/list_tile_button_page.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:risto_widgets/risto_widgets.dart'; + +class ListTileButtonPage extends StatelessWidget { + const ListTileButtonPage({super.key}); + + @override + Widget build(BuildContext context) { + return PaddedChildrenList( + children: [ + Text( + 'List Tile Button Examples', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + + // ListTileButton with Elevation + ListTileButton( + body: const Text('List Tile Button with Elevation'), + subtitle: const Text('Subtitle Text'), + onPressed: () {}, + backgroundColor: Colors.white, + borderColor: Colors.blue, + elevation: 4.0, + trailing: const Icon(Icons.arrow_forward), + minHeight: 90, + ), + const SizedBox(height: 16), + + // ListTileButton without Elevation + ListTileButton( + body: const Text('List Tile Button without Elevation'), + subtitle: const Text('Subtitle Text'), + onPressed: () {}, + backgroundColor: Colors.white, + borderColor: Colors.blue, + trailing: const Icon(Icons.arrow_forward), + ), + const SizedBox(height: 16), + + ListTileButton( + body: const Text('List Tile Button with icon'), + backgroundColor: Colors.white, + subtitle: const Text('Subtitle Text'), + trailing: Icon( + Icons.error, + size: 18, + color: Theme.of(context).iconTheme.color, + ), + ), + const SizedBox(height: 16), + + // IconListTileButton with Elevation + IconListTileButton( + title: const Text('Icon List Tile with Elevation'), + icon: Icons.star, + subtitle: const Text('Subtitle Text'), + onPressed: () {}, + backgroundColor: Colors.white, + borderColor: Colors.purple, + iconColor: Colors.purple, + elevation: 4.0, + trailing: const Icon(Icons.arrow_forward), + ), + const SizedBox(height: 16), + + // IconListTileButton without Elevation + IconListTileButton( + title: const Text('Icon List Tile without Elevation'), + icon: Icons.star_border, + subtitle: const Text('Subtitle Text'), + onPressed: () {}, + backgroundColor: Colors.white, + borderColor: Colors.purple, + iconColor: Colors.purple, + trailing: const Icon(Icons.arrow_forward), + sizeFactor: 3), + const SizedBox(height: 20), + + Text( + 'Double List Tile Buttons', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + + // Double List Tile Buttons with Elevation + DoubleListTileButtons( + firstButton: ListTileButton( + body: const Center(child: Text('First Button with Elevation')), + onPressed: () {}, + backgroundColor: Colors.red, + elevation: 2.0, + ), + secondButton: ListTileButton( + body: const Center(child: Text('Second Button with Elevation')), + onPressed: () {}, + backgroundColor: Colors.green, + elevation: 2.0, + ), + padding: const EdgeInsets.symmetric(vertical: 8.0), + space: 16.0, + ), + const SizedBox(height: 16), + + // Double List Tile Buttons without Elevation + DoubleListTileButtons( + firstButton: ListTileButton( + body: const Center(child: Text('First Button without Elevation')), + onPressed: () {}, + backgroundColor: Colors.red, + ), + secondButton: ListTileButton( + body: const Center(child: Text('Second Button without Elevation')), + onPressed: () {}, + backgroundColor: Colors.green, + ), + padding: const EdgeInsets.symmetric(vertical: 8.0), + space: 16.0, + ), + ], + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index a0fa23c..6cbad88 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -400,7 +400,7 @@ packages: path: ".." relative: true source: path - version: "0.0.4" + version: "0.0.5" rxdart: dependency: transitive description: @@ -442,10 +442,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.4+3" stack_trace: dependency: transitive description: @@ -482,10 +482,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+2" term_glyph: dependency: transitive description: @@ -514,10 +514,10 @@ packages: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.0" vector_math: dependency: transitive description: @@ -538,10 +538,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" webdriver: dependency: transitive description: @@ -559,5 +559,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.4.1 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.22.0" diff --git a/lib/risto_widgets.dart b/lib/risto_widgets.dart index a45d752..3678e6f 100644 --- a/lib/risto_widgets.dart +++ b/lib/risto_widgets.dart @@ -1,6 +1,5 @@ library risto_widgets; -export 'widgets/buttons/action_wrapper.dart'; export 'widgets/buttons/custom_action_button.dart'; export 'widgets/buttons/expandable_list_tile_button.dart'; export 'widgets/buttons/list_tile_button.dart'; diff --git a/lib/widgets/buttons/action_wrapper.dart b/lib/widgets/buttons/action_wrapper.dart deleted file mode 100644 index 597b117..0000000 --- a/lib/widgets/buttons/action_wrapper.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -class ActionButtonWrapper extends StatelessWidget { - final EdgeInsetsGeometry _margin; - final double _height; - final double _width; - final Widget child; - final Color? backgroundColor; - final BorderRadius? borderRadius; - - ActionButtonWrapper({ - super.key, - EdgeInsetsGeometry? margin, - double paddingValue = 16.0, // Default value - double? height, - double? width, - required this.child, - this.backgroundColor, - this.borderRadius, - }) : _margin = margin ?? - EdgeInsets.only( - bottom: 8, - right: paddingValue, - left: paddingValue, - ), - _height = height ?? 60, - _width = width ?? double.infinity; - - @override - Widget build(BuildContext context) { - return Container( - margin: _margin, - width: _width, - height: _height, - decoration: BoxDecoration( - color: backgroundColor ?? Colors.transparent, - borderRadius: borderRadius ?? BorderRadius.circular(10), - ), - child: child, - ); - } -} diff --git a/lib/widgets/buttons/custom_action_button.dart b/lib/widgets/buttons/custom_action_button.dart index 85bd944..bd2229a 100644 --- a/lib/widgets/buttons/custom_action_button.dart +++ b/lib/widgets/buttons/custom_action_button.dart @@ -1,275 +1,462 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; -import 'action_wrapper.dart'; +enum ButtonType { elevated, flat, minimal, longPress } + +class CustomActionButton extends StatefulWidget { + final VoidCallback? onPressed; + final VoidCallback? onLongPress; + + final Widget child; + + final ButtonType? buttonType; -class CustomActionButton extends StatelessWidget { final Color? backgroundColor; - final Color? borderColor; final Color? foregroundColor; - final Color? splashColor; // Added splash color - final InteractiveInkFeatureFactory? splashFactory; + final Color? shadowColor; + final Color? splashColor; + final Color? disabledBackgroundColor; + final Color? disabledBorderColor; + final Color? borderColor; + final double? elevation; final double? borderRadius; - final EdgeInsetsGeometry? padding; - final BorderSide? side; - final ShapeBorder? shape; + final double? width; + final double? height; - final void Function()? onPressed; - final Widget? child; - final double? size; // For circular button + final OutlinedBorder? shape; + + final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; - final double? height; - final double? width; + + final InteractiveInkFeatureFactory? splashFactory; const CustomActionButton({ super.key, - this.margin, - this.width, - this.height, + required this.child, + this.buttonType, + this.onPressed, + this.onLongPress, this.backgroundColor, - this.borderColor, this.foregroundColor, + this.shadowColor, this.splashColor, - this.splashFactory, + this.disabledBackgroundColor, + this.disabledBorderColor, + this.borderColor, this.elevation, - this.onPressed, this.borderRadius, - this.padding, - this.side, + this.width, + this.height, this.shape, - required this.child, - this.size, // for circular button + this.padding, + this.margin, + this.splashFactory, }); - // Factory for a flat button with splash effect but no elevation - factory CustomActionButton.flat({ - required void Function()? onPressed, + factory CustomActionButton.elevated({ + required VoidCallback? onPressed, required Widget child, Color? backgroundColor, + Color? foregroundColor, Color? splashColor, + Color? disabledBackgroundColor, + Color? disabledForegroundColor, Color? borderColor, - double borderRadius = 10.0, - InteractiveInkFeatureFactory? splashFactory, + double elevation = 2.0, + double borderRadius = 8.0, + BorderSide? side, + OutlinedBorder? shape, double? width, double? height, - EdgeInsetsGeometry? margin, EdgeInsetsGeometry? padding, + EdgeInsetsGeometry? margin, + InteractiveInkFeatureFactory? splashFactory, }) { return CustomActionButton( + buttonType: ButtonType.elevated, onPressed: onPressed, - backgroundColor: backgroundColor ?? Colors.transparent, - splashFactory: splashFactory ?? InkRipple.splashFactory, - splashColor: splashColor ?? Colors.grey.withOpacity(0.2), - borderColor: borderColor ?? Colors.transparent, + backgroundColor: backgroundColor, + shadowColor: foregroundColor, + splashColor: splashColor, + disabledBackgroundColor: disabledBackgroundColor, + disabledBorderColor: disabledForegroundColor, + borderColor: borderColor, + elevation: elevation, borderRadius: borderRadius, + shape: shape, width: width, height: height, - margin: margin, padding: padding, + margin: margin, + splashFactory: splashFactory, child: child, ); } - // Factory for a raised button with elevation but no splash effect - factory CustomActionButton.raised({ - required void Function()? onPressed, + factory CustomActionButton.flat({ + required VoidCallback? onPressed, required Widget child, Color? backgroundColor, + Color? foregroundColor, + Color? splashColor, + Color? disabledBackgroundColor, + Color? disabledForegroundColor, Color? borderColor, - double borderRadius = 10.0, - double elevation = 6.0, + double borderRadius = 8.0, + BorderSide? side, + OutlinedBorder? shape, double? width, double? height, + EdgeInsetsGeometry? padding, EdgeInsetsGeometry? margin, + InteractiveInkFeatureFactory? splashFactory, }) { return CustomActionButton( + buttonType: ButtonType.flat, onPressed: onPressed, backgroundColor: backgroundColor, - elevation: elevation, - splashFactory: NoSplash.splashFactory, - splashColor: Colors.transparent, + shadowColor: foregroundColor, + splashColor: splashColor, + disabledBackgroundColor: disabledBackgroundColor, + disabledBorderColor: disabledForegroundColor, borderColor: borderColor, borderRadius: borderRadius, + shape: shape, width: width, height: height, + padding: padding, margin: margin, + splashFactory: splashFactory, child: child, ); } - // Factory for a minimal button with no elevation or splash factory CustomActionButton.minimal({ - required void Function()? onPressed, + required VoidCallback? onPressed, required Widget child, + Color? disabledForegroundColor, + Color? borderColor, double? width, double? height, + OutlinedBorder? shape, + EdgeInsetsGeometry? padding, EdgeInsetsGeometry? margin, }) { return CustomActionButton( + buttonType: ButtonType.minimal, onPressed: onPressed, - backgroundColor: Colors.transparent, - borderColor: Colors.transparent, + disabledBorderColor: disabledForegroundColor, + borderColor: borderColor, width: width, height: height, + shape: shape, + padding: padding, margin: margin, child: child, ); } - @override - Widget build(BuildContext context) { - final isCircular = size != null && borderRadius == size! / 2; - - return ActionButtonWrapper( + factory CustomActionButton.longPress({ + required VoidCallback? onPressed, + VoidCallback? onLongPress, + required Widget child, + Color? backgroundColor, + Color? foregroundColor, + Color? splashColor, + Color? disabledBackgroundColor, + Color? disabledForegroundColor, + Color? borderColor, + double elevation = 2.0, + double borderRadius = 8.0, + BorderSide? side, + OutlinedBorder? shape, + double? width, + double? height, + EdgeInsetsGeometry? padding, + EdgeInsetsGeometry? margin, + InteractiveInkFeatureFactory? splashFactory, + }) { + return CustomActionButton( + buttonType: ButtonType.longPress, + onPressed: onPressed, + onLongPress: onLongPress, + backgroundColor: backgroundColor, + shadowColor: foregroundColor, + splashColor: splashColor, + disabledBackgroundColor: disabledBackgroundColor, + disabledBorderColor: disabledForegroundColor, + borderColor: borderColor, + elevation: elevation, + borderRadius: borderRadius, + shape: shape, width: width, height: height, + padding: padding, margin: margin, - borderRadius: BorderRadius.circular(borderRadius ?? 10), - backgroundColor: backgroundColor, - child: onPressed != null - ? ElevatedButton( - style: ButtonStyle( - foregroundColor: WidgetStateProperty.all( - foregroundColor ?? Theme.of(context).primaryColor, - ), - backgroundColor: WidgetStateProperty.all( - backgroundColor ?? Theme.of(context).primaryColor, - ), - padding: WidgetStateProperty.all( - padding ?? EdgeInsets.zero, - ), - side: WidgetStateProperty.all( - side ?? BorderSide(color: borderColor ?? Colors.transparent), - ), - shape: WidgetStateProperty.all( - shape is OutlinedBorder - ? shape as OutlinedBorder - : isCircular - ? const CircleBorder() - : RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(borderRadius ?? 10), - ), - ), - ), - splashFactory: splashFactory, - surfaceTintColor: WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.pressed)) { - return splashColor; // Splash effect only for flat - } - return null; - }, - ), - overlayColor: onPressed != null && elevation == null - ? WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.pressed)) { - return splashColor; // Splash effect only for flat - } - return null; - }, - ) - : null, - // No overlay for raised and minimal - elevation: elevation != null - ? WidgetStateProperty.resolveWith( - (states) { - switch (states.firstOrNull) { - case WidgetState.pressed: - return elevation! + 6; - - default: - return elevation; - } - }, - ) // Elevation for raised - : WidgetStateProperty.all( - 0), // No elevation for flat/minimal - ), - onPressed: onPressed, - child: child, - ) - : CustomActionDisable( - backgroundColor: - backgroundColor ?? Theme.of(context).disabledColor, - borderColor: borderColor ?? Colors.transparent, - child: child, - ), + splashFactory: splashFactory, + child: child, ); } + + @override + State createState() => _CustomActionButtonState(); } -class CustomActionDisable extends StatelessWidget { - final Color backgroundColor; - final Color borderColor; - final Widget? child; +class _CustomActionButtonState extends State { + Timer? _longPressTimer; - const CustomActionDisable({ - super.key, - required this.backgroundColor, - this.borderColor = Colors.transparent, - required this.child, - }); + void _handleLongPress() { + if (widget.onLongPress != null) { + _longPressTimer = Timer.periodic( + const Duration(milliseconds: 100), + (timer) { + widget.onLongPress?.call(); + }, + ); + } + } + + void _cancelLongPress() { + _longPressTimer?.cancel(); + } @override Widget build(BuildContext context) { + if (widget.onPressed == null && widget.buttonType != ButtonType.longPress) { + return _buildDisabledButton(context); + } + + switch (widget.buttonType) { + case ButtonType.minimal: + return _buildMinimalButton(context); + case ButtonType.longPress: + return _buildLongPressButton(context); + case ButtonType.elevated: + return _buildElevatedButton(context); + case ButtonType.flat: + default: + return _buildFlatButton(context); + } + } + + Widget _buildDisabledButton(BuildContext context) { + final ButtonStyle buttonStyle = ElevatedButton.styleFrom( + overlayColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + shadowColor: widget.shadowColor ?? Colors.black, + foregroundColor: widget.foregroundColor ?? Colors.transparent, + backgroundColor: widget.disabledBackgroundColor ?? + widget.backgroundColor ?? + Theme.of(context).primaryColor, + padding: widget.padding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: widget.shape ?? + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(widget.borderRadius ?? 8.0), + side: (widget.disabledBorderColor ?? widget.borderColor) != null + ? BorderSide( + color: widget.disabledBorderColor ?? + widget.borderColor ?? + Colors.transparent, + width: 1) + : BorderSide.none, + ), + elevation: widget.elevation, + ); + return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - color: backgroundColor, - border: Border.fromBorderSide(BorderSide(color: borderColor)), + margin: widget.margin, + width: widget.width, + height: widget.height, + child: AbsorbPointer( + absorbing: true, + child: ElevatedButton( + style: buttonStyle, + onPressed: () {}, + child: widget.child, + ), ), - child: child, ); } + + Widget _buildElevatedButton(BuildContext context) { + final ButtonStyle buttonStyle = ElevatedButton.styleFrom( + foregroundColor: widget.shadowColor ?? Colors.black, + backgroundColor: widget.backgroundColor ?? Theme.of(context).primaryColor, + padding: widget.padding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: widget.shape ?? + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(widget.borderRadius ?? 8.0), + side: widget.borderColor != null + ? BorderSide(color: widget.borderColor!, width: 1) + : BorderSide.none, + ), + elevation: widget.elevation ?? 2.0, + overlayColor: widget.splashColor ?? Colors.transparent, + splashFactory: widget.splashFactory, + ); + + return Container( + margin: widget.margin, + width: widget.width, + height: widget.height, + child: ElevatedButton( + style: buttonStyle, + onPressed: widget.onPressed, + child: widget.child, + ), + ); + } + + Widget _buildFlatButton(BuildContext context) { + final ButtonStyle buttonStyle = TextButton.styleFrom( + foregroundColor: widget.shadowColor ?? Colors.white, + backgroundColor: widget.backgroundColor ?? Theme.of(context).primaryColor, + overlayColor: widget.splashColor ?? Colors.grey.withOpacity(0.2), + padding: widget.padding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: widget.shape ?? + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(widget.borderRadius ?? 8.0), + side: widget.borderColor != null + ? BorderSide(color: widget.borderColor!, width: 1) + : BorderSide.none, + ), + splashFactory: widget.splashFactory ?? InkRipple.splashFactory, + ); + + return Container( + margin: widget.margin, + width: widget.width, + height: widget.height, + child: TextButton( + style: buttonStyle, + onPressed: widget.onPressed, + child: widget.child, + ), + ); + } + + Widget _buildMinimalButton(BuildContext context) { + final ButtonStyle buttonStyle = TextButton.styleFrom( + foregroundColor: Colors.black, + padding: widget.padding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: widget.shape ?? const RoundedRectangleBorder(), + backgroundColor: Colors.transparent, + side: widget.borderColor != null + ? BorderSide(color: widget.borderColor!, width: 1) + : BorderSide.none, + ).copyWith( + overlayColor: WidgetStateProperty.all(Colors.transparent), + splashFactory: NoSplash.splashFactory, + ); + + return Container( + margin: widget.margin, + width: widget.width, + height: widget.height, + child: TextButton( + style: buttonStyle, + onPressed: widget.onPressed, + child: widget.child, + ), + ); + } + + Widget _buildLongPressButton(BuildContext context) { + final ButtonStyle buttonStyle = ElevatedButton.styleFrom( + foregroundColor: widget.shadowColor ?? Colors.white, + backgroundColor: widget.backgroundColor ?? Theme.of(context).primaryColor, + padding: widget.padding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: widget.shape ?? + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(widget.borderRadius ?? 8.0), + side: widget.borderColor != null + ? BorderSide(color: widget.borderColor!, width: 1) + : BorderSide.none, + ), + elevation: widget.elevation ?? 2.0, + ).copyWith( + overlayColor: widget.splashColor != null + ? WidgetStateProperty.all(widget.splashColor) + : null, + splashFactory: widget.splashFactory, + ); + + return Container( + margin: widget.margin, + width: widget.width, + height: widget.height, + child: GestureDetector( + onTap: widget.onPressed, + onLongPressStart: (_) => _handleLongPress(), + onLongPressEnd: (_) => _cancelLongPress(), + child: ElevatedButton( + style: buttonStyle, + onPressed: widget.onPressed, + child: widget.child, + ), + ), + ); + } + + @override + void dispose() { + _longPressTimer?.cancel(); + super.dispose(); + } } class CustomIconText extends StatelessWidget { final IconData icon; final String text; final Color? color; - final MainAxisAlignment axisAlignment; + final MainAxisAlignment mainAxisAlignment; final TextStyle? textStyle; + final double? iconSize; + final double spacing; const CustomIconText({ super.key, required this.icon, required this.text, this.color, - this.axisAlignment = MainAxisAlignment.center, + this.mainAxisAlignment = MainAxisAlignment.center, this.textStyle, + this.iconSize, + this.spacing = 8.0, }); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: axisAlignment, - children: [ - Container( - margin: const EdgeInsets.only(right: 10), - child: Icon( - icon, - size: textStyle?.fontSize ?? Theme.of(context).iconTheme.size, - color: color ?? Theme.of(context).iconTheme.color, - ), - ), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - text, - style: textStyle ?? - Theme.of(context).textTheme.titleLarge!.copyWith( - color: color ?? - Theme.of(context).textTheme.bodyLarge!.color), - ), + final TextStyle effectiveTextStyle = textStyle ?? + Theme.of(context).textTheme.bodyMedium!.copyWith( + color: color ?? Theme.of(context).textTheme.bodyMedium!.color); + + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + icon, + size: iconSize ?? effectiveTextStyle.fontSize, + color: color ?? Theme.of(context).iconTheme.color, + ), + SizedBox(width: spacing), + Flexible( + child: Text( + text, + style: effectiveTextStyle, + overflow: TextOverflow.ellipsis, ), - ], - ), + ), + ], ); } } diff --git a/lib/widgets/buttons/expandable_list_tile_button.dart b/lib/widgets/buttons/expandable_list_tile_button.dart index adb31c5..a350605 100644 --- a/lib/widgets/buttons/expandable_list_tile_button.dart +++ b/lib/widgets/buttons/expandable_list_tile_button.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:risto_widgets/risto_widgets.dart'; + +import 'list_tile_button.dart'; class ExpandableListTileButton extends StatefulWidget { final Widget expanded; - final Widget? buttonBody; - final double collapsedHeight; + final Widget? title; + final Widget? subtitle; final Color? backgroundColor; final Color? expandedColor; final Color? iconColor; @@ -13,14 +14,13 @@ class ExpandableListTileButton extends StatefulWidget { final double elevation; final Widget? leading; final IconData? icon; - final Widget? trailing; - final Widget Function(Function tapAction)? customHeader; + final Widget Function(Function tapAction, bool isExpanded)? customHeader; const ExpandableListTileButton({ super.key, required this.expanded, - this.buttonBody, - this.collapsedHeight = 60.0, + this.title, + this.subtitle, this.backgroundColor, this.expandedColor, this.iconColor, @@ -29,57 +29,61 @@ class ExpandableListTileButton extends StatefulWidget { this.elevation = 4.0, this.leading, this.icon, - this.trailing, this.customHeader, }); - // Named constructors for different types of headers factory ExpandableListTileButton.listTile({ required Widget expanded, - Widget? buttonBody, - double collapsedHeight = 60.0, + required Widget title, + Widget? subtitle, Color? backgroundColor, Color? expandedColor, - Color? iconColor, Color? trailingIconColor, Color? borderColor, double elevation = 4.0, Widget? leading, - Widget? trailing, }) { return ExpandableListTileButton( expanded: expanded, - buttonBody: buttonBody, - collapsedHeight: collapsedHeight, + title: title, + subtitle: subtitle, backgroundColor: backgroundColor, expandedColor: expandedColor, - iconColor: iconColor, trailingIconColor: trailingIconColor, borderColor: borderColor, elevation: elevation, leading: leading, - trailing: trailing, - customHeader: null, + customHeader: (toggleExpansion, isExpanded) => ListTileButton( + onPressed: () => toggleExpansion.call(), + leading: leading, + body: title, + subtitle: subtitle, + trailing: Icon( + isExpanded ? Icons.expand_less : Icons.expand_more, + color: trailingIconColor, + ), + backgroundColor: backgroundColor, + ), ); } factory ExpandableListTileButton.iconListTile({ required Widget expanded, - IconData? icon, - Color? iconColor, - double collapsedHeight = 60.0, + required IconData icon, + required Widget title, + Widget? subtitle, Color? backgroundColor, Color? expandedColor, + Color? iconColor, Color? trailingIconColor, Color? borderColor, double elevation = 4.0, - Widget? trailing, - Widget? buttonBody, + double sizeFactor = 1.0, }) { return ExpandableListTileButton( expanded: expanded, - buttonBody: buttonBody, - collapsedHeight: collapsedHeight, + title: title, + subtitle: subtitle, backgroundColor: backgroundColor, expandedColor: expandedColor, icon: icon, @@ -87,15 +91,25 @@ class ExpandableListTileButton extends StatefulWidget { trailingIconColor: trailingIconColor, borderColor: borderColor, elevation: elevation, - trailing: trailing, - customHeader: null, + customHeader: (toggleExpansion, isExpanded) => IconListTileButton( + icon: icon, + title: title, + subtitle: subtitle, + trailing: Icon( + isExpanded ? Icons.expand_less : Icons.expand_more, + color: trailingIconColor, + ), + onPressed: () => toggleExpansion.call(), + backgroundColor: backgroundColor, + iconColor: iconColor, + sizeFactor: sizeFactor, + ), ); } factory ExpandableListTileButton.custom({ required Widget expanded, - required Widget Function(Function tapAction) customHeader, - double collapsedHeight = 60.0, + required Widget Function(Function tapAction, bool isExpanded) customHeader, Color? backgroundColor, Color? expandedColor, Color? iconColor, @@ -105,7 +119,6 @@ class ExpandableListTileButton extends StatefulWidget { }) { return ExpandableListTileButton( expanded: expanded, - collapsedHeight: collapsedHeight, backgroundColor: backgroundColor, expandedColor: expandedColor, iconColor: iconColor, @@ -127,6 +140,8 @@ class _ExpandableListTileButtonState extends State late AnimationController _controller; late Animation _animation; late Widget _bodyWidget; + double _headerHeight = 0.0; + final GlobalKey _headerKey = GlobalKey(); @override void initState() { @@ -140,17 +155,44 @@ class _ExpandableListTileButtonState extends State parent: _controller, curve: Curves.easeInOut, ); + _controller.addStatusListener((status) { if (status == AnimationStatus.dismissed) { setState(() { _bodyWidget = const SizedBox(); }); - } else { + } else if (status == AnimationStatus.forward || + status == AnimationStatus.completed) { setState(() { _bodyWidget = widget.expanded; }); } }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateHeaderHeight(); + }); + } + + void _updateHeaderHeight() { + final RenderBox? renderBox = + _headerKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null) { + final height = renderBox.size.height; + if (_headerHeight != height) { + setState(() { + _headerHeight = height; + }); + } + } + } + + @override + void didUpdateWidget(covariant ExpandableListTileButton oldWidget) { + super.didUpdateWidget(oldWidget); + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateHeaderHeight(); + }); } @override @@ -172,13 +214,12 @@ class _ExpandableListTileButtonState extends State @override Widget build(BuildContext context) { - final theme = Theme.of(context); // Get the current theme + final theme = Theme.of(context); return Stack( children: [ - // Expanded body below header Padding( - padding: EdgeInsets.only(top: widget.collapsedHeight / 2), + padding: EdgeInsets.only(top: _headerHeight / 2), child: SizeTransition( sizeFactor: _animation, axisAlignment: 1.0, @@ -195,15 +236,17 @@ class _ExpandableListTileButtonState extends State color: widget.borderColor ?? Colors.transparent, ), ), - padding: const EdgeInsets.only(top: 30), + padding: EdgeInsets.only(top: _headerHeight / 2), child: _bodyWidget, ), ), ), - // Header - widget.customHeader != null - ? widget.customHeader!(_toggleExpansion) - : _buildDefaultHeader(context, theme), + Container( + key: _headerKey, + child: widget.customHeader != null + ? widget.customHeader!(_toggleExpansion, _isExpanded) + : _buildDefaultHeader(context, theme), + ), ], ); } @@ -213,28 +256,25 @@ class _ExpandableListTileButtonState extends State ? IconListTileButton( icon: widget.icon!, iconColor: widget.iconColor ?? theme.iconTheme.color, - title: widget.buttonBody!, - size: widget.collapsedHeight, + title: widget.title!, + subtitle: widget.subtitle, onPressed: _toggleExpansion, trailing: Icon( _isExpanded ? Icons.expand_less : Icons.expand_more, - color: widget.trailingIconColor ?? - theme.iconTheme.color, // Use custom or theme color + color: widget.trailingIconColor ?? theme.iconTheme.color, ), - backgroundColor: widget.backgroundColor ?? - theme.cardColor, // Use custom or theme color + backgroundColor: widget.backgroundColor ?? theme.cardColor, ) : ListTileButton( onPressed: _toggleExpansion, leading: widget.leading, - body: widget.buttonBody, + body: widget.title, + subtitle: widget.subtitle, trailing: Icon( _isExpanded ? Icons.expand_less : Icons.expand_more, - color: widget.trailingIconColor ?? - theme.iconTheme.color, // Custom or theme color + color: widget.trailingIconColor ?? theme.iconTheme.color, ), - backgroundColor: widget.backgroundColor ?? - theme.cardColor, // Custom or theme color + backgroundColor: widget.backgroundColor ?? theme.cardColor, ); } } diff --git a/lib/widgets/buttons/list_tile_button.dart b/lib/widgets/buttons/list_tile_button.dart index 37968be..98a8408 100644 --- a/lib/widgets/buttons/list_tile_button.dart +++ b/lib/widgets/buttons/list_tile_button.dart @@ -7,22 +7,20 @@ class ListTileButton extends StatelessWidget { final Widget? subtitle; final Widget? leading; final Widget? trailing; - final double? height; - final double? minTileHeight; - final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? contentPadding; final Color? borderColor; - final double trailingSize; final double borderRadius; final VisualDensity? visualDensity; final Color? backgroundColor; - final double? sizeFactor; - final ListTileTitleAlignment? bodyAlignment; + final double? elevation; + final double? leadingSizeFactor; + final double? minHeight; const ListTileButton({ super.key, - this.padding = EdgeInsets.zero, + this.padding, this.contentPadding, this.borderColor, this.backgroundColor, @@ -30,65 +28,72 @@ class ListTileButton extends StatelessWidget { required this.body, this.subtitle, this.trailing, - this.height, - this.minTileHeight, this.onPressed, - this.trailingSize = 30, this.borderRadius = 10, this.visualDensity, this.onLongPress, this.bodyAlignment, - this.sizeFactor, + this.elevation, + this.leadingSizeFactor, + this.minHeight, }); @override Widget build(BuildContext context) { return RoundedContainer( - borderColor: borderColor ?? Colors.transparent, - backgroundColor: backgroundColor ?? Theme.of(context).cardColor, - height: height, + borderColor: borderColor, + backgroundColor: backgroundColor, + elevation: elevation, borderRadius: borderRadius, - padding: padding, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - foregroundColor: Theme.of(context).splashColor, - splashFactory: Theme.of(context).splashFactory, - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - padding: EdgeInsets.zero, - disabledBackgroundColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(borderRadius), + child: Material( + type: MaterialType.transparency, + child: InkWell( + borderRadius: BorderRadius.circular(borderRadius), + onTap: onPressed, + onLongPress: onLongPress, + child: Padding( + padding: padding ?? const EdgeInsets.all(8), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: minHeight ?? 50.0, + ), + child: IntrinsicHeight( + child: Row( + children: [ + if (leading != null) + SizedBox( + width: (leadingSizeFactor != null && + leadingSizeFactor! * 24 > 48) + ? leadingSizeFactor! * 24 + : 48, + height: double.infinity, + child: leading, + ), + Expanded( + child: ListTile( + titleAlignment: bodyAlignment, + visualDensity: visualDensity ?? VisualDensity.compact, + contentPadding: + contentPadding ?? const EdgeInsets.only(left: 8), + minVerticalPadding: 0, + minLeadingWidth: 0, + title: body, + subtitle: subtitle, + ), + ), + if (trailing != null) + Container( + margin: const EdgeInsets.only(right: 12), + alignment: Alignment.center, + height: double.infinity, + child: trailing, + ), + ], + ), + ), + ), ), ), - onPressed: onPressed, - onLongPress: onLongPress, - child: ListTile( - titleAlignment: bodyAlignment, - visualDensity: visualDensity, - minVerticalPadding: height != null ? 0 : null, - contentPadding: - contentPadding ?? const EdgeInsets.symmetric(horizontal: 16.0), - // Replace with dynamic padding if needed - minLeadingWidth: 0, - leading: leading != null - ? SizedBox.square( - dimension: (height ?? 30) * (sizeFactor ?? 0.5), - child: leading, - ) - : null, - title: body, - subtitle: subtitle, - minTileHeight: height, - trailing: trailing != null - ? SizedBox.square( - dimension: trailingSize, - child: Center( - child: trailing, - ), - ) - : null, - ), ), ); } @@ -102,9 +107,9 @@ class IconListTileButton extends StatelessWidget { final Color? iconColor; final Color? backgroundColor; final Color? borderColor; - final double size; - final double? sizeFactor; - final EdgeInsetsGeometry padding; + final double? elevation; + final double sizeFactor; + final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? contentPadding; final void Function()? onPressed; @@ -114,42 +119,35 @@ class IconListTileButton extends StatelessWidget { required this.title, this.subtitle, this.trailing, - required this.size, this.iconColor, this.backgroundColor, this.borderColor, - this.sizeFactor, - this.padding = EdgeInsets.zero, + this.sizeFactor = 1.0, + this.padding, this.contentPadding, this.onPressed, + this.elevation, }); @override Widget build(BuildContext context) { return ListTileButton( borderColor: borderColor, - backgroundColor: backgroundColor ?? Theme.of(context).cardColor, + backgroundColor: backgroundColor, body: title, - leading: AspectRatio( - aspectRatio: 1, - child: FittedBox( - fit: BoxFit.cover, - alignment: Alignment.center, - child: Icon( - icon, - color: iconColor ?? Theme.of(context).iconTheme.color, - ), - ), + leading: Icon( + icon, + color: iconColor ?? Theme.of(context).iconTheme.color, + size: 24.0 * sizeFactor, ), - height: size, + leadingSizeFactor: sizeFactor, padding: padding, contentPadding: contentPadding, - sizeFactor: sizeFactor, - visualDensity: const VisualDensity(horizontal: 2, vertical: 2), bodyAlignment: ListTileTitleAlignment.threeLine, subtitle: subtitle, trailing: trailing, onPressed: onPressed, + elevation: elevation, ); } } @@ -157,41 +155,49 @@ class IconListTileButton extends StatelessWidget { class RoundedContainer extends StatelessWidget { final Color? borderColor; final Color? backgroundColor; - final double? height; final double borderRadius; - final EdgeInsetsGeometry padding; final Widget child; + final double? elevation; + final EdgeInsetsGeometry? padding; + final EdgeInsetsGeometry? margin; const RoundedContainer({ super.key, this.borderColor, this.backgroundColor, - this.height, - this.padding = EdgeInsets.zero, this.borderRadius = 10, required this.child, + this.elevation, + this.padding, + this.margin, }); @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(borderRadius)), - border: Border.fromBorderSide( - BorderSide(color: borderColor ?? Colors.transparent, width: 2)), + return Padding( + padding: margin ?? EdgeInsets.zero, + child: Material( + elevation: elevation ?? 0, + borderRadius: BorderRadius.circular(borderRadius), color: backgroundColor ?? Theme.of(context).cardColor, + child: Container( + padding: padding, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius), + border: borderColor != null + ? Border.all(color: borderColor!, width: 2) + : null, + ), + child: child, + ), ), - height: height, - margin: padding, - padding: EdgeInsets.zero, - child: child, ); } } class DoubleListTileButtons extends StatelessWidget { - final ListTileButton firstButton; - final ListTileButton secondButton; + final Widget firstButton; + final Widget secondButton; final EdgeInsetsGeometry padding; final bool expanded; final double? space; @@ -217,7 +223,7 @@ class DoubleListTileButtons extends StatelessWidget { ? [ Expanded(child: firstButton), SizedBox(width: space), - Expanded(child: secondButton) + Expanded(child: secondButton), ] : [ firstButton, diff --git a/lib/widgets/input/increment_decrement_widget.dart b/lib/widgets/input/increment_decrement_widget.dart index 065e13b..67830cc 100644 --- a/lib/widgets/input/increment_decrement_widget.dart +++ b/lib/widgets/input/increment_decrement_widget.dart @@ -1,168 +1,474 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import '../buttons/custom_action_button.dart'; -class IncrementDecrementWidget extends StatelessWidget { +typedef ValueUpdate = dynamic Function(int updateValue); + +class IncrementDecrementWidget extends StatefulWidget { + // Quantity properties final int quantity; - final int maxQuantity; - final int minValue; - final void Function() onIncrement; - final void Function() onDecrement; + final int? maxQuantity; + final int? minValue; + + // Callback functions + final ValueUpdate? onChanged; + + // Customization properties final Color? backgroundColor; final Color? iconColor; final double? elevation; - final EdgeInsetsGeometry margin; - final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry? margin; + final EdgeInsetsGeometry? valuePadding; final TextStyle? quantityTextStyle; + final double? borderRadius; + final double? width; + final double? height; + final EdgeInsetsGeometry? buttonPadding; + final EdgeInsetsGeometry? buttonMargin; + final double? buttonWidth; + final double? buttonHeight; + + // CustomActionButton parameters + final Color? splashColor; + final Color? borderColor; + final InteractiveInkFeatureFactory? splashFactory; + + // Factory-specific properties + final Widget? incrementIcon; + final Widget? decrementIcon; + + // Button shape + final OutlinedBorder? buttonShape; + + // Long-press settings + final Duration longPressInterval; + + // Alignment + final MainAxisAlignment? alignment; const IncrementDecrementWidget({ super.key, required this.quantity, - required this.maxQuantity, - required this.minValue, - required this.onIncrement, - required this.onDecrement, + this.maxQuantity, + this.minValue, + this.onChanged, this.backgroundColor, this.iconColor, - this.elevation = 0.0, - this.margin = const EdgeInsets.all(5), - this.padding = const EdgeInsets.symmetric(horizontal: 5), + this.elevation, + this.margin, + this.valuePadding, this.quantityTextStyle, + this.borderRadius, + this.width, + this.height, + this.buttonPadding, + this.buttonMargin, + this.buttonWidth, + this.buttonHeight, + this.splashColor, + this.borderColor, + this.splashFactory, + this.incrementIcon, + this.decrementIcon, + this.buttonShape, + this.longPressInterval = const Duration(milliseconds: 100), + this.alignment, }); - // Custom constructor for a flat design + /// Factory constructor for a flat design. factory IncrementDecrementWidget.flat({ required int quantity, - required int maxQuantity, - required int minValue, - required void Function() onIncrement, - required void Function() onDecrement, + int? maxQuantity, + int? minValue, + ValueUpdate? onChanged, Color? backgroundColor, Color? iconColor, + EdgeInsetsGeometry? margin, + EdgeInsetsGeometry? valuePadding, + EdgeInsetsGeometry? buttonMargin, + EdgeInsetsGeometry? buttonPadding, + TextStyle? quantityTextStyle, + double? borderRadius, + double? width, + double? height, + double? buttonWidth, + double? buttonHeight, + Color? splashColor, + Color? borderColor, + InteractiveInkFeatureFactory? splashFactory, + Widget? incrementIcon, + Widget? decrementIcon, + Duration longPressInterval = const Duration(milliseconds: 100), + MainAxisAlignment? alignment, }) { return IncrementDecrementWidget( quantity: quantity, maxQuantity: maxQuantity, minValue: minValue, - onIncrement: onIncrement, - onDecrement: onDecrement, - backgroundColor: backgroundColor, + onChanged: onChanged, + backgroundColor: backgroundColor ?? Colors.transparent, iconColor: iconColor, elevation: 0.0, - // Flat design, no elevation - margin: const EdgeInsets.all(8.0), + margin: margin, + valuePadding: valuePadding, + buttonMargin: buttonMargin, + buttonPadding: buttonPadding, + quantityTextStyle: quantityTextStyle, + borderRadius: borderRadius ?? 10.0, + width: width, + height: height, + buttonWidth: buttonWidth, + buttonHeight: buttonHeight, + splashColor: splashColor, + borderColor: borderColor, + splashFactory: splashFactory, + incrementIcon: incrementIcon, + decrementIcon: decrementIcon, + longPressInterval: longPressInterval, + alignment: alignment, ); } - // Custom constructor for a raised design with elevation + /// Factory constructor for a raised design. factory IncrementDecrementWidget.raised({ required int quantity, - required int maxQuantity, - required int minValue, - required void Function() onIncrement, - required void Function() onDecrement, + int? maxQuantity, + int? minValue, + ValueUpdate? onChanged, Color? backgroundColor, Color? iconColor, + double? elevation, + EdgeInsetsGeometry? margin, + EdgeInsetsGeometry? valuePadding, + EdgeInsetsGeometry? buttonMargin, + EdgeInsetsGeometry? buttonPadding, + TextStyle? quantityTextStyle, + double? borderRadius, + double? width, + double? height, + double? buttonWidth, + double? buttonHeight, + Color? borderColor, + Widget? incrementIcon, + Widget? decrementIcon, + Duration longPressInterval = const Duration(milliseconds: 100), + MainAxisAlignment? alignment, }) { return IncrementDecrementWidget( quantity: quantity, maxQuantity: maxQuantity, minValue: minValue, - onIncrement: onIncrement, - onDecrement: onDecrement, + onChanged: onChanged, backgroundColor: backgroundColor, iconColor: iconColor, - elevation: 6.0, - // Raised with elevation - margin: const EdgeInsets.all(8.0), + elevation: elevation ?? 6.0, + margin: margin, + valuePadding: valuePadding, + buttonMargin: buttonMargin, + buttonPadding: buttonPadding, + quantityTextStyle: quantityTextStyle, + borderRadius: borderRadius ?? 10.0, + width: width, + height: height, + buttonWidth: buttonWidth, + buttonHeight: buttonHeight, + borderColor: borderColor, + splashFactory: NoSplash.splashFactory, + splashColor: Colors.transparent, + incrementIcon: incrementIcon, + decrementIcon: decrementIcon, + longPressInterval: longPressInterval, + alignment: alignment, ); } - // Custom constructor for a minimalistic design + /// Factory constructor for a minimalistic design. factory IncrementDecrementWidget.minimal({ required int quantity, - required int maxQuantity, - required int minValue, - required void Function() onIncrement, - required void Function() onDecrement, + int? maxQuantity, + int? minValue, + ValueUpdate? onChanged, Color? iconColor, + EdgeInsetsGeometry? margin, + EdgeInsetsGeometry? valuePadding, + EdgeInsetsGeometry? buttonMargin, + EdgeInsetsGeometry? buttonPadding, + TextStyle? quantityTextStyle, + double? width, + double? height, + double? buttonWidth, + double? buttonHeight, + Widget? incrementIcon, + Widget? decrementIcon, + Duration longPressInterval = const Duration(milliseconds: 100), + MainAxisAlignment? alignment, }) { return IncrementDecrementWidget( quantity: quantity, maxQuantity: maxQuantity, minValue: minValue, - onIncrement: onIncrement, - onDecrement: onDecrement, + onChanged: onChanged, backgroundColor: Colors.transparent, - // Minimalist design iconColor: iconColor, elevation: 0.0, - // No elevation by default - margin: EdgeInsets.zero, // No margin for minimal design + margin: margin, + valuePadding: valuePadding, + buttonMargin: buttonMargin, + buttonPadding: buttonPadding, + quantityTextStyle: quantityTextStyle, + width: width, + height: height, + buttonWidth: buttonWidth, + buttonHeight: buttonHeight, + splashFactory: NoSplash.splashFactory, + splashColor: Colors.transparent, + incrementIcon: incrementIcon, + decrementIcon: decrementIcon, + longPressInterval: longPressInterval, + alignment: alignment, + ); + } + + /// Factory constructor for squared buttons with equal width and height and customizable border radius. + factory IncrementDecrementWidget.squared({ + required int quantity, + int? maxQuantity, + int? minValue, + ValueUpdate? onChanged, + Color? backgroundColor, + Color? iconColor, + double? elevation, + EdgeInsetsGeometry? margin, + EdgeInsetsGeometry? valuePadding, + EdgeInsetsGeometry? buttonMargin, + EdgeInsetsGeometry? buttonPadding, + TextStyle? quantityTextStyle, + double? width, + double? height, + double? buttonSize, + double? borderRadius, // Allow specifying border radius + Color? borderColor, + Widget? incrementIcon, + Widget? decrementIcon, + Duration longPressInterval = const Duration(milliseconds: 100), + MainAxisAlignment? alignment, + }) { + final double size = buttonSize ?? 50.0; + final double effectiveBorderRadius = borderRadius ?? 10.0; + return IncrementDecrementWidget( + quantity: quantity, + maxQuantity: maxQuantity, + minValue: minValue, + onChanged: onChanged, + backgroundColor: backgroundColor, + iconColor: iconColor, + elevation: elevation ?? 0.0, + margin: margin, + valuePadding: valuePadding, + buttonMargin: buttonMargin, + buttonPadding: buttonPadding, + quantityTextStyle: quantityTextStyle, + width: width, + height: height, + buttonWidth: size, + buttonHeight: size, + borderColor: borderColor, + borderRadius: effectiveBorderRadius, + // Set the border radius + splashColor: Colors.grey.withOpacity(0.2), + incrementIcon: incrementIcon, + decrementIcon: decrementIcon, + buttonShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(effectiveBorderRadius), + ), + longPressInterval: longPressInterval, + alignment: alignment ?? MainAxisAlignment.center, ); } + @override + State createState() => + _IncrementDecrementWidgetState(); +} + +class _IncrementDecrementWidgetState extends State { + late int _currentQuantity; + + @override + void initState() { + super.initState(); + _currentQuantity = widget.quantity; + } + + Future _increment() async { + if (widget.maxQuantity == null || _currentQuantity < widget.maxQuantity!) { + int updatedQuantity = _currentQuantity + 1; + int newQuantity = updatedQuantity; + + if (widget.onChanged != null) { + var result = widget.onChanged!(updatedQuantity); + + if (result is Future) { + newQuantity = await result ?? updatedQuantity; + } else if (result is int?) { + newQuantity = result ?? updatedQuantity; + } + // If result is void (null), use updatedQuantity + } + + setState(() { + _currentQuantity = newQuantity; + }); + } + } + + Future _decrement() async { + if (widget.minValue == null || _currentQuantity > widget.minValue!) { + int updatedQuantity = _currentQuantity - 1; + int newQuantity = updatedQuantity; + + if (widget.onChanged != null) { + var result = widget.onChanged!(updatedQuantity); + + if (result is Future) { + newQuantity = await result ?? updatedQuantity; + } else if (result is int?) { + newQuantity = result ?? updatedQuantity; + } + // If result is void (null), use updatedQuantity + } + + setState(() { + _currentQuantity = newQuantity; + }); + } + } + @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildActionButton( - context, - Icons.remove, - onPressed: quantity > minValue ? onDecrement : null, - isEnabled: quantity > minValue, - ), - _buildQuantityDisplay(context), - _buildActionButton( - context, - Icons.add, - onPressed: quantity < maxQuantity ? onIncrement : null, - isEnabled: quantity < maxQuantity, - ), - ], + final Color effectiveBackgroundColor = + widget.backgroundColor ?? Theme.of(context).cardColor; + final EdgeInsetsGeometry valuePadding = + widget.valuePadding ?? const EdgeInsets.symmetric(horizontal: 10); + final EdgeInsetsGeometry buttonMargin = + widget.buttonMargin ?? EdgeInsets.zero; + final double? effectiveWidth = widget.width; + final MainAxisAlignment effectiveAlignment = + widget.alignment ?? MainAxisAlignment.spaceBetween; + + return SizedBox( + width: effectiveWidth, + child: Row( + mainAxisAlignment: effectiveAlignment, + children: [ + _buildActionButton( + context, + widget.decrementIcon ?? const Icon(Icons.remove), + onPressed: + (widget.minValue == null || _currentQuantity > widget.minValue!) + ? _decrement + : null, + isEnabled: + widget.minValue == null || _currentQuantity > widget.minValue!, + onLongPress: _decrement, + effectiveBackgroundColor: effectiveBackgroundColor, + effectiveMargin: buttonMargin, + effectiveWidth: widget.buttonWidth, + effectiveHeight: widget.buttonHeight, + ), + _buildQuantityDisplay(context, valuePadding), + _buildActionButton( + context, + widget.incrementIcon ?? const Icon(Icons.add), + onPressed: (widget.maxQuantity == null || + _currentQuantity < widget.maxQuantity!) + ? _increment + : null, + isEnabled: widget.maxQuantity == null || + _currentQuantity < widget.maxQuantity!, + onLongPress: _increment, + effectiveBackgroundColor: effectiveBackgroundColor, + effectiveMargin: buttonMargin, + effectiveWidth: widget.buttonWidth, + effectiveHeight: widget.buttonHeight, + ), + ], + ), ); } - /// Builds an increment or decrement button using CustomActionButton. - Widget _buildActionButton(BuildContext context, IconData icon, - {required VoidCallback? onPressed, required bool isEnabled}) { - return Expanded( - child: CustomActionButton( - margin: margin, - backgroundColor: backgroundColor ?? Theme.of(context).cardColor, - onPressed: onPressed, - child: FittedBox( - alignment: Alignment.center, - fit: BoxFit.scaleDown, - child: Icon( - icon, - color: _iconColor(context, isEnabled), + Widget _buildActionButton( + BuildContext context, + Widget icon, { + required VoidCallback? onPressed, + required bool isEnabled, + required VoidCallback onLongPress, + required Color effectiveBackgroundColor, + required EdgeInsetsGeometry effectiveMargin, + double? effectiveWidth, + double? effectiveHeight, + }) { + final Color effectiveIconColor = + _iconColor(context, isEnabled) ?? Theme.of(context).iconTheme.color!; + + // **KEY MODIFICATION:** + // When the button is disabled, set onLongPress to null to prevent the timer from running. + final VoidCallback? effectiveOnLongPress = isEnabled ? onLongPress : null; + + Widget button = CustomActionButton.longPress( + margin: effectiveMargin, + width: effectiveWidth, + height: effectiveHeight, + backgroundColor: effectiveBackgroundColor, + shape: widget.buttonShape ?? + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(widget.borderRadius ?? 10.0), ), - ), + elevation: widget.elevation ?? 0, + padding: widget.buttonPadding, + onPressed: onPressed, + onLongPress: effectiveOnLongPress, + // Pass null if disabled + child: IconTheme( + data: IconThemeData(color: effectiveIconColor), + child: icon, ), ); + + if (effectiveWidth == null || effectiveHeight == null) { + return Expanded(child: button); + } else { + return SizedBox( + width: effectiveWidth, + height: effectiveHeight, + child: button, + ); + } } - /// Displays the current quantity between the increment and decrement buttons. - Widget _buildQuantityDisplay(BuildContext context) { + Widget _buildQuantityDisplay( + BuildContext context, + EdgeInsetsGeometry padding, + ) { return Expanded( flex: 0, child: Container( alignment: Alignment.center, padding: padding, child: Text( - quantity.toString(), - style: - quantityTextStyle ?? Theme.of(context).textTheme.headlineMedium, + _currentQuantity.toString(), + style: widget.quantityTextStyle ?? + Theme.of(context).textTheme.titleLarge, ), ), ); } - Color _iconColor(BuildContext context, bool isEnabled) { - final Color defaultColor = iconColor ?? Theme.of(context).iconTheme.color!; + Color? _iconColor(BuildContext context, bool isEnabled) { + final Color defaultColor = + widget.iconColor ?? Theme.of(context).iconTheme.color!; return isEnabled ? defaultColor : defaultColor.withOpacity(0.2); } } diff --git a/lib/widgets/sheets/open_custom_sheet.dart b/lib/widgets/sheets/open_custom_sheet.dart index 482003a..4df2093 100644 --- a/lib/widgets/sheets/open_custom_sheet.dart +++ b/lib/widgets/sheets/open_custom_sheet.dart @@ -1,21 +1,26 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import '../buttons/custom_action_button.dart'; import '../buttons/list_tile_button.dart'; class OpenCustomSheet extends StatelessWidget { final bool barrierDismissible; final String? barrierLabel; final Color? barrierColor; - final Function? onClose; + final Function(dynamic)? onClose; final Widget Function({ScrollController? scrollController}) body; - // builder - final bool? scrollable; - final bool? expand; - final double? initialChildSize; - final double? minChildSize; - final double? maxChildSize; + final bool scrollable; + final bool expand; + final double initialChildSize; + final double minChildSize; + final double maxChildSize; + + final Color? backgroundColor; + final Color? handleColor; + final ShapeBorder? sheetShape; + final EdgeInsetsGeometry? sheetPadding; const OpenCustomSheet({ super.key, @@ -24,139 +29,267 @@ class OpenCustomSheet extends StatelessWidget { this.barrierLabel, this.onClose, required this.body, - this.expand, - this.initialChildSize, - this.minChildSize, - this.maxChildSize, - this.scrollable, + this.expand = true, + this.initialChildSize = 0.5, + this.minChildSize = 0.25, + this.maxChildSize = 1.0, + this.scrollable = false, + this.backgroundColor, + this.handleColor, + this.sheetShape, + this.sheetPadding, }); - void show(BuildContext context) { - showModalBottomSheet( - isDismissible: barrierDismissible, - backgroundColor: Colors.transparent, + factory OpenCustomSheet.scrollableSheet( + BuildContext context, { + required Widget Function({ScrollController? scrollController}) body, + Function(dynamic)? onClose, + bool expand = true, + double initialChildSize = 0.5, + double minChildSize = 0.25, + double maxChildSize = 1.0, + Color? barrierColor, + Color? backgroundColor, + Color? handleColor, + bool barrierDismissible = true, + ShapeBorder? sheetShape, + EdgeInsetsGeometry? sheetPadding, + }) { + return OpenCustomSheet( + scrollable: true, + expand: expand, + initialChildSize: initialChildSize, + minChildSize: minChildSize, + maxChildSize: maxChildSize, + onClose: onClose, barrierColor: barrierColor, - barrierLabel: barrierLabel, - isScrollControlled: scrollable ?? false, - useSafeArea: true, - context: context, - builder: (context) { - if (scrollable ?? false) { - return DraggableScrollableSheet( - expand: expand ?? true, - initialChildSize: initialChildSize ?? 0.5, - minChildSize: minChildSize ?? 0.25, - maxChildSize: maxChildSize ?? 1.0, - builder: (context, scrollController) => - body(scrollController: scrollController), - ); - } - - return body(); - }, - ).then( - (Object? value) { - try { - onClose?.call(value) ?? () {}; - } catch (e) { - debugPrint("sheet error: $e"); - } - }, + barrierDismissible: barrierDismissible, + backgroundColor: backgroundColor, + sheetShape: sheetShape, + sheetPadding: sheetPadding, + body: ({scrollController}) => body(scrollController: scrollController), ); } - @override - Widget build(BuildContext context) { - return const SizedBox.shrink(); + static Widget _buildScrollableSheetContent( + BuildContext context, { + required Widget body, + Color? backgroundColor, + Color? handleColor, + ShapeBorder? sheetShape, + EdgeInsetsGeometry? sheetPadding, + }) { + return MediaQuery.removePadding( + context: context, + removeBottom: true, + child: Container( + decoration: ShapeDecoration( + shape: sheetShape ?? + const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + color: backgroundColor ?? Theme.of(context).cardColor, + ), + padding: sheetPadding ?? const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (handleColor != Colors.transparent) _buildHandle(handleColor), + Expanded(child: body), + ], + ), + ), + ); } - static void openConfirmSheet( + factory OpenCustomSheet.openConfirmSheet( BuildContext context, { required Widget body, - void Function(dynamic value)? onClose, - bool? expand, - double? initialChildSize, - double? minChildSize, - double? maxChildSize, - bool? scrollable, + Function(dynamic)? onClose, + bool expand = true, + double initialChildSize = 0.5, + double minChildSize = 0.25, + double maxChildSize = 1.0, + bool scrollable = false, Color? backgroundColor, Color? handleColor, - Color? buttonColor, - Color? buttonTextColor, + bool barrierDismissible = true, + Color? firstButtonColor, + Color? secondButtonColor, + Color? firstButtonTextColor, + Color? secondButtonTextColor, EdgeInsetsGeometry? padding, double? buttonSpacing, }) { - OpenCustomSheet( + return OpenCustomSheet( scrollable: scrollable, expand: expand, initialChildSize: initialChildSize, minChildSize: minChildSize, maxChildSize: maxChildSize, onClose: onClose, - body: ({scrollController}) => Container( + backgroundColor: backgroundColor, + barrierDismissible: barrierDismissible, + body: ({scrollController}) => _buildSheetContent( + context, + body: body, + backgroundColor: backgroundColor, + handleColor: handleColor, + firstButtonColor: firstButtonColor, + secondButtonColor: secondButtonColor, + firstButtonTextColor: firstButtonTextColor, + secondButtonTextColor: secondButtonTextColor, + padding: padding, + buttonSpacing: buttonSpacing, + ), + ); + } + + static Widget _buildSheetContent( + BuildContext context, { + required Widget body, + Color? backgroundColor, + Color? handleColor, + Color? firstButtonColor, + Color? secondButtonColor, + Color? firstButtonTextColor, + Color? secondButtonTextColor, + EdgeInsetsGeometry? padding, + double? buttonSpacing, + }) { + return MediaQuery.removePadding( + context: context, + removeBottom: true, + child: Container( decoration: BoxDecoration( - borderRadius: - const BorderRadiusDirectional.vertical(top: Radius.circular(10)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(10)), color: backgroundColor ?? Theme.of(context).cardColor, ), padding: padding ?? const EdgeInsets.only(bottom: 16.0), child: Column( mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Center( - child: Container( - width: 150, - height: 5, - decoration: BoxDecoration( - color: handleColor ?? CupertinoColors.inactiveGray, - borderRadius: BorderRadius.circular(20)), - margin: const EdgeInsets.all(10), - ), - ), + _buildHandle(handleColor), Padding( padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), - child: Center(child: body), + child: body, ), - DoubleListTileButtons( - padding: padding ?? - const EdgeInsets.symmetric(vertical: 10, horizontal: 16.0), - space: buttonSpacing ?? 16.0, - firstButton: ListTileButton( - onPressed: () => Navigator.pop(context, false), - backgroundColor: buttonColor ?? Theme.of(context).cardColor, - body: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - 'Close', // Replaced `tr('chiudi')` with a static string or you can handle localization separately. - style: Theme.of(context) - .textTheme - .labelLarge! - .copyWith(color: buttonTextColor ?? Colors.white), - textAlign: TextAlign.center, - ), - ), - ), - secondButton: ListTileButton( - onPressed: () => Navigator.pop(context, true), - backgroundColor: buttonColor ?? Theme.of(context).primaryColor, - body: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - 'Confirm', // Replaced `tr('conferma')` with a static string or you can handle localization separately. - style: Theme.of(context) - .textTheme - .labelLarge! - .copyWith(color: buttonTextColor ?? Colors.white), - textAlign: TextAlign.center, - ), - ), - ), + _buildButtons( + context, + firstButtonColor, + secondButtonColor, + firstButtonTextColor, + secondButtonTextColor, + buttonSpacing, ), ], ), ), - ).show(context); + ); + } + + static Widget _buildButtons( + BuildContext context, + Color? firstButtonColor, + Color? secondButtonColor, + Color? firstButtonTextColor, + Color? secondButtonTextColor, + double? buttonSpacing, + ) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: DoubleListTileButtons( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + space: buttonSpacing ?? 8, + firstButton: CustomActionButton.flat( + margin: EdgeInsets.zero, + width: double.infinity, + onPressed: () => Navigator.pop(context, false), + backgroundColor: firstButtonColor ?? Colors.red, + child: Text( + 'Close', + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: firstButtonTextColor ?? Colors.white, + ), + ), + ), + secondButton: CustomActionButton.flat( + onPressed: () => Navigator.pop(context, true), + margin: EdgeInsets.zero, + width: double.infinity, + backgroundColor: secondButtonColor ?? Colors.green, + child: Text( + 'Confirm', + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: secondButtonTextColor ?? Colors.white, + ), + ), + ), + ), + ); + } + + static Widget _buildHandle(Color? handleColor) { + return Center( + child: Container( + width: 150, + height: 5, + decoration: BoxDecoration( + color: handleColor ?? CupertinoColors.inactiveGray, + borderRadius: BorderRadius.circular(20), + ), + margin: const EdgeInsets.all(10), + ), + ); + } + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } + + void show(BuildContext context) { + showModalBottomSheet( + backgroundColor: Colors.transparent, + isDismissible: barrierDismissible, + barrierColor: barrierColor, + barrierLabel: barrierLabel, + isScrollControlled: scrollable, + useSafeArea: true, + context: context, + builder: (context) { + if (scrollable) { + return DraggableScrollableSheet( + expand: expand, + initialChildSize: initialChildSize, + minChildSize: minChildSize, + maxChildSize: maxChildSize, + builder: (context, scrollController) { + return GestureDetector( + onVerticalDragUpdate: (details) {}, + child: _buildScrollableSheetContent( + context, + body: body(scrollController: scrollController), + backgroundColor: backgroundColor, + handleColor: handleColor, + sheetShape: sheetShape, + sheetPadding: sheetPadding, + ), + ); + }, + ); + } else { + return MediaQuery.removePadding( + context: context, + removeBottom: true, + child: SafeArea(child: body()), + ); + } + }, + ).then((value) { + if (onClose != null) { + onClose!(value); + } + }); } } diff --git a/pubspec.lock b/pubspec.lock index 8a88e3f..cd84b75 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -404,10 +404,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.4+3" stack_trace: dependency: transitive description: @@ -436,10 +436,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+2" term_glyph: dependency: transitive description: @@ -468,10 +468,10 @@ packages: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.0" vector_math: dependency: transitive description: @@ -492,10 +492,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" xdg_directories: dependency: transitive description: @@ -505,5 +505,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.4.1 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.22.0" diff --git a/test/action_wrapper_test.dart b/test/action_wrapper_test.dart deleted file mode 100644 index 20a69bb..0000000 --- a/test/action_wrapper_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:risto_widgets/risto_widgets.dart'; - -main() { - testWidgets('ActionButtonWrapper renders with correct dimensions and margin', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ActionButtonWrapper( - margin: const EdgeInsets.all(10.0), - height: 50.0, - width: 100.0, - child: const Text('Button'), - ), - ), - ), - ); - - // Verify the ActionButtonWrapper renders with correct size and margin. - final container = tester.widget(find.byType(Container)); - expect(container.constraints?.maxHeight, equals(50.0)); - expect(container.constraints?.maxWidth, equals(100.0)); - expect(container.margin, equals(const EdgeInsets.all(10.0))); - }); -} diff --git a/test/custom_action_button_test.dart b/test/custom_action_button_test.dart index fb1b0ff..6d75faf 100644 --- a/test/custom_action_button_test.dart +++ b/test/custom_action_button_test.dart @@ -3,8 +3,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:risto_widgets/risto_widgets.dart'; void main() { - group('ActionButtonWrapper Tests', () {}); - group('CustomActionButton Tests', () { testWidgets('CustomActionButton increments counter when pressed', (WidgetTester tester) async { @@ -33,7 +31,7 @@ void main() { expect(counter, 1); }); - testWidgets('CustomActionButton.flat renders correctly with 0 elevation', + testWidgets('CustomActionButton.flat renders correctly', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -48,28 +46,31 @@ void main() { ); expect(find.text('Flat Button'), findsOneWidget); - final elevatedButton = - tester.widget(find.byType(ElevatedButton)); - expect(elevatedButton.style?.elevation?.resolve({}), equals(0.0)); + + // Ensure the button is a TextButton + expect(find.byType(TextButton), findsOneWidget); + + // Ensure there is no ElevatedButton + expect(find.byType(ElevatedButton), findsNothing); }); - testWidgets('CustomActionButton.raised renders with correct elevation', + testWidgets('CustomActionButton.elevated renders with correct elevation', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: CustomActionButton.raised( + body: CustomActionButton.elevated( onPressed: () {}, backgroundColor: Colors.green, elevation: 5.0, - child: const Text('Raised Button'), + child: const Text('Elevated Button'), ), ), ), ); // Expect the button to be present - expect(find.text('Raised Button'), findsOneWidget); + expect(find.text('Elevated Button'), findsOneWidget); // Find the ElevatedButton within the CustomActionButton final elevatedButtonFinder = find.descendant( @@ -86,94 +87,108 @@ void main() { expect(elevatedButton.style?.elevation?.resolve({}), equals(5.0)); }); - testWidgets('CustomActionButton.raised renders with correct elevation', + testWidgets('CustomActionButton.minimal renders correctly', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: CustomActionButton.raised( + body: CustomActionButton.minimal( onPressed: () {}, - backgroundColor: Colors.green, - elevation: 5.0, - child: const Text('Raised Button'), + child: const Text('Minimal Button'), ), ), ), ); - // Ensure the button is present - expect(find.text('Raised Button'), findsOneWidget); + expect(find.text('Minimal Button'), findsOneWidget); - // Find the ElevatedButton widget using find.byType - final elevatedButton = - tester.widget(find.byType(ElevatedButton)); + // Ensure the button is a TextButton + expect(find.byType(TextButton), findsOneWidget); - // Verify the elevation is correct - expect(elevatedButton.style?.elevation?.resolve({}), equals(5.0)); + // Ensure that the button has no overlay or splash + final textButton = tester.widget(find.byType(TextButton)); + + // Check that overlayColor is transparent + final overlayColor = textButton.style?.overlayColor?.resolve({}); + + expect(overlayColor, Colors.transparent); + + // Check that splashFactory is NoSplash + expect(textButton.style?.splashFactory, NoSplash.splashFactory); }); - testWidgets('CustomActionButton shows disabled state correctly', + testWidgets('CustomActionButton.longPress triggers onLongPress', (WidgetTester tester) async { - // Build the widget with a disabled button + int longPressCounter = 0; + await tester.pumpWidget( - const MaterialApp( + MaterialApp( home: Scaffold( - body: CustomActionButton( - onPressed: null, // Disabled state - child: Text('Disabled Button'), + body: CustomActionButton.longPress( + onPressed: () {}, + onLongPress: () { + longPressCounter++; + }, + child: const Text('Long Press Button'), ), ), ), ); - // Verify the disabled button is present with correct text - expect(find.text('Disabled Button'), findsOneWidget); + expect(find.text('Long Press Button'), findsOneWidget); - // Verify that a CustomActionDisable is rendered instead of ElevatedButton - final customActionDisableFinder = find.byType(CustomActionDisable); - expect(customActionDisableFinder, findsOneWidget); + // Simulate a long press + final gesture = await tester + .startGesture(tester.getCenter(find.byType(CustomActionButton))); + await tester + .pump(const Duration(milliseconds: 600)); // Recognize long press + await tester + .pump(const Duration(milliseconds: 300)); // Allow timer to trigger - // Now find the specific `Container` inside `CustomActionDisable` - final containerFinder = find.descendant( - of: customActionDisableFinder, - matching: find.byType(Container), - ); - - expect( - containerFinder, findsOneWidget); // Ensure there's only one container - - // Verify the decoration of the Container (background color and border) - final container = tester.widget(containerFinder); - final decoration = container.decoration as BoxDecoration; + // Release the gesture + await gesture.up(); - // Check for the disabled color and transparent border - expect(decoration.color, - equals(Theme.of(tester.element(containerFinder)).disabledColor)); - expect(decoration.border?.top.color, equals(Colors.transparent)); - - // Ensure no interaction (disabled state) - await tester.tap(find.byType(CustomActionButton)); - await tester.pump(); // No state change should occur + expect(longPressCounter, greaterThan(0)); }); - }); - group('CustomIconText Tests', () { - testWidgets('CustomIconText renders with correct icon and text', + testWidgets('CustomActionButton shows disabled state correctly', (WidgetTester tester) async { + int counter = 0; + await tester.pumpWidget( - const MaterialApp( + MaterialApp( home: Scaffold( - body: CustomIconText( - icon: Icons.star, - text: 'Star', - color: Colors.amber, + body: CustomActionButton.elevated( + onPressed: null, // Disabled state + child: const Text('Disabled Button'), ), ), ), ); - expect(find.byIcon(Icons.star), findsOneWidget); - expect(find.text('Star'), findsOneWidget); + // Verify the disabled button is present with correct text + expect(find.text('Disabled Button'), findsOneWidget); + + // Locate the CustomActionButton widget + final customActionButtonFinder = find.byType(CustomActionButton); + expect(customActionButtonFinder, findsOneWidget); + + // Find the specific AbsorbPointer inside the CustomActionButton + final absorbPointerFinder = find.descendant( + of: customActionButtonFinder, matching: find.byType(AbsorbPointer)); + expect(absorbPointerFinder, findsOneWidget); + + // Ensure that the AbsorbPointer is absorbing (i.e., button is disabled) + final absorbPointerWidget = + tester.widget(absorbPointerFinder); + expect(absorbPointerWidget.absorbing, true); + + // Try to tap the button + await tester.tap(customActionButtonFinder, warnIfMissed: false); + await tester.pump(); + + // Counter should not have incremented + expect(counter, 0); }); }); } diff --git a/test/expandable_list_tile_button_test.dart b/test/expandable_list_tile_button_test.dart index fdfaa2f..9609e35 100644 --- a/test/expandable_list_tile_button_test.dart +++ b/test/expandable_list_tile_button_test.dart @@ -17,7 +17,7 @@ void main() { color: Colors.grey[200], child: const Text('Expanded content goes here'), ), - buttonBody: const Text('Expandable ListTile Button'), + title: const Text('Expandable ListTile Button'), backgroundColor: Colors.white, ), ), @@ -58,7 +58,7 @@ void main() { color: Colors.grey[200], child: const Text('Expanded content goes here'), ), - customHeader: (tapAction) => GestureDetector( + customHeader: (tapAction, isExpanded) => GestureDetector( onTap: () => tapAction.call(), child: Container( padding: const EdgeInsets.all(16.0), diff --git a/test/increment_decrement_widget_test.dart b/test/increment_decrement_widget_test.dart index ba1028e..d89c9ac 100644 --- a/test/increment_decrement_widget_test.dart +++ b/test/increment_decrement_widget_test.dart @@ -3,13 +3,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:risto_widgets/risto_widgets.dart'; void main() { - group('IncrementDecrementWidget', () { + group('IncrementDecrementWidget - Synchronous Callbacks', () { testWidgets('Initial state is displayed correctly', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: _IncrementDecrementTestWidget(), + body: _IncrementDecrementSyncTestWidget(), ), ), ); @@ -23,7 +23,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: _IncrementDecrementTestWidget(), + body: _IncrementDecrementSyncTestWidget(), ), ), ); @@ -41,7 +41,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: _IncrementDecrementTestWidget(), + body: _IncrementDecrementSyncTestWidget(), ), ), ); @@ -63,7 +63,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: _IncrementDecrementTestWidget(), + body: _IncrementDecrementSyncTestWidget(), ), ), ); @@ -72,8 +72,8 @@ void main() { final decrementButton = find.widgetWithIcon(CustomActionButton, Icons.remove); final CustomActionButton buttonWidget = tester.widget(decrementButton); - expect(buttonWidget.onPressed, - isNull); // onPressed should be null when disabled + expect(buttonWidget.onPressed, isNull, + reason: 'Decrement button should be disabled at minValue'); }); testWidgets('Increment button is disabled when quantity is at maxQuantity', @@ -81,7 +81,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: _IncrementDecrementTestWidget(), + body: _IncrementDecrementSyncTestWidget(), ), ), ); @@ -96,21 +96,136 @@ void main() { final incrementButton = find.widgetWithIcon(CustomActionButton, Icons.add); final CustomActionButton buttonWidget = tester.widget(incrementButton); - expect(buttonWidget.onPressed, - isNull); // onPressed should be null when disabled + expect(buttonWidget.onPressed, isNull, + reason: 'Increment button should be disabled at maxQuantity'); + }); + }); + + group('IncrementDecrementWidget - Asynchronous Callbacks', () { + testWidgets('Initial state is displayed correctly', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: _IncrementDecrementAsyncTestWidget(), + ), + ), + ); + + // Verify the initial quantity is 1. + expect(find.text('1'), findsOneWidget); + }); + + testWidgets( + 'Increments quantity when add button is tapped with async callback', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: _IncrementDecrementAsyncTestWidget(), + ), + ), + ); + + // Tap the increment button. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); // Start the async operation. + + // Since the async operation takes 100ms, pump until completion. + await tester.pump(const Duration(milliseconds: 100)); + + // Verify the quantity increases to 2 (1 + 1 from async callback). + expect(find.text('2'), findsOneWidget); + }); + + testWidgets( + 'Decrements quantity when remove button is tapped with async callback', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: _IncrementDecrementAsyncTestWidget(), + ), + ), + ); + + // Tap the increment button once to set quantity to 2. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); // Start the async operation. + await tester.pump(const Duration(milliseconds: 100)); + + // Tap the decrement button. + await tester.tap(find.byIcon(Icons.remove)); + await tester.pump(); // Start the async operation. + await tester.pump(const Duration(milliseconds: 100)); + + // Verify the quantity decreases back to 1 (2 - 1 + 0 from async callback). + expect(find.text('1'), findsOneWidget); + }); + + testWidgets( + 'Decrement button is disabled when quantity is at minValue with async callback', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: _IncrementDecrementAsyncTestWidget(), + ), + ), + ); + + // Ensure quantity is at minValue (1). + expect(find.text('1'), findsOneWidget); + + // Verify the decrement button is disabled. + final decrementButton = + find.widgetWithIcon(CustomActionButton, Icons.remove); + final CustomActionButton buttonWidget = tester.widget(decrementButton); + expect(buttonWidget.onPressed, isNull, + reason: 'Decrement button should be disabled at minValue'); + }); + + testWidgets( + 'Increment button is disabled when quantity is at maxQuantity with async callback', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: _IncrementDecrementAsyncTestWidget(), + ), + ), + ); + + // Tap the increment button until quantity reaches maxQuantity (10). + for (int i = 0; i < 9; i++) { + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); // Start async operation + await tester.pump( + const Duration(milliseconds: 100)); // Complete async operation + } + + // Since maxQuantity is 10, quantity should cap at 10. + expect(find.text('10'), findsOneWidget); + + // Verify the increment button is disabled when quantity reaches maxQuantity. + final incrementButton = + find.widgetWithIcon(CustomActionButton, Icons.add); + final CustomActionButton buttonWidget = tester.widget(incrementButton); + expect(buttonWidget.onPressed, isNull, + reason: 'Increment button should be disabled at maxQuantity'); }); }); } -// A Stateful widget to manage the state during tests -class _IncrementDecrementTestWidget extends StatefulWidget { +// A Stateful widget to manage the state during synchronous tests +class _IncrementDecrementSyncTestWidget extends StatefulWidget { @override - __IncrementDecrementTestWidgetState createState() => - __IncrementDecrementTestWidgetState(); + __IncrementDecrementSyncTestWidgetState createState() => + __IncrementDecrementSyncTestWidgetState(); } -class __IncrementDecrementTestWidgetState - extends State<_IncrementDecrementTestWidget> { +class __IncrementDecrementSyncTestWidgetState + extends State<_IncrementDecrementSyncTestWidget> { int quantity = 1; @override @@ -119,16 +234,41 @@ class __IncrementDecrementTestWidgetState quantity: quantity, maxQuantity: 10, minValue: 1, - onIncrement: () { + onChanged: (newQuantity) { setState(() { - quantity++; - }); - }, - onDecrement: () { - setState(() { - quantity--; + quantity = newQuantity; }); + return quantity; // Returning int? }, ); } } + +// A Stateful widget to manage the state during asynchronous tests +class _IncrementDecrementAsyncTestWidget extends StatefulWidget { + @override + __IncrementDecrementAsyncTestWidgetState createState() => + __IncrementDecrementAsyncTestWidgetState(); +} + +class __IncrementDecrementAsyncTestWidgetState + extends State<_IncrementDecrementAsyncTestWidget> { + int quantity = 1; + + Future asyncOnChanged(int newQuantity) async { + // Simulate an asynchronous operation, e.g., server validation + await Future.delayed(const Duration(milliseconds: 100)); + // Return the updated quantity without modification + return newQuantity; + } + + @override + Widget build(BuildContext context) { + return IncrementDecrementWidget( + quantity: quantity, + maxQuantity: 10, + minValue: 1, + onChanged: asyncOnChanged, // Returning Future + ); + } +} diff --git a/test/list_tile_button_test.dart b/test/list_tile_button_test.dart index 84cf3a7..77f2c77 100644 --- a/test/list_tile_button_test.dart +++ b/test/list_tile_button_test.dart @@ -1,32 +1,138 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:risto_widgets/risto_widgets.dart'; +import 'package:risto_widgets/widgets/buttons/list_tile_button.dart'; void main() { - testWidgets('ListTileButton test', (WidgetTester tester) async { - bool buttonPressed = false; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: ListTileButton( - body: const Text('ListTileButton'), - leading: const Icon(Icons.list), - onPressed: () { - buttonPressed = true; - }, + group('ListTileButton Tests', () { + testWidgets('ListTileButton renders correctly and responds to tap', + (WidgetTester tester) async { + bool buttonPressed = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ListTileButton( + body: const Text('ListTileButton'), + leading: const Icon(Icons.list), + onPressed: () { + buttonPressed = true; + }, + ), + ), + ), + ); + + // Verify the ListTileButton is present. + expect(find.text('ListTileButton'), findsOneWidget); + expect(find.byIcon(Icons.list), findsOneWidget); + + // Tap the ListTileButton and verify it triggers the onPressed callback. + await tester.tap(find.byType(ListTileButton)); + await tester.pump(); // Rebuild the widget with the new state. + + expect(buttonPressed, true); + }); + + testWidgets('ListTileButton displays subtitle and trailing widgets', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ListTileButton( + body: const Text('ListTileButton'), + subtitle: const Text('Subtitle Text'), + trailing: const Icon(Icons.arrow_forward), + onPressed: () {}, + ), ), ), - ), - ); + ); + + // Verify the body, subtitle, and trailing widgets are present. + expect(find.text('ListTileButton'), findsOneWidget); + expect(find.text('Subtitle Text'), findsOneWidget); + expect(find.byIcon(Icons.arrow_forward), findsOneWidget); + }); + }); - // Verify the ListTileButton is present. - expect(find.text('ListTileButton'), findsOneWidget); + group('IconListTileButton Tests', () { + testWidgets('IconListTileButton renders correctly with custom sizeFactor', + (WidgetTester tester) async { + const double sizeFactor = 1.5; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: IconListTileButton( + icon: Icons.star, + title: const Text('Icon ListTileButton'), + sizeFactor: sizeFactor, + onPressed: () {}, + ), + ), + ), + ); - // Tap the ListTileButton and verify it triggers the onPressed callback. - await tester.tap(find.byType(ListTileButton)); - await tester.pump(); // Rebuild the widget with the new state. + // Verify the IconListTileButton is present. + expect(find.text('Icon ListTileButton'), findsOneWidget); + expect(find.byIcon(Icons.star), findsOneWidget); + + // Verify the icon size is adjusted according to sizeFactor. + final iconFinder = find.byIcon(Icons.star); + final Icon iconWidget = tester.widget(iconFinder); + expect(iconWidget.size, + 24.0 * sizeFactor); // Default size 24.0 times sizeFactor + }); + + testWidgets('IconListTileButton responds to tap', + (WidgetTester tester) async { + bool buttonPressed = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: IconListTileButton( + icon: Icons.star, + title: const Text('Icon ListTileButton'), + onPressed: () { + buttonPressed = true; + }, + ), + ), + ), + ); + + // Tap the IconListTileButton and verify it triggers the onPressed callback. + await tester.tap(find.byType(IconListTileButton)); + await tester.pump(); // Rebuild the widget with the new state. + + expect(buttonPressed, true); + }); + }); + + group('DoubleListTileButtons Tests', () { + testWidgets('DoubleListTileButtons renders two ListTileButtons', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DoubleListTileButtons( + firstButton: ListTileButton( + body: const Text('First Button'), + onPressed: () {}, + ), + secondButton: ListTileButton( + body: const Text('Second Button'), + onPressed: () {}, + ), + ), + ), + ), + ); - expect(buttonPressed, true); + // Verify both buttons are present. + expect(find.text('First Button'), findsOneWidget); + expect(find.text('Second Button'), findsOneWidget); + }); }); } diff --git a/test/open_custom_sheet_test.dart b/test/open_custom_sheet_test.dart index b17e2d1..9f49193 100644 --- a/test/open_custom_sheet_test.dart +++ b/test/open_custom_sheet_test.dart @@ -3,44 +3,174 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:risto_widgets/risto_widgets.dart'; void main() { - testWidgets('OpenCustomSheet test', (WidgetTester tester) async { - bool confirmed = false; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Builder( - builder: (context) => ElevatedButton( - onPressed: () { - OpenCustomSheet.openConfirmSheet( - context, - body: const Text('Are you sure?'), - onClose: (value) { - confirmed = value == true; - }, - ); - }, - child: const Text('Open Sheet'), + group('OpenCustomSheet Tests', () { + testWidgets('OpenCustomSheet Confirm Sheet Test', + (WidgetTester tester) async { + bool confirmed = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () { + OpenCustomSheet.openConfirmSheet( + context, + body: const Text('Are you sure?'), + onClose: (value) { + confirmed = value == true; + }, + ).show(context); // Make sure to call the show() method + }, + child: const Text('Open Sheet'), + ), + ), + ), + ), + ); + + // Verify the button to open the sheet is present. + expect(find.text('Open Sheet'), findsOneWidget); + + // Tap the button to open the sheet. + await tester.tap(find.text('Open Sheet')); + await tester.pumpAndSettle(); // Wait for the modal to render. + + // Ensure the sheet content is displayed. + expect(find.text('Are you sure?'), findsOneWidget); + + // Tap the Confirm button and verify the onClose callback is triggered. + await tester.tap(find.text('Confirm')); + await tester.pumpAndSettle(); // Wait for the sheet to close. + + expect(confirmed, true); + }); + + testWidgets('OpenCustomSheet Close Sheet Test', + (WidgetTester tester) async { + bool closed = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () { + OpenCustomSheet.openConfirmSheet( + context, + body: const Text('Are you sure?'), + onClose: (value) { + closed = value == false; + }, + ).show(context); // Call show() method + }, + child: const Text('Open Sheet'), + ), + ), + ), + ), + ); + + // Tap the button to open the sheet. + await tester.tap(find.text('Open Sheet')); + await tester.pumpAndSettle(); // Wait for the modal to render. + + // Ensure the sheet content is displayed. + expect(find.text('Are you sure?'), findsOneWidget); + + // Tap the Close button and verify the onClose callback is triggered. + await tester.tap(find.text('Close')); + await tester.pumpAndSettle(); // Wait for the sheet to close. + + expect(closed, true); + }); + + testWidgets('OpenCustomSheet Scrollable Sheet Test', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () { + OpenCustomSheet.scrollableSheet( + context, + initialChildSize: 0.5, // Start smaller + minChildSize: 0.25, + maxChildSize: 1.0, + body: ({scrollController}) { + return ListView.builder( + controller: scrollController, + itemCount: 50, + itemBuilder: (context, index) => ListTile( + title: Text('Item $index'), + ), + ); + }, + ).show(context); + }, + child: const Text('Open Scrollable Sheet'), + ), ), ), ), - ), - ); + ); + + // Tap the button to open the sheet + await tester.tap(find.text('Open Scrollable Sheet')); + await tester.pumpAndSettle(); - // Verify the button to open the sheet is present. - expect(find.text('Open Sheet'), findsOneWidget); + // Now scroll the list + await tester.dragUntilVisible( + find.text('Item 19'), + find.byType(ListView), + const Offset(0, -100), + ); + await tester.pumpAndSettle(); + + // Check if Item 19 is visible + expect(find.text('Item 19'), findsOneWidget); + }); + + testWidgets('OpenCustomSheet barrier dismiss test', + (WidgetTester tester) async { + bool dismissed = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () { + OpenCustomSheet( + barrierDismissible: true, + body: ({scrollController}) => + const Text('Dismissable Sheet'), + onClose: (value) { + dismissed = value == null; + }, + ).show(context); + }, + child: const Text('Open Dismissable Sheet'), + ), + ), + ), + ), + ); - // Tap the button to open the sheet. - await tester.tap(find.text('Open Sheet')); - await tester.pumpAndSettle(); // Wait for the sheet to appear. + // Verify the button to open the sheet is present. + expect(find.text('Open Dismissable Sheet'), findsOneWidget); - // Verify the sheet content is displayed. - expect(find.text('Are you sure?'), findsOneWidget); + // Tap the button to open the sheet. + await tester.tap(find.text('Open Dismissable Sheet')); + await tester.pumpAndSettle(); // Wait for the sheet to appear. - // Tap the Confirm button and verify the onClose callback is triggered. - await tester.tap(find.text('Confirm')); - await tester.pumpAndSettle(); // Wait for the sheet to close. + // Tap outside the sheet to dismiss it. + await tester.tapAt(const Offset(10, 10)); + await tester.pumpAndSettle(); // Wait for the sheet to dismiss. - expect(confirmed, true); + // Verify that the dismiss action was triggered. + expect(dismissed, true); + }); }); }