diff --git a/core/lib/presentation/views/text/type_ahead_form_field_builder.dart b/core/lib/presentation/views/text/type_ahead_form_field_builder.dart new file mode 100644 index 000000000..da5a46d56 --- /dev/null +++ b/core/lib/presentation/views/text/type_ahead_form_field_builder.dart @@ -0,0 +1,110 @@ +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; + +class TypeAheadFormFieldBuilder extends StatefulWidget { + + final TextDirection textDirection; + final Duration debounceDuration; + final SuggestionsCallback suggestionsCallback; + final ItemBuilder itemBuilder; + final SuggestionSelectionCallback onSuggestionSelected; + final SuggestionsBoxDecoration suggestionsBoxDecoration; + final WidgetBuilder? noItemsFoundBuilder; + final bool hideOnEmpty; + final bool hideOnError; + final bool hideOnLoading; + final TextEditingController? controller; + final FocusNode? focusNode; + final ValueChanged? onTextChange; + final ValueChanged? onTextSubmitted; + final TextInputAction? textInputAction; + final bool autocorrect; + final List? autofillHints; + final TextInputType keyboardType; + final InputDecoration decoration; + + const TypeAheadFormFieldBuilder({ + super.key, + required this.suggestionsCallback, + required this.itemBuilder, + required this.onSuggestionSelected, + this.suggestionsBoxDecoration = const SuggestionsBoxDecoration(), + this.textDirection = TextDirection.ltr, + this.debounceDuration = const Duration(milliseconds: 300), + this.decoration = const InputDecoration(), + this.noItemsFoundBuilder, + this.hideOnEmpty = false, + this.hideOnError = false, + this.hideOnLoading = false, + this.autocorrect = false, + this.keyboardType = TextInputType.text, + this.controller, + this.focusNode, + this.autofillHints, + this.textInputAction, + this.onTextChange, + this.onTextSubmitted, + }); + + @override + State> createState() => _TypeAheadFormFieldBuilderState(); +} + +class _TypeAheadFormFieldBuilderState extends State> { + + late TextEditingController _controller; + late TextDirection _textDirection; + + @override + void initState() { + _textDirection = widget.textDirection; + _controller = widget.controller ?? TextEditingController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return TypeAheadFormField( + key: widget.key, + textFieldConfiguration: TextFieldConfiguration( + controller: widget.controller, + textInputAction: widget.textInputAction, + autocorrect: widget.autocorrect, + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + decoration: widget.decoration, + textDirection: _textDirection, + onChanged: (value) { + widget.onTextChange?.call(value); + if (value.isNotEmpty) { + final directionByText = DirectionUtils.getDirectionByEndsText(value); + if (directionByText != _textDirection) { + setState(() { + _textDirection = directionByText; + }); + } + } + }, + onSubmitted: widget.onTextSubmitted + ), + debounceDuration: widget.debounceDuration, + suggestionsCallback: widget.suggestionsCallback, + itemBuilder: widget.itemBuilder, + onSuggestionSelected: widget.onSuggestionSelected, + suggestionsBoxDecoration: widget.suggestionsBoxDecoration, + noItemsFoundBuilder: widget.noItemsFoundBuilder, + hideOnEmpty: widget.hideOnEmpty, + hideOnError: widget.hideOnError, + hideOnLoading: widget.hideOnLoading, + ); + } + + @override + void dispose() { + if (widget.controller == null) { + _controller.dispose(); + } + super.dispose(); + } +} \ No newline at end of file diff --git a/core/pubspec.lock b/core/pubspec.lock index d8896ee54..4aa93a9a8 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -194,10 +194,10 @@ packages: dependency: "direct main" description: name: flutter_keyboard_visibility - sha256: "86b71bbaffa38e885f5c21b1182408b9be6951fd125432cf6652c636254cef2d" + sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.1" flutter_keyboard_visibility_linux: dependency: transitive description: @@ -259,6 +259,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: "direct main" + description: + name: flutter_typeahead + sha256: f31211a8536f87908c3dcbdb88666e2f4d77f5f06c2b3a48eaad5599969ff32d + url: "https://pub.dev" + source: hosted + version: "4.6.0" flutter_web_plugins: dependency: transitive description: flutter diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 95047b9ab..376160483 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -73,6 +73,8 @@ dependencies: # flutter_localizations depends on intl 0.17.0 intl: 0.17.0 + flutter_typeahead: 4.6.0 + dev_dependencies: flutter_test: sdk: flutter @@ -83,6 +85,10 @@ dev_dependencies: # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec +dependency_overrides: + + flutter_keyboard_visibility: 5.4.1 + flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in diff --git a/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart index e44e81ea1..edfd81f2d 100644 --- a/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart @@ -1,5 +1,7 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; @@ -40,18 +42,16 @@ class IdentityInputWithDropListFieldBuilder extends StatelessWidget { fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)), const SizedBox(height: 8), - TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - focusNode: focusNode, - controller: editingController, - textInputAction: TextInputAction.done, - decoration: (IdentityInputDecorationBuilder() - ..setContentPadding(const EdgeInsets.symmetric( - vertical: BuildUtils.isWeb ? 16 : 12, - horizontal: 12)) - ..setErrorText(_error)) - .build() - ), + TypeAheadFormFieldBuilder( + focusNode: focusNode, + controller: editingController, + textInputAction: TextInputAction.done, + decoration: (IdentityInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric( + vertical: PlatformInfo.isWeb ? 16 : 12, + horizontal: 12)) + ..setErrorText(_error)) + .build(), debounceDuration: const Duration(milliseconds: 500), suggestionsCallback: (pattern) async { if (onChangeInputSuggestionAction != null) { diff --git a/lib/features/login/presentation/base_login_view.dart b/lib/features/login/presentation/base_login_view.dart index 24ec73b39..4907f80bc 100644 --- a/lib/features/login/presentation/base_login_view.dart +++ b/lib/features/login/presentation/base_login_view.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:get/get.dart'; @@ -106,32 +107,26 @@ abstract class BaseLoginView extends GetWidget { Widget buildUserNameInput(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 24, right: 24, left: 24), - child: TypeAheadFormField( + child: TypeAheadFormFieldBuilder( key: const Key('login_username_input'), - textFieldConfiguration: TextFieldConfiguration( - controller: loginController.usernameInputController, - onChanged: (value) => loginController.setUserNameText(value), - textInputAction: TextInputAction.next, - autocorrect: false, - autofillHints: [AutofillHints.email], - keyboardType: TextInputType.emailAddress, - decoration: (LoginInputDecorationBuilder() - ..setLabelText(AppLocalizations.of(context).email) - ..setHintText(AppLocalizations.of(context).email)) - .build(), - ), + controller: loginController.usernameInputController, + onTextChange: loginController.setUserNameText, + textInputAction: TextInputAction.next, + autocorrect: false, + autofillHints: const [AutofillHints.email], + keyboardType: TextInputType.emailAddress, + decoration: (LoginInputDecorationBuilder() + ..setLabelText(AppLocalizations.of(context).email) + ..setHintText(AppLocalizations.of(context).email)) + .build(), debounceDuration: const Duration(milliseconds: 300), - suggestionsCallback: (pattern) async { - return await loginController.getAllRecentLoginUsernameAction(pattern); - }, - itemBuilder: (context, loginUsername) => - RecentItemTileWidget(loginUsername, imagePath: imagePaths), + suggestionsCallback: loginController.getAllRecentLoginUsernameAction, + itemBuilder: (context, loginUsername) => RecentItemTileWidget(loginUsername, imagePath: imagePaths), onSuggestionSelected: (recentUsername) { loginController.setUsername(recentUsername.username); passFocusNode.requestFocus(); }, - suggestionsBoxDecoration: const SuggestionsBoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(14))), + suggestionsBoxDecoration: const SuggestionsBoxDecoration(borderRadius: BorderRadius.all(Radius.circular(14))), noItemsFoundBuilder: (context) => const SizedBox(), hideOnEmpty: true, hideOnError: true, diff --git a/lib/features/login/presentation/login_view.dart b/lib/features/login/presentation/login_view.dart index f0a62471a..1d53987de 100644 --- a/lib/features/login/presentation/login_view.dart +++ b/lib/features/login/presentation/login_view.dart @@ -1,4 +1,5 @@ import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; @@ -135,27 +136,23 @@ class LoginView extends BaseLoginView { Widget _buildUrlInput(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 24, left: 24, bottom: 24), - child: TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - controller: loginController.urlInputController, - textInputAction: TextInputAction.next, - keyboardType: TextInputType.url, - onSubmitted: (value) => controller.handleNextInUrlInputFormPress(), - decoration: (LoginInputDecorationBuilder() - ..setLabelText(AppLocalizations.of(context).prefix_https) - ..setPrefixText(AppLocalizations.of(context).prefix_https)) - .build() - ), + child: TypeAheadFormFieldBuilder( + controller: loginController.urlInputController, + textInputAction: TextInputAction.next, + keyboardType: TextInputType.url, + onTextSubmitted: (value) => controller.handleNextInUrlInputFormPress(), + decoration: (LoginInputDecorationBuilder() + ..setLabelText(AppLocalizations.of(context).prefix_https) + ..setPrefixText(AppLocalizations.of(context).prefix_https)) + .build(), debounceDuration: const Duration(milliseconds: 300), suggestionsCallback: (pattern) async { loginController.formatUrl(pattern); return loginController.getAllRecentLoginUrlAction(pattern); }, - itemBuilder: (context, loginUrl) => - RecentItemTileWidget(loginUrl, imagePath: imagePaths), + itemBuilder: (context, loginUrl) => RecentItemTileWidget(loginUrl, imagePath: imagePaths), onSuggestionSelected: (loginUrl) => controller.formatUrl(loginUrl.url), - suggestionsBoxDecoration: const SuggestionsBoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(14))), + suggestionsBoxDecoration: const SuggestionsBoxDecoration(borderRadius: BorderRadius.all(Radius.circular(14))), noItemsFoundBuilder: (context) => const SizedBox(), hideOnEmpty: true, hideOnError: true,