Commit 6b5518b
Changed files (3)
lib
components
dialoges
settings
test
ui
components
lib/components/dialoges/input_dialoge.dart
@@ -8,10 +8,11 @@ class InputDialoge extends StatefulWidget {
///
/// Pops the context after value submission with object of type [String?].
const InputDialoge({super.key,
+ this.hintText,
+ this.initialValue,
this.inputFormatters,
this.keyboardType,
- this.hintText,
- this.initialValue});
+ this.validator,});
/// Initial content of the input field.
final String? initialValue;
@@ -24,6 +25,16 @@ class InputDialoge extends StatefulWidget {
final TextInputType? keyboardType;
+ /// Validation function called after submit.
+ ///
+ /// When the validator returns null the dialoge completes normally,
+ /// in case of receiving a String it will be displayed to the user
+ /// and pressing of the submit button will be ignored.
+ ///
+ /// It is still possible to cancel a dialoge in case the validator fails.
+ /// TODO: test
+ final String? Function(String)? validator;
+
@override
State<InputDialoge> createState() => _InputDialogeState();
}
@@ -32,6 +43,8 @@ class _InputDialogeState extends State<InputDialoge> {
final controller = TextEditingController();
final focusNode = FocusNode();
+ String? errorText;
+
@override
void initState() {
super.initState();
@@ -57,7 +70,8 @@ class _InputDialogeState extends State<InputDialoge> {
keyboardType: widget.keyboardType,
decoration: InputDecoration(
hintText: widget.hintText,
- labelText: widget.hintText
+ labelText: widget.hintText,
+ errorText: errorText
),
onSubmitted: _onSubmit,
),
@@ -73,11 +87,44 @@ class _InputDialogeState extends State<InputDialoge> {
}
void _onSubmit(String value) {
+ final validationResult = widget.validator?.call(value);
+ if (validationResult != null) {
+ setState(() {
+ errorText = validationResult;
+ });
+ return;
+ }
Navigator.of(context).pop(value);
}
}
/// Creates a dialoge for prompting a single user input.
+///
+/// Add supporting text describing the input field through [hintText].
+/// [initialValue] specifies the initial input field content.
Future<String?> showInputDialoge(BuildContext context, {String? hintText, String? initialValue}) async =>
showDialog<String?>(context: context, builder: (context) =>
- InputDialoge(hintText: hintText, initialValue: initialValue,));
\ No newline at end of file
+ InputDialoge(hintText: hintText, initialValue: initialValue,));
+
+Future<double?> showNumberInputDialoge(BuildContext context, {String? hintText, num? initialValue}) async {
+ final result = await showDialog<String?>(context: context, builder: (context) =>
+ InputDialoge(
+ hintText: hintText,
+ initialValue: initialValue?.toString(),
+ inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'([0-9]+(\.([0-9]*))?)')),],
+ keyboardType: TextInputType.number,
+ validator: (text) {
+ double? value = double.tryParse(text);
+ value ??= int.tryParse(text)?.toDouble();
+ if (text.isEmpty || value == null) {
+ return AppLocalizations.of(context)!.errNaN;
+ }
+ return null;
+ },
+ ));
+
+ if (result == null) return null;
+ double? value = double.tryParse(result);
+ value ??= int.tryParse(result)?.toDouble();
+ return value;
+}
\ No newline at end of file
lib/components/settings/input_list_tile.dart
@@ -1,4 +1,4 @@
-import 'package:blood_pressure_app/components/dialoges/oldinput_dialoge.dart';
+import 'package:blood_pressure_app/components/dialoges/input_dialoge.dart';
import 'package:flutter/material.dart';
/// A list tile for exposing editable strings.
@@ -26,18 +26,9 @@ class InputListTile extends StatelessWidget {
title: Text(label),
subtitle: Text(value),
trailing: const Icon(Icons.edit),
- onTap: () {
- showDialog(
- context: context,
- builder: (context) => InputDialoge(
- initialValue: value,
- hintText: label,
- onSubmit: (value) {
- Navigator.of(context).pop();
- onSubmit(value);
- },
- ),
- );
+ onTap: () async {
+ final input = await showInputDialoge(context, initialValue: value, hintText: label);
+ if (input != null) onSubmit(input);
},
);
}
test/ui/components/input_dialoge_test.dart
@@ -97,6 +97,80 @@ void main() {
await widgetTester.tap(find.text(localizations.btnCancel));
await widgetTester.pumpAndSettle();
+ expect(result, null);
+ });
+ });
+ group('showNumberInputDialoge', () {
+ testWidgets('should start with input focused', (widgetTester) async {
+ await widgetTester.pumpWidget(MaterialApp(
+ localizationsDelegates: const [AppLocalizations.delegate,], locale: const Locale('en'),
+ home: Builder(builder: (BuildContext context) => TextButton(onPressed:
+ () => showNumberInputDialoge(context, initialValue: 123), child: const Text('X')))
+ ));
+ await widgetTester.tap(find.text('X'));
+ await widgetTester.pumpAndSettle();
+
+ expect(find.byType(InputDialoge), findsOneWidget);
+ expect(find.text('123'), findsOneWidget);
+
+ final primaryFocus = FocusManager.instance.primaryFocus;
+ expect(primaryFocus?.context?.widget, isNotNull);
+ final focusedTextField = find.ancestor(
+ of: find.byWidget(primaryFocus!.context!.widget),
+ matching: find.byType(TextField),
+ );
+ expect(find.descendant(of: focusedTextField, matching: find.text('123')), findsOneWidget);
+ });
+ testWidgets('should allow entering a number', (widgetTester) async {
+ double? result = -1;
+ await widgetTester.pumpWidget(MaterialApp(
+ localizationsDelegates: const [AppLocalizations.delegate,], locale: const Locale('en'),
+ home: Builder(builder: (BuildContext context) => TextButton(onPressed:
+ () async {
+ result = await showNumberInputDialoge(context);
+ }, child: const Text('X')))
+ ));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+ await widgetTester.tap(find.text('X'));
+ await widgetTester.pumpAndSettle();
+
+ expect(find.byType(InputDialoge), findsOneWidget);
+ expect(find.byType(TextField), findsOneWidget);
+
+ await widgetTester.enterText(find.byType(TextField), '123.76');
+ expect(find.text(localizations.btnConfirm), findsOneWidget);
+ await widgetTester.tap(find.text(localizations.btnConfirm));
+ await widgetTester.pumpAndSettle();
+
+ expect(result, 123.76);
+ });
+ testWidgets('should not allow entering text', (widgetTester) async {
+ double? result = -1;
+ await widgetTester.pumpWidget(MaterialApp(
+ localizationsDelegates: const [AppLocalizations.delegate,], locale: const Locale('en'),
+ home: Builder(builder: (BuildContext context) => TextButton(onPressed:
+ () async {
+ result = await showNumberInputDialoge(context);
+ }, child: const Text('X')))
+ ));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+ await widgetTester.tap(find.text('X'));
+ await widgetTester.pumpAndSettle();
+
+ expect(find.byType(InputDialoge), findsOneWidget);
+
+ await widgetTester.enterText(find.byType(TextField), 'test');
+ expect(find.text(localizations.btnConfirm), findsOneWidget);
+ await widgetTester.tap(find.text(localizations.btnConfirm));
+ await widgetTester.pumpAndSettle();
+
+ expect(find.byType(InputDialoge), findsOneWidget); // unclosable through confirm
+ expect(find.text(localizations.errNaN), findsOneWidget);
+
+ expect(find.text(localizations.btnCancel), findsOneWidget);
+ await widgetTester.tap(find.text(localizations.btnCancel));
+ await widgetTester.pumpAndSettle();
+
expect(result, null);
});
});