diff --git a/stevia/lib/services.dart b/stevia/lib/services.dart index 677ae0df..4b2a032d 100644 --- a/stevia/lib/services.dart +++ b/stevia/lib/services.dart @@ -2,8 +2,9 @@ /// General-purpose widgets and Flutter services. /// /// * [ColorFilters] -/// * [IntTextInputFormatter] /// +/// * [CaseTextInputFormatter] +/// * [IntTextInputFormatter] /// /// ## Timer /// Controllers that simply the implementation of timers. diff --git a/stevia/lib/src/services/text_input_formatters.dart b/stevia/lib/src/services/text_input_formatters.dart index bb5de8f8..f9e25b6a 100644 --- a/stevia/lib/src/services/text_input_formatters.dart +++ b/stevia/lib/src/services/text_input_formatters.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Interval; import 'package:flutter/services.dart'; import 'package:sugar/sugar.dart'; import 'dart:math' as math; @@ -7,29 +7,31 @@ import 'dart:math' as math; /// /// There is **no** guarantee that the text being edited is an integer since it may be empty or `-`. /// -/// Empty text and a single `-` are ignored. Furthermore, a [IntTextInputFormatter] trims all commas separating parts of -/// the integer, and leading and trailing whitespaces. For example, both ` ` and `-` are allowed while ` 1,000 ` is trimmed -/// to `1000`. -/// +/// A [IntTextInputFormatter] trims all commas separating parts of the integer, and leading and trailing whitespaces. +/// For example, both ` ` and `,` are allowed while ` 1,000 ` is trimmed to `1000`. /// /// It is recommended to use set the enclosing [TextField]'s `keyboardType` to [TextInputType.number]. /// /// ```dart /// TextField( /// keyboardType: TextInputType.number, -/// inputFormatters: [ IntTextInputFormatter(Interval.closedOpen(0, 5)) ], // 0 <= value < 5 +/// inputFormatters: [ IntTextInputFormatter(Interval.closedOpen(-1, 5)) ], // -1 <= value < 5 /// ); /// ``` class IntTextInputFormatter extends TextInputFormatter { final Range _range; + final bool _hyphen; /// Creates a [IntTextInputFormatter] in the given [Range]. - IntTextInputFormatter(this._range): super(); + IntTextInputFormatter(this._range): _hyphen = switch (_range) { + Interval(min: (:final value, :final open)) || Min(:final value, :final open) => value < -1 || (value == -1 && !open), + _ => true, + }, super(); @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { final TextEditingValue(:text, :selection, :composing) = newValue; - if (text.isEmpty || text == '-') { + if (text.isEmpty || (_hyphen && text == '-')) { return newValue; } @@ -53,3 +55,38 @@ class IntTextInputFormatter extends TextInputFormatter { }; } } + +/// A [CaseTextInputFormatter] converts all characters to either upper or lower case. +/// +/// ```dart +/// TextField( +/// inputFormatters: [ const CaseTextInputFormatter.upper() ], +/// ); +/// ``` +class CaseTextInputFormatter extends TextInputFormatter { + + static String _upper(String string) => string.toUpperCase(); + + static String _lower(String string) => string.toLowerCase(); + + + final String Function(String) _format; + + /// Creates a [CaseTextInputFormatter] that converts all characters to uppercase. + const CaseTextInputFormatter.upper(): this._(_upper); + + /// Creates a [CaseTextInputFormatter] that converts all characters to lowercase. + const CaseTextInputFormatter.lower(): this._(_lower); + + const CaseTextInputFormatter._(this._format); + + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + if (_format(newValue.text) case final text when text != newValue.text) { + return newValue.copyWith(text: text); + } + + return newValue; + } + +} diff --git a/stevia/lib/src/widgets/positioned/positioned_route.dart b/stevia/lib/src/widgets/positioned/positioned_route.dart index 2dde96fa..f98678f3 100644 --- a/stevia/lib/src/widgets/positioned/positioned_route.dart +++ b/stevia/lib/src/widgets/positioned/positioned_route.dart @@ -1,8 +1,12 @@ import 'package:flutter/cupertino.dart'; -import 'package:meta/meta.dart'; +/// TODO class PositionedRoute extends PopupRoute { + /// Used build the route's primary contents. + /// + /// See [ModalRoute.buildPage] for complete definition of the parameters. + final RoutePageBuilder builder; @override final Color? barrierColor; @override @@ -14,16 +18,28 @@ class PositionedRoute extends PopupRoute { /// Creates a [PositionedRoute]. PositionedRoute({ + required this.builder, this.barrierLabel, this.barrierColor, this.barrierDismissible = true, this.transitionDuration = const Duration(milliseconds: 300), }); + + // @override + // Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + // return + // } + + @override + Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) => builder(context, animation, secondaryAnimation); + @override - Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { - // TODO: implement buildPage - throw UnimplementedError(); - } + Animation createAnimation() => CurvedAnimation( + parent: super.createAnimation(), + // A cubic animation curve that starts slowly and ends with an overshoot of its bounds before reaching its end. + curve: const Cubic(0.075, 0.82, 0.4, 1.065), + reverseCurve: Curves.easeIn, + ); } diff --git a/stevia/test/src/services/text_input_formatters_test.dart b/stevia/test/src/services/text_input_formatters_test.dart index e8d295eb..f7e8f497 100644 --- a/stevia/test/src/services/text_input_formatters_test.dart +++ b/stevia/test/src/services/text_input_formatters_test.dart @@ -6,42 +6,178 @@ import 'package:sugar/sugar.dart'; void main() { group('IntTextInputFormatter', () { - late Widget widget; - - setUp(() => widget = MaterialApp( - home: Scaffold( - body: TextField( - keyboardType: TextInputType.number, - inputFormatters: [ IntTextInputFormatter(Interval.closedOpen(-50, 50)) ], - ), - ) - )); - - for (final (actual, expected) in [ - ('-50', '-50'), - ('-51', ''), - ('49', '49'), - ('50', ''), - ('-', '-'), - ('0.0', ''), - (' 0 ', '0'), - ('1,0', '10'), - (' 1,0 ', '10'), - ]) { - testWidgets('values', (tester) async { + group('negative range', () { + late Widget widget; + + setUp(() => widget = MaterialApp( + home: Scaffold( + body: TextField( + keyboardType: TextInputType.number, + inputFormatters: [ IntTextInputFormatter(Interval.closedOpen(-50, 50)) ], + ), + ) + )); + + for (final (actual, expected) in [ + ('-50', '-50'), + ('-51', ''), + ('49', '49'), + ('50', ''), + ('-', '-'), + ('0.0', ''), + (' 0 ', '0'), + ('1,0', '10'), + (' 1,0 ', '10'), + ]) { + testWidgets('values', (tester) async { + await tester.pumpWidget(widget); + await tester.enterText(find.byType(TextField), actual); + + expect(find.text(expected), findsOneWidget); + }); + } + + testWidgets('empty string', (tester) async { await tester.pumpWidget(widget); - await tester.enterText(find.byType(TextField), actual); + await tester.enterText(find.byType(TextField), '1'); + await tester.enterText(find.byType(TextField), ''); - expect(find.text(expected), findsOneWidget); + expect(find.text(''), findsOneWidget); }); - } + }); + + group('[-1, 50)', () { + late Widget widget; + + setUp(() => widget = MaterialApp( + home: Scaffold( + body: TextField( + keyboardType: TextInputType.number, + inputFormatters: [ IntTextInputFormatter(Interval.closedOpen(-1, 50)) ], + ), + ) + )); + + for (final (actual, expected) in [ + ('-2', ''), + ('-1', '-1'), + ('-', '-'), + ]) { + testWidgets('values', (tester) async { + await tester.pumpWidget(widget); + await tester.enterText(find.byType(TextField), actual); + + expect(find.text(expected), findsOneWidget); + }); + } + }); + + group('(-1, 50)', () { + late Widget widget; + + setUp(() => widget = MaterialApp( + home: Scaffold( + body: TextField( + keyboardType: TextInputType.number, + inputFormatters: [ IntTextInputFormatter(Interval.open(-1, 50)) ], + ), + ) + )); + + for (final (actual, expected) in [ + ('-1', ''), + ('0', '0'), + ('-', ''), + ]) { + testWidgets('values', (tester) async { + await tester.pumpWidget(widget); + await tester.enterText(find.byType(TextField), actual); + + expect(find.text(expected), findsOneWidget); + }); + } + }); + + group('(-2, 50)', () { + late Widget widget; + + setUp(() => widget = MaterialApp( + home: Scaffold( + body: TextField( + keyboardType: TextInputType.number, + inputFormatters: [ IntTextInputFormatter(Interval.open(-2, 50)) ], + ), + ) + )); + + for (final (actual, expected) in [ + ('-2', ''), + ('-1', '-1'), + ('-', '-'), + ]) { + testWidgets('values', (tester) async { + await tester.pumpWidget(widget); + await tester.enterText(find.byType(TextField), actual); + + expect(find.text(expected), findsOneWidget); + }); + } + }); + }); + + group('CaseTextInputFormatter', () { + group('upper case', () { + late Widget widget; + + setUp(() => widget = const MaterialApp( + home: Scaffold( + body: TextField( + keyboardType: TextInputType.number, + inputFormatters: [ CaseTextInputFormatter.upper() ], + ), + ) + )); + + for (final (actual, expected) in [ + ('', ''), + ('UPPER', 'UPPER'), + ('miXEd', 'MIXED'), + ('something', 'SOMETHING'), + ]) { + testWidgets('values', (tester) async { + await tester.pumpWidget(widget); + await tester.enterText(find.byType(TextField), actual); + + expect(find.text(expected), findsOneWidget); + }); + } + }); + + group('lower case', () { + late Widget widget; + + setUp(() => widget = const MaterialApp( + home: Scaffold( + body: TextField( + keyboardType: TextInputType.number, + inputFormatters: [ CaseTextInputFormatter.lower() ], + ), + ) + )); - testWidgets('empty string', (tester) async { - await tester.pumpWidget(widget); - await tester.enterText(find.byType(TextField), '1'); - await tester.enterText(find.byType(TextField), ''); + for (final (actual, expected) in [ + ('', ''), + ('lower', 'lower'), + ('miXEd', 'mixed'), + ('SOMETHING', 'something'), + ]) { + testWidgets('values', (tester) async { + await tester.pumpWidget(widget); + await tester.enterText(find.byType(TextField), actual); - expect(find.text(''), findsOneWidget); + expect(find.text(expected), findsOneWidget); + }); + } }); }); }