Skip to content

Commit

Permalink
Add CaseTextInputFormatter
Browse files Browse the repository at this point in the history
  • Loading branch information
Pante committed Oct 30, 2023
1 parent 3c0ecaa commit 1cc24dd
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 45 deletions.
3 changes: 2 additions & 1 deletion stevia/lib/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
/// General-purpose widgets and Flutter services.
///
/// * [ColorFilters]
/// * [IntTextInputFormatter]
///
/// * [CaseTextInputFormatter]
/// * [IntTextInputFormatter]
///
/// ## Timer
/// Controllers that simply the implementation of timers.
Expand Down
53 changes: 45 additions & 8 deletions stevia/lib/src/services/text_input_formatters.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<int> _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;
}

Expand All @@ -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;
}

}
26 changes: 21 additions & 5 deletions stevia/lib/src/widgets/positioned/positioned_route.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import 'package:flutter/cupertino.dart';
import 'package:meta/meta.dart';

/// TODO
class PositionedRoute extends PopupRoute<void> {

/// Used build the route's primary contents.
///
/// See [ModalRoute.buildPage] for complete definition of the parameters.
final RoutePageBuilder builder;
@override
final Color? barrierColor;
@override
Expand All @@ -14,16 +18,28 @@ class PositionedRoute extends PopupRoute<void> {

/// 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<double> animation, Animation<double> secondaryAnimation, Widget child) {
// return
// }

@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => builder(context, animation, secondaryAnimation);

@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
// TODO: implement buildPage
throw UnimplementedError();
}
Animation<double> 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,
);

}
198 changes: 167 additions & 31 deletions stevia/test/src/services/text_input_formatters_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
});
});
}

0 comments on commit 1cc24dd

Please sign in to comment.