Skip to content

Commit

Permalink
Add IntTextInputFormatter
Browse files Browse the repository at this point in the history
  • Loading branch information
Pante committed Oct 24, 2023
1 parent 995fff0 commit 90cfaec
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 1 deletion.
54 changes: 54 additions & 0 deletions stevia/lib/src/widgets/foundation/text_input_formatters.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sugar/sugar.dart';
import 'dart:math' as math;

/// A [IntTextInputFormatter] validates whether the text being edited is an integer in a given [Range].
///
/// 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`.
///
///
/// 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
/// );
/// ```
class IntTextInputFormatter extends TextInputFormatter {
final Range<int> _range;

/// Creates a [IntTextInputFormatter] in the given [Range].
IntTextInputFormatter(this._range): super();

@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
if (newValue.text.isEmpty || newValue.text == '-') {
return newValue;
}

final trimmed = newValue.text.trim().replaceAll(',', '');
return switch (int.tryParse(trimmed)) {
final value? when _range.contains(value) => switch (newValue.text.length == trimmed.length) {
true => newValue,
false => TextEditingValue(
text: trimmed,
selection: newValue.selection.copyWith(
baseOffset: math.min(newValue.selection.start, trimmed.length),
extentOffset: math.min(newValue.selection.end, trimmed.length),
),
composing: !newValue.composing.isCollapsed && trimmed.length > newValue.composing.start ? TextRange(
start: newValue.composing.start,
end: math.min(newValue.composing.end, trimmed.length),
): TextRange.empty,
)
},
_ => oldValue,
};
}
}
4 changes: 3 additions & 1 deletion stevia/lib/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
/// * [StreamValueBuilder]
///
/// ## Foundation
/// General-purpose widgets.
/// General-purpose widgets and Flutter services.
///
/// * [ColorFilters]
/// * [IntTextInputFormatter]
///
/// ## Resizable
/// Widgets that contain children which can be resized either horizontally or vertically.
Expand All @@ -38,6 +39,7 @@ export 'src/widgets/async/future/future_builder.dart' show
export 'src/widgets/async/stream_value_builder.dart';

export 'src/widgets/foundation/color_filters.dart' hide Matrix5;
export 'src/widgets/foundation/text_input_formatters.dart';

export 'src/widgets/resizable/resizable_box.dart';
export 'src/widgets/resizable/resizable_icon.dart';
Expand Down
47 changes: 47 additions & 0 deletions stevia/test/src/widgets/foundation/text_input_formatters_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart' hide Interval;
import 'package:flutter_test/flutter_test.dart';

import 'package:stevia/stevia.dart';
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 {
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), '1');
await tester.enterText(find.byType(TextField), '');

expect(find.text(''), findsOneWidget);
});
});
}

0 comments on commit 90cfaec

Please sign in to comment.