diff --git a/.flutter-stylizer.yml b/.flutter-stylizer.yml deleted file mode 100644 index a9db281..0000000 --- a/.flutter-stylizer.yml +++ /dev/null @@ -1,13 +0,0 @@ -groupAndSortGetterMethods: true -sortOtherMethods: true -memberOrdering: public-static-variables - private-static-variables - public-override-variables - public-instance-variables - private-instance-variables - public-constructor - named-constructors - public-override-methods - public-other-methods - private-other-methods - build-method diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9cef013..7d5a0c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ jobs: with: channel: 'stable' - run: flutter pub get + - run: dart format --fix --set-exit-if-changed . - run: flutter analyze - run: flutter test --coverage - name: Setup LCOV diff --git a/example/example.dart b/example/example.dart new file mode 100644 index 0000000..2a241ed --- /dev/null +++ b/example/example.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:generic_dropdown_widget/generic_dropdown_widget.dart'; + +class Dropdown extends StatelessWidget { + const Dropdown({super.key}); + + @override + Widget build(BuildContext context) => GenericDropdown( + openOnRender: false, + closeOnOutsideTap: true, + toggleBuilder: (context, isOpen) => Container( + height: 50, + width: 50, + color: isOpen ? Colors.green : Colors.red, + ), + contentBuilder: (context, repaint, close) => Container( + height: 100, + width: 100, + color: Colors.blue, + ), + ); +} diff --git a/lib/src/generic_dropdown.dart b/lib/src/generic_dropdown.dart index 2b2ba04..9b62817 100644 --- a/lib/src/generic_dropdown.dart +++ b/lib/src/generic_dropdown.dart @@ -44,7 +44,8 @@ enum DropdownDirection { downRight, } -typedef ContentBuilder = Widget Function(BuildContext context, VoidCallback repaint, VoidCallback close); +typedef ContentBuilder = Widget Function( + BuildContext context, VoidCallback repaint, VoidCallback close); typedef ToggleBuilder = Widget Function(BuildContext context, bool isOpen); /// A generic dropdown widget that enables arbitrary content @@ -130,7 +131,10 @@ class _GenericDropdownState extends State { } RenderBox? _ancestor(BuildContext context) => - GenericDropdownConfigProvider.of(context)?.rootScreenKey?.currentContext?.findRenderObject() as RenderBox?; + GenericDropdownConfigProvider.of(context) + ?.rootScreenKey + ?.currentContext + ?.findRenderObject() as RenderBox?; void _close() { _overlayEntry?.remove(); @@ -142,7 +146,8 @@ class _GenericDropdownState extends State { final renderBox = context.findRenderObject() as RenderBox; final size = renderBox.size; - final togglePosition = renderBox.localToGlobal(Offset.zero, ancestor: _ancestor(context)); + final togglePosition = + renderBox.localToGlobal(Offset.zero, ancestor: _ancestor(context)); final screenSize = _screenSize(context); @@ -166,61 +171,93 @@ class _GenericDropdownState extends State { double? top, left, bottom, right; // Anchor TOP LEFT - if (widget.anchor == DropdownAnchor.topLeft && widget.direction == DropdownDirection.upLeft) { + if (widget.anchor == DropdownAnchor.topLeft && + widget.direction == DropdownDirection.upLeft) { bottom = screenSize.height - togglePosition.dy + widget.offset.dy; right = screenSize.width - togglePosition.dx + widget.offset.dx; - } else if (widget.anchor == DropdownAnchor.topLeft && widget.direction == DropdownDirection.upRight) { + } else if (widget.anchor == DropdownAnchor.topLeft && + widget.direction == DropdownDirection.upRight) { bottom = screenSize.height - togglePosition.dy + widget.offset.dy; left = togglePosition.dx + widget.offset.dx; - } else if (widget.anchor == DropdownAnchor.topLeft && widget.direction == DropdownDirection.downLeft) { + } else if (widget.anchor == DropdownAnchor.topLeft && + widget.direction == DropdownDirection.downLeft) { top = togglePosition.dy + widget.offset.dy; right = screenSize.width - togglePosition.dx + widget.offset.dx; - } else if (widget.anchor == DropdownAnchor.topLeft && widget.direction == DropdownDirection.downRight) { + } else if (widget.anchor == DropdownAnchor.topLeft && + widget.direction == DropdownDirection.downRight) { top = togglePosition.dy + widget.offset.dy; left = togglePosition.dx + widget.offset.dx; } // Anchor TOP RIGHT - if (widget.anchor == DropdownAnchor.topRight && widget.direction == DropdownDirection.upLeft) { + if (widget.anchor == DropdownAnchor.topRight && + widget.direction == DropdownDirection.upLeft) { bottom = screenSize.height - togglePosition.dy + widget.offset.dy; - right = screenSize.width - togglePosition.dx + widget.offset.dx - size.width; - } else if (widget.anchor == DropdownAnchor.topRight && widget.direction == DropdownDirection.upRight) { + right = + screenSize.width - togglePosition.dx + widget.offset.dx - size.width; + } else if (widget.anchor == DropdownAnchor.topRight && + widget.direction == DropdownDirection.upRight) { bottom = screenSize.height - togglePosition.dy + widget.offset.dy; left = togglePosition.dx + widget.offset.dx + size.width; - } else if (widget.anchor == DropdownAnchor.topRight && widget.direction == DropdownDirection.downLeft) { + } else if (widget.anchor == DropdownAnchor.topRight && + widget.direction == DropdownDirection.downLeft) { top = togglePosition.dy + widget.offset.dy; - right = screenSize.width - togglePosition.dx + widget.offset.dx - size.width; - } else if (widget.anchor == DropdownAnchor.topRight && widget.direction == DropdownDirection.downRight) { + right = + screenSize.width - togglePosition.dx + widget.offset.dx - size.width; + } else if (widget.anchor == DropdownAnchor.topRight && + widget.direction == DropdownDirection.downRight) { top = togglePosition.dy + widget.offset.dy; left = togglePosition.dx + widget.offset.dx + size.width; } // Anchor BOTTOM LEFT - if (widget.anchor == DropdownAnchor.bottomLeft && widget.direction == DropdownDirection.upLeft) { - bottom = screenSize.height - togglePosition.dy + widget.offset.dy - size.height; + if (widget.anchor == DropdownAnchor.bottomLeft && + widget.direction == DropdownDirection.upLeft) { + bottom = screenSize.height - + togglePosition.dy + + widget.offset.dy - + size.height; right = screenSize.width - togglePosition.dx + widget.offset.dx; - } else if (widget.anchor == DropdownAnchor.bottomLeft && widget.direction == DropdownDirection.upRight) { - bottom = screenSize.height - togglePosition.dy + widget.offset.dy - size.height; + } else if (widget.anchor == DropdownAnchor.bottomLeft && + widget.direction == DropdownDirection.upRight) { + bottom = screenSize.height - + togglePosition.dy + + widget.offset.dy - + size.height; left = togglePosition.dx + widget.offset.dx; - } else if (widget.anchor == DropdownAnchor.bottomLeft && widget.direction == DropdownDirection.downLeft) { + } else if (widget.anchor == DropdownAnchor.bottomLeft && + widget.direction == DropdownDirection.downLeft) { top = togglePosition.dy + widget.offset.dy + size.height; right = screenSize.width - togglePosition.dx + widget.offset.dx; - } else if (widget.anchor == DropdownAnchor.bottomLeft && widget.direction == DropdownDirection.downRight) { + } else if (widget.anchor == DropdownAnchor.bottomLeft && + widget.direction == DropdownDirection.downRight) { top = togglePosition.dy + widget.offset.dy + size.height; left = togglePosition.dx + widget.offset.dx; } // Anchor BOTTOM RIGHT - if (widget.anchor == DropdownAnchor.bottomRight && widget.direction == DropdownDirection.upLeft) { - bottom = screenSize.height - togglePosition.dy + widget.offset.dy - size.height; - right = screenSize.width - togglePosition.dx + widget.offset.dx - size.width; - } else if (widget.anchor == DropdownAnchor.bottomRight && widget.direction == DropdownDirection.upRight) { - bottom = screenSize.height - togglePosition.dy + widget.offset.dy - size.height; + if (widget.anchor == DropdownAnchor.bottomRight && + widget.direction == DropdownDirection.upLeft) { + bottom = screenSize.height - + togglePosition.dy + + widget.offset.dy - + size.height; + right = + screenSize.width - togglePosition.dx + widget.offset.dx - size.width; + } else if (widget.anchor == DropdownAnchor.bottomRight && + widget.direction == DropdownDirection.upRight) { + bottom = screenSize.height - + togglePosition.dy + + widget.offset.dy - + size.height; left = togglePosition.dx + widget.offset.dx + size.width; - } else if (widget.anchor == DropdownAnchor.bottomRight && widget.direction == DropdownDirection.downLeft) { + } else if (widget.anchor == DropdownAnchor.bottomRight && + widget.direction == DropdownDirection.downLeft) { top = togglePosition.dy + widget.offset.dy + size.height; - right = screenSize.width - togglePosition.dx + widget.offset.dx - size.width; - } else if (widget.anchor == DropdownAnchor.bottomRight && widget.direction == DropdownDirection.downRight) { + right = + screenSize.width - togglePosition.dx + widget.offset.dx - size.width; + } else if (widget.anchor == DropdownAnchor.bottomRight && + widget.direction == DropdownDirection.downRight) { top = togglePosition.dy + widget.offset.dy + size.height; left = togglePosition.dx + widget.offset.dx + size.width; } @@ -249,8 +286,8 @@ class _GenericDropdownState extends State { // content. }, child: StatefulBuilder( - builder: (context, setState) => - widget.contentBuilder.call(context, () => setState(() {}), _close)), + builder: (context, setState) => widget.contentBuilder + .call(context, () => setState(() {}), _close)), ), ), ], @@ -265,7 +302,8 @@ class _GenericDropdownState extends State { setState(() => _isOpen = true); } - Size _screenSize(BuildContext context) => _ancestor(context)?.size ?? MediaQuery.of(context).size; + Size _screenSize(BuildContext context) => + _ancestor(context)?.size ?? MediaQuery.of(context).size; @override Widget build(BuildContext context) => Row( diff --git a/lib/src/generic_dropdown_config_provider.dart b/lib/src/generic_dropdown_config_provider.dart index 350e1a9..20e8046 100644 --- a/lib/src/generic_dropdown_config_provider.dart +++ b/lib/src/generic_dropdown_config_provider.dart @@ -18,11 +18,13 @@ class GenericDropdownConfigProvider extends InheritedWidget { /// of the content overlay to the toggle. final GlobalKey? rootScreenKey; - const GenericDropdownConfigProvider({super.key, this.rootScreenKey, required super.child}); + const GenericDropdownConfigProvider( + {super.key, this.rootScreenKey, required super.child}); @override - bool updateShouldNotify(GenericDropdownConfigProvider oldWidget) => rootScreenKey != oldWidget.rootScreenKey; + bool updateShouldNotify(GenericDropdownConfigProvider oldWidget) => + rootScreenKey != oldWidget.rootScreenKey; - static GenericDropdownConfigProvider? of(BuildContext context) => - context.dependOnInheritedWidgetOfExactType(); + static GenericDropdownConfigProvider? of(BuildContext context) => context + .dependOnInheritedWidgetOfExactType(); } diff --git a/storybook/lib/main.dart b/storybook/lib/main.dart index 75b8d25..a4100b7 100644 --- a/storybook/lib/main.dart +++ b/storybook/lib/main.dart @@ -20,7 +20,8 @@ void main() { home: Scaffold( body: SafeArea( child: GenericDropdownConfigProvider( - rootScreenKey: rootKey, child: Center(key: UniqueKey(), child: child))))), + rootScreenKey: rootKey, + child: Center(key: UniqueKey(), child: child))))), // initialStory: 'Generic Dropdown', stories: [ _dropdown(), @@ -73,26 +74,37 @@ Story _dropdown() => Story( toggleBuilder: (context, isOpen) => Container( height: 120, width: 120, - color: isOpen ? Colors.amber.withOpacity(.25) : Colors.blue.withOpacity(.25), + color: isOpen + ? Colors.amber.withOpacity(.25) + : Colors.blue.withOpacity(.25), child: Text('Toggle (${isOpen ? 'Open' : 'Closed'})'), ), offset: Offset( - context.knobs.sliderInt(label: 'X Offset', initial: 0, min: -100, max: 100).toDouble(), - context.knobs.sliderInt(label: 'Y Offset', initial: 0, min: -100, max: 100).toDouble(), + context.knobs + .sliderInt(label: 'X Offset', initial: 0, min: -100, max: 100) + .toDouble(), + context.knobs + .sliderInt(label: 'Y Offset', initial: 0, min: -100, max: 100) + .toDouble(), ), anchor: context.knobs.options( label: 'Anchor', description: 'The anchor for the content dropdown.', initial: DropdownAnchor.bottomLeft, - options: DropdownAnchor.values.map((v) => Option(label: v.name, value: v)).toList()), + options: DropdownAnchor.values + .map((v) => Option(label: v.name, value: v)) + .toList()), direction: context.knobs.options( label: 'Direction', description: 'The direction where the dropdown should open to.', initial: DropdownDirection.downRight, - options: DropdownDirection.values.map((v) => Option(label: v.name, value: v)).toList()), + options: DropdownDirection.values + .map((v) => Option(label: v.name, value: v)) + .toList()), closeOnOutsideTap: context.knobs.boolean( label: 'Close On Outside Tap', - description: 'Whether the content is closed on an outside tap or only if the content calls close().', + description: + 'Whether the content is closed on an outside tap or only if the content calls close().', initial: true), )); }); @@ -142,26 +154,37 @@ Story _openDropdown() => Story( toggleBuilder: (context, isOpen) => Container( height: 120, width: 120, - color: isOpen ? Colors.amber.withOpacity(.25) : Colors.blue.withOpacity(.25), + color: isOpen + ? Colors.amber.withOpacity(.25) + : Colors.blue.withOpacity(.25), child: Text('Toggle (${isOpen ? 'Open' : 'Closed'})'), ), offset: Offset( - context.knobs.sliderInt(label: 'X Offset', initial: 0, min: -100, max: 100).toDouble(), - context.knobs.sliderInt(label: 'Y Offset', initial: 0, min: -100, max: 100).toDouble(), + context.knobs + .sliderInt(label: 'X Offset', initial: 0, min: -100, max: 100) + .toDouble(), + context.knobs + .sliderInt(label: 'Y Offset', initial: 0, min: -100, max: 100) + .toDouble(), ), anchor: context.knobs.options( label: 'Anchor', description: 'The anchor for the content dropdown.', initial: DropdownAnchor.bottomLeft, - options: DropdownAnchor.values.map((v) => Option(label: v.name, value: v)).toList()), + options: DropdownAnchor.values + .map((v) => Option(label: v.name, value: v)) + .toList()), direction: context.knobs.options( label: 'Direction', description: 'The direction where the dropdown should open to.', initial: DropdownDirection.downRight, - options: DropdownDirection.values.map((v) => Option(label: v.name, value: v)).toList()), + options: DropdownDirection.values + .map((v) => Option(label: v.name, value: v)) + .toList()), closeOnOutsideTap: context.knobs.boolean( label: 'Close On Outside Tap', - description: 'Whether the content is closed on an outside tap or only if the content calls close().', + description: + 'Whether the content is closed on an outside tap or only if the content calls close().', initial: true), )); }); diff --git a/test/generic_dropdown_widget_test.dart b/test/generic_dropdown_widget_test.dart index c028b55..ec1f9a6 100644 --- a/test/generic_dropdown_widget_test.dart +++ b/test/generic_dropdown_widget_test.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:generic_dropdown_widget/src/generic_dropdown.dart'; -Widget _wrapper(Widget child) => MaterialApp(home: Scaffold(body: SafeArea(child: Center(child: child)))); +Widget _wrapper(Widget child) => + MaterialApp(home: Scaffold(body: SafeArea(child: Center(child: child)))); class _PlacementCase { final DropdownAnchor anchor; @@ -15,17 +16,17 @@ class _PlacementCase { required this.translation, }); - Offset bottomLeft(Rect toggle, Rect content) => - _anchor(toggle).translate(content.width * translation[2][0], content.height * translation[2][1]); + Offset bottomLeft(Rect toggle, Rect content) => _anchor(toggle).translate( + content.width * translation[2][0], content.height * translation[2][1]); - Offset bottomRight(Rect toggle, Rect content) => - _anchor(toggle).translate(content.width * translation[3][0], content.height * translation[3][1]); + Offset bottomRight(Rect toggle, Rect content) => _anchor(toggle).translate( + content.width * translation[3][0], content.height * translation[3][1]); - Offset topLeft(Rect toggle, Rect content) => - _anchor(toggle).translate(content.width * translation[0][0], content.height * translation[0][1]); + Offset topLeft(Rect toggle, Rect content) => _anchor(toggle).translate( + content.width * translation[0][0], content.height * translation[0][1]); - Offset topRight(Rect toggle, Rect content) => - _anchor(toggle).translate(content.width * translation[1][0], content.height * translation[1][1]); + Offset topRight(Rect toggle, Rect content) => _anchor(toggle).translate( + content.width * translation[1][0], content.height * translation[1][1]); Offset _anchor(Rect toggle) { switch (anchor) { @@ -140,7 +141,8 @@ void main() { expect(find.byKey(contentKey), findsNothing); }); - testWidgets('should render content when "openOnRender" is "true".', (tester) async { + testWidgets('should render content when "openOnRender" is "true".', + (tester) async { const toggleKey = Key('toggle'); const contentKey = Key('content'); @@ -165,7 +167,8 @@ void main() { expect(find.byKey(contentKey), findsOneWidget); }); - testWidgets('should not close content when "closeOnOutsideTap" is "false".', (tester) async { + testWidgets('should not close content when "closeOnOutsideTap" is "false".', + (tester) async { const toggleKey = Key('toggle'); const contentKey = Key('content'); @@ -213,7 +216,10 @@ void main() { height: 50, width: 200, color: Colors.blue, - child: TextButton(onPressed: close, key: closeButtonKey, child: const Text('Close'))), + child: TextButton( + onPressed: close, + key: closeButtonKey, + child: const Text('Close'))), ))); await tester.pumpAndSettle(); @@ -464,10 +470,16 @@ void main() { final toggleRect = tester.getRect(find.byKey(toggleKey)); final contentRect = tester.getRect(find.byKey(contentKey)); - expect(contentRect.topLeft, testCase.topLeft(toggleRect, contentRect), reason: 'topLeft mismatch'); - expect(contentRect.topRight, testCase.topRight(toggleRect, contentRect), reason: 'topRight mismatch'); - expect(contentRect.bottomLeft, testCase.bottomLeft(toggleRect, contentRect), reason: 'bottomLeft mismatch'); - expect(contentRect.bottomRight, testCase.bottomRight(toggleRect, contentRect), + expect(contentRect.topLeft, testCase.topLeft(toggleRect, contentRect), + reason: 'topLeft mismatch'); + expect( + contentRect.topRight, testCase.topRight(toggleRect, contentRect), + reason: 'topRight mismatch'); + expect(contentRect.bottomLeft, + testCase.bottomLeft(toggleRect, contentRect), + reason: 'bottomLeft mismatch'); + expect(contentRect.bottomRight, + testCase.bottomRight(toggleRect, contentRect), reason: 'bottomRight mismatch'); }); }