Commit 6b5518b

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2023-11-24 17:35:24
implement number input dialog with new input dialoge
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent 2b607ff
Changed files (3)
lib
components
test
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);
     });
   });