TF-4011 Support arrow keys on keyboard to get again focus after deleting a recipient

This commit is contained in:
dab246
2025-09-11 16:43:23 +07:00
committed by Dat H. Pham
parent 9abde49c62
commit cf40d22cc8
8 changed files with 51 additions and 55 deletions
@@ -83,7 +83,7 @@ class _DefaultAutocompleteInputFieldWidgetState
extends State<DefaultAutocompleteInputFieldWidget> { extends State<DefaultAutocompleteInputFieldWidget> {
final _imagePaths = Get.find<ImagePaths>(); final _imagePaths = Get.find<ImagePaths>();
bool _lastTagFocused = false; int _tagIndexFocused = -1;
bool _isDragging = false; bool _isDragging = false;
late List<EmailAddress> _currentListEmailAddress; late List<EmailAddress> _currentListEmailAddress;
Timer? _gapBetweenTagChangedAndFindSuggestion; Timer? _gapBetweenTagChangedAndFindSuggestion;
@@ -157,17 +157,14 @@ class _DefaultAutocompleteInputFieldWidgetState
onSubmitted: _handleSubmitTagAction, onSubmitted: _handleSubmitTagAction,
tagBuilder: (context, index) { tagBuilder: (context, index) {
final currentEmailAddress = _currentListEmailAddress.elementAt(index); final currentEmailAddress = _currentListEmailAddress.elementAt(index);
final isLatestEmail =
currentEmailAddress == _currentListEmailAddress.last;
return DefaultAutocompleteTagItemWidget( return DefaultAutocompleteTagItemWidget(
field: widget.field, field: widget.field,
currentEmailAddress: currentEmailAddress, currentEmailAddress: currentEmailAddress,
currentListEmailAddress: _currentListEmailAddress, currentListEmailAddress: _currentListEmailAddress,
collapsedListEmailAddress: _collapsedListEmailAddress, collapsedListEmailAddress: _collapsedListEmailAddress,
iconClose: _imagePaths.icClose, iconClose: _imagePaths.icClose,
isLatestEmail: isLatestEmail,
isCollapsed: _isCollapse, isCollapsed: _isCollapse,
isLatestTagFocused: _lastTagFocused, isTagFocused: _tagIndexFocused == index,
onDeleteTagAction: _handleDeleteTagAction, onDeleteTagAction: _handleDeleteTagAction,
onShowFullAction: widget.onShowFullListEmailAddressAction, onShowFullAction: widget.onShowFullListEmailAddressAction,
); );
@@ -335,13 +332,17 @@ class _DefaultAutocompleteInputFieldWidgetState
); );
} }
void _handleFocusTagAction(bool focused) { void _handleFocusTagAction(int index) {
_setStateSafety(() => _lastTagFocused = focused); _setStateSafety(() => _tagIndexFocused = index);
} }
void _handleDeleteLatestTagAction() { void _handleDeleteLatestTagAction(int index) {
if (_currentListEmailAddress.isNotEmpty) { if (_currentListEmailAddress.isNotEmpty &&
_setStateSafety(_currentListEmailAddress.removeLast); index >= 0 &&
index < _currentListEmailAddress.length) {
_setStateSafety(() {
_currentListEmailAddress.removeAt(index);
});
_updateListEmailAddressAction(); _updateListEmailAddressAction();
} }
} }
@@ -16,8 +16,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/ad
class DefaultAutocompleteTagItemWidget extends StatelessWidget { class DefaultAutocompleteTagItemWidget extends StatelessWidget {
final bool isCollapsed; final bool isCollapsed;
final bool isLatestTagFocused; final bool isTagFocused;
final bool isLatestEmail;
final FilterField field; final FilterField field;
final EmailAddress currentEmailAddress; final EmailAddress currentEmailAddress;
final String iconClose; final String iconClose;
@@ -34,8 +33,7 @@ class DefaultAutocompleteTagItemWidget extends StatelessWidget {
required this.collapsedListEmailAddress, required this.collapsedListEmailAddress,
required this.iconClose, required this.iconClose,
this.isCollapsed = false, this.isCollapsed = false,
this.isLatestTagFocused = false, this.isTagFocused = false,
this.isLatestEmail = false,
this.onDeleteTagAction, this.onDeleteTagAction,
this.onShowFullAction, this.onShowFullAction,
}) : super(key: key); }) : super(key: key);
@@ -141,7 +139,7 @@ class DefaultAutocompleteTagItemWidget extends StatelessWidget {
} }
Color _getTagBackgroundColor() { Color _getTagBackgroundColor() {
if (isLatestTagFocused && isLatestEmail) { if (isTagFocused) {
return AppColor.colorItemRecipientSelected; return AppColor.colorItemRecipientSelected;
} else { } else {
return AppColor.colorEmailAddressTag; return AppColor.colorEmailAddressTag;
@@ -149,7 +147,7 @@ class DefaultAutocompleteTagItemWidget extends StatelessWidget {
} }
BorderSide _getTagBorderSide() { BorderSide _getTagBorderSide() {
if (isLatestTagFocused && isLatestEmail) { if (isTagFocused) {
return const BorderSide(width: 1, color: AppColor.primaryColor); return const BorderSide(width: 1, color: AppColor.primaryColor);
} else { } else {
return const BorderSide(width: 0, color: AppColor.colorEmailAddressTag); return const BorderSide(width: 0, color: AppColor.colorEmailAddressTag);
@@ -126,7 +126,7 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
final _responsiveUtils = Get.find<ResponsiveUtils>(); final _responsiveUtils = Get.find<ResponsiveUtils>();
Timer? _gapBetweenTagChangedAndFindSuggestion; Timer? _gapBetweenTagChangedAndFindSuggestion;
bool _lastTagFocused = false; int _tagIndexFocused = -1;
bool _isDragging = false; bool _isDragging = false;
late List<EmailAddress> _currentListEmailAddress; late List<EmailAddress> _currentListEmailAddress;
@@ -204,8 +204,8 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
suggestionItemHeight: RecipientComposerWidgetStyle.suggestionBoxItemHeight, suggestionItemHeight: RecipientComposerWidgetStyle.suggestionBoxItemHeight,
suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth),
textStyle: RecipientComposerWidgetStyle.inputTextStyle, textStyle: RecipientComposerWidgetStyle.inputTextStyle,
onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), onFocusTagAction: (index) => _handleFocusTagAction.call(index, stateSetter),
onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), onDeleteTagAction: (index) => _handleDeleteLatestTagAction.call(index, stateSetter),
onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter),
onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter),
onTapOutside: (_) {}, onTapOutside: (_) {},
@@ -217,7 +217,6 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
inputDecoration: const InputDecoration(border: InputBorder.none), inputDecoration: const InputDecoration(border: InputBorder.none),
tagBuilder: (context, index) { tagBuilder: (context, index) {
final currentEmailAddress = _currentListEmailAddress[index]; final currentEmailAddress = _currentListEmailAddress[index];
final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last;
return RecipientTagItemWidget( return RecipientTagItemWidget(
index: index, index: index,
@@ -227,9 +226,8 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
currentEmailAddress: currentEmailAddress, currentEmailAddress: currentEmailAddress,
currentListEmailAddress: _currentListEmailAddress, currentListEmailAddress: _currentListEmailAddress,
collapsedListEmailAddress: _collapsedListEmailAddress, collapsedListEmailAddress: _collapsedListEmailAddress,
isLatestEmail: isLatestEmail,
isCollapsed: _isCollapse, isCollapsed: _isCollapse,
isLatestTagFocused: _lastTagFocused, isTagFocused: _tagIndexFocused == index,
maxWidth: widget.maxWidth, maxWidth: widget.maxWidth,
isMobile: _responsiveUtils.isMobile(context), isMobile: _responsiveUtils.isMobile(context),
onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter),
@@ -299,8 +297,8 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
suggestionItemHeight: RecipientComposerWidgetStyle.suggestionBoxItemHeight, suggestionItemHeight: RecipientComposerWidgetStyle.suggestionBoxItemHeight,
suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth),
textStyle: RecipientComposerWidgetStyle.inputTextStyle, textStyle: RecipientComposerWidgetStyle.inputTextStyle,
onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), onFocusTagAction: (index) => _handleFocusTagAction.call(index, stateSetter),
onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), onDeleteTagAction: (index) => _handleDeleteLatestTagAction.call(index, stateSetter),
onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter),
onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter),
onTapOutside: (_) {}, onTapOutside: (_) {},
@@ -312,7 +310,6 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
inputDecoration: const InputDecoration(border: InputBorder.none), inputDecoration: const InputDecoration(border: InputBorder.none),
tagBuilder: (context, index) { tagBuilder: (context, index) {
final currentEmailAddress = _currentListEmailAddress[index]; final currentEmailAddress = _currentListEmailAddress[index];
final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last;
return RecipientTagItemWidget( return RecipientTagItemWidget(
index: index, index: index,
@@ -322,9 +319,8 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
currentEmailAddress: currentEmailAddress, currentEmailAddress: currentEmailAddress,
currentListEmailAddress: _currentListEmailAddress, currentListEmailAddress: _currentListEmailAddress,
collapsedListEmailAddress: _collapsedListEmailAddress, collapsedListEmailAddress: _collapsedListEmailAddress,
isLatestEmail: isLatestEmail,
isCollapsed: _isCollapse, isCollapsed: _isCollapse,
isLatestTagFocused: _lastTagFocused, isTagFocused: _tagIndexFocused == index,
maxWidth: widget.maxWidth, maxWidth: widget.maxWidth,
isMobile: _responsiveUtils.isMobile(context), isMobile: _responsiveUtils.isMobile(context),
onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter),
@@ -525,13 +521,17 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
); );
} }
void _handleFocusTagAction(bool focused, StateSetter stateSetter) { void _handleFocusTagAction(int index, StateSetter stateSetter) {
stateSetter(() => _lastTagFocused = focused); stateSetter(() => _tagIndexFocused = index);
} }
void _handleDeleteLatestTagAction(StateSetter stateSetter) { void _handleDeleteLatestTagAction(int index, StateSetter stateSetter) {
if (_currentListEmailAddress.isNotEmpty) { if (_currentListEmailAddress.isNotEmpty &&
stateSetter(_currentListEmailAddress.removeLast); index >= 0 &&
index < _currentListEmailAddress.length) {
stateSetter(() {
_currentListEmailAddress.removeAt(index);
});
_updateListEmailAddressAction(); _updateListEmailAddressAction();
} }
} }
@@ -23,8 +23,7 @@ import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart
class RecipientTagItemWidget extends StatelessWidget { class RecipientTagItemWidget extends StatelessWidget {
final bool isCollapsed; final bool isCollapsed;
final bool isLatestTagFocused; final bool isTagFocused;
final bool isLatestEmail;
final ImagePaths imagePaths; final ImagePaths imagePaths;
final double? maxWidth; final double? maxWidth;
final int index; final int index;
@@ -50,8 +49,7 @@ class RecipientTagItemWidget extends StatelessWidget {
required this.imagePaths, required this.imagePaths,
@visibleForTesting this.isTestingForWeb = false, @visibleForTesting this.isTestingForWeb = false,
this.isCollapsed = false, this.isCollapsed = false,
this.isLatestTagFocused = false, this.isTagFocused = false,
this.isLatestEmail = false,
this.isMobile = false, this.isMobile = false,
this.onShowFullAction, this.onShowFullAction,
this.onDeleteTagAction, this.onDeleteTagAction,
@@ -195,7 +193,7 @@ class RecipientTagItemWidget extends StatelessWidget {
int get countRecipients => currentListEmailAddress.length - collapsedListEmailAddress.length; int get countRecipients => currentListEmailAddress.length - collapsedListEmailAddress.length;
Color _getTagBackgroundColor() { Color _getTagBackgroundColor() {
if (isLatestTagFocused && isLatestEmail) { if (isTagFocused) {
return AppColor.colorItemRecipientSelected; return AppColor.colorItemRecipientSelected;
} else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) { } else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) {
return AppColor.grayBackgroundColor; return AppColor.grayBackgroundColor;
@@ -205,7 +203,7 @@ class RecipientTagItemWidget extends StatelessWidget {
} }
BorderSide _getTagBorderSide() { BorderSide _getTagBorderSide() {
if (isLatestTagFocused && isLatestEmail) { if (isTagFocused) {
return const BorderSide(width: 1, color: AppColor.primaryColor); return const BorderSide(width: 1, color: AppColor.primaryColor);
} else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) { } else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) {
return const BorderSide(width: 1, color: AppColor.colorEmailAddressTag); return const BorderSide(width: 1, color: AppColor.colorEmailAddressTag);
@@ -18,16 +18,14 @@ typedef DeleteContactCallbackAction = Function(EmailAddress contactDeleted);
class ContactInputTagItem extends StatelessWidget { class ContactInputTagItem extends StatelessWidget {
final EmailAddress contact; final EmailAddress contact;
final bool isLastContact; final bool isTagFocused;
final bool lastTagFocused;
final DeleteContactCallbackAction? deleteContactCallbackAction; final DeleteContactCallbackAction? deleteContactCallbackAction;
const ContactInputTagItem( const ContactInputTagItem(
this.contact, this.contact,
{ {
Key? key, Key? key,
this.isLastContact = false, this.isTagFocused = false,
this.lastTagFocused = false,
this.deleteContactCallbackAction this.deleteContactCallbackAction
} }
) : super(key: key); ) : super(key: key);
@@ -97,7 +95,7 @@ class ContactInputTagItem extends StatelessWidget {
bool _isValidEmailAddress(String value) => value.isEmail || AppUtils.isEmailLocalhost(value); bool _isValidEmailAddress(String value) => value.isEmail || AppUtils.isEmailLocalhost(value);
Color _getTagBackgroundColor() { Color _getTagBackgroundColor() {
if (lastTagFocused && isLastContact) { if (isTagFocused) {
return AppColor.colorItemRecipientSelected; return AppColor.colorItemRecipientSelected;
} else { } else {
return _isValidEmailAddress(contact.emailAddress) return _isValidEmailAddress(contact.emailAddress)
@@ -107,7 +105,7 @@ class ContactInputTagItem extends StatelessWidget {
} }
BorderSide _getTagBorderSide() { BorderSide _getTagBorderSide() {
if (lastTagFocused && isLastContact) { if (isTagFocused) {
return const BorderSide( return const BorderSide(
width: 1, width: 1,
color: AppColor.primaryColor); color: AppColor.primaryColor);
@@ -67,7 +67,7 @@ class _AutocompleteContactTextFieldWithTagsState extends State<AutocompleteConta
late List<EmailAddress> listEmailAddress; late List<EmailAddress> listEmailAddress;
bool lastTagFocused = false; int _tagIndexFocused = -1;
@override @override
void initState() { void initState() {
@@ -107,15 +107,17 @@ class _AutocompleteContactTextFieldWithTagsState extends State<AutocompleteConta
suggestionsBoxBackgroundColor: Colors.white, suggestionsBoxBackgroundColor: Colors.white,
suggestionsBoxRadius: 16, suggestionsBoxRadius: 16,
suggestionsBoxMaxHeight: 350, suggestionsBoxMaxHeight: 350,
onFocusTagAction: (focused) { onFocusTagAction: (index) {
setState(() { setState(() {
lastTagFocused = focused; _tagIndexFocused = index;
}); });
}, },
onDeleteTagAction: () { onDeleteTagAction: (index) {
if (listEmailAddress.isNotEmpty) { if (listEmailAddress.isNotEmpty &&
index >= 0 &&
index < listEmailAddress.length) {
setState(() { setState(() {
listEmailAddress.removeLast(); listEmailAddress.removeAt(index);
}); });
} }
}, },
@@ -136,8 +138,7 @@ class _AutocompleteContactTextFieldWithTagsState extends State<AutocompleteConta
), ),
tagBuilder: (context, index) => ContactInputTagItem( tagBuilder: (context, index) => ContactInputTagItem(
listEmailAddress[index], listEmailAddress[index],
isLastContact: index == listEmailAddress.length - 1, isTagFocused: _tagIndexFocused == index,
lastTagFocused: lastTagFocused,
deleteContactCallbackAction: (contact) { deleteContactCallbackAction: (contact) {
setState(() => listEmailAddress.remove(contact)); setState(() => listEmailAddress.remove(contact));
} }
+2 -2
View File
@@ -2083,10 +2083,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: super_tag_editor name: super_tag_editor
sha256: "291b4b5ef5c44d1d06dc0337f66b71f7afd77b8c98d0ad0f4f8baca6a6f3cbc4" sha256: fbf81653bc01a89e6fe63b9ae01cc12d557020cf74c870ed49e8e406ad710aac
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.2" version: "0.3.3"
syncfusion_flutter_core: syncfusion_flutter_core:
dependency: transitive dependency: transitive
description: description:
+1 -1
View File
@@ -110,7 +110,7 @@ dependencies:
ref: master ref: master
### Dependencies from pub.dev ### ### Dependencies from pub.dev ###
super_tag_editor: 0.3.2 super_tag_editor: 0.3.3
cupertino_icons: 1.0.6 cupertino_icons: 1.0.6