From 371ff6c3353f68c5e49af34376eb1e119c75f990 Mon Sep 17 00:00:00 2001 From: Jacques Date: Thu, 27 Feb 2025 17:25:20 +0100 Subject: [PATCH] feat: add semantics for buttons --- CHANGELOG.md | 1 + .../lib/src/config/chat_semantics.dart | 119 +++++++++++ .../lib/src/flutter_chat_entry_widget.dart | 98 +++++----- .../chat_detail/chat_detail_screen.dart | 41 ++-- .../chat_detail/widgets/chat_bottom.dart | 33 ++-- .../widgets/default_message_builder.dart | 2 +- .../lib/src/screens/chat_profile_screen.dart | 119 ++++++----- .../lib/src/screens/chat_screen.dart | 108 +++++----- .../src/screens/creation/new_chat_screen.dart | 38 ++-- .../creation/new_group_chat_overview.dart | 184 ++++++++++-------- .../creation/new_group_chat_screen.dart | 29 +-- .../widgets/default_image_picker.dart | 18 +- .../screens/creation/widgets/search_icon.dart | 17 +- .../screens/creation/widgets/user_list.dart | 135 ++++++------- packages/flutter_chat/pubspec.yaml | 3 + 15 files changed, 579 insertions(+), 366 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ec2d57..61e8b74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Added enabled boolean to the messageInputBuilder and made parameters named - Added autoScrollTriggerOffset to the ChatPaginationControls to adjust when the auto scroll should be enabled - Added the ability to set the color of the CircularProgressIndicator of the ImageLoadingSnackbar by theme.snackBarTheme.actionTextColor +- Added semantics for variable text, buttons and textfields ## 4.0.0 - Move to the new user story architecture diff --git a/packages/flutter_chat/lib/src/config/chat_semantics.dart b/packages/flutter_chat/lib/src/config/chat_semantics.dart index bb8bb4e..4dc8529 100644 --- a/packages/flutter_chat/lib/src/config/chat_semantics.dart +++ b/packages/flutter_chat/lib/src/config/chat_semantics.dart @@ -33,6 +33,25 @@ class ChatSemantics { required this.newChatBioInput, required this.newChatSearchInput, required this.newGroupChatSearchInput, + required this.profileStartChatButton, + required this.chatsStartChatButton, + required this.chatsDeleteConfirmButton, + required this.newChatCreateGroupChatButton, + required this.newGroupChatCreateGroupChatButton, + required this.newGroupChatNextButton, + required this.imagePickerCancelButton, + required this.chatSelectImageIconButton, + required this.chatSendMessageIconButton, + required this.newChatSearchIconButton, + required this.newGroupChatSearchIconButton, + required this.chatBackButton, + required this.chatTitleButton, + required this.newGroupChatSelectImage, + required this.newGroupChatRemoveImage, + required this.newGroupChatRemoveUser, + required this.profileTapUserButton, + required this.chatsOpenChatButton, + required this.userListTapUser, }); /// Default translations for the chat component view @@ -58,6 +77,25 @@ class ChatSemantics { this.newChatBioInput = "input_text_bio", this.newChatSearchInput = "input_text_search", this.newGroupChatSearchInput = "input_text_search", + this.profileStartChatButton = "button_start_chat", + this.chatsStartChatButton = "button_start_chat", + this.chatsDeleteConfirmButton = "button_delete_chat_confirm", + this.newChatCreateGroupChatButton = "button_create_group_chat", + this.newGroupChatCreateGroupChatButton = "button_create_group_chat", + this.newGroupChatNextButton = "button_next", + this.imagePickerCancelButton = "button_cancel", + this.chatSelectImageIconButton = "button_icon_select_image", + this.chatSendMessageIconButton = "button_icon_send_message", + this.newChatSearchIconButton = "button_icon_search", + this.newGroupChatSearchIconButton = "button_icon_search", + this.chatBackButton = "button_back", + this.chatTitleButton = "button_open_profile", + this.newGroupChatSelectImage = "button_select_image", + this.newGroupChatRemoveImage = "button_remove_image", + this.newGroupChatRemoveUser = "button_remove_user", + this.profileTapUserButton = _defaultProfileTapUserButton, + this.chatsOpenChatButton = _defaultChatsOpenChatButton, + this.userListTapUser = _defaultUserListTapUser, }); // Text @@ -87,6 +125,33 @@ class ChatSemantics { final String newChatSearchInput; final String newGroupChatSearchInput; + // Buttons + final String profileStartChatButton; + final String chatsStartChatButton; + final String chatsDeleteConfirmButton; + final String newChatCreateGroupChatButton; + final String newGroupChatCreateGroupChatButton; + final String newGroupChatNextButton; + final String imagePickerCancelButton; + + // Icon buttons + final String chatSelectImageIconButton; + final String chatSendMessageIconButton; + final String newChatSearchIconButton; + final String newGroupChatSearchIconButton; + + // Inkwells + final String chatBackButton; + final String chatTitleButton; + final String newGroupChatSelectImage; + final String newGroupChatRemoveImage; + final String newGroupChatRemoveUser; + + // Indexed inkwells + final String Function(int index) profileTapUserButton; + final String Function(int index) chatsOpenChatButton; + final String Function(int index) userListTapUser; + ChatSemantics copyWith({ String? profileTitle, String? profileDescription, @@ -109,6 +174,25 @@ class ChatSemantics { String? newChatBioInput, String? newChatSearchInput, String? newGroupChatSearchInput, + String? profileStartChatButton, + String? chatsStartChatButton, + String? chatsDeleteConfirmButton, + String? newChatCreateGroupChatButton, + String? newGroupChatCreateGroupChatButton, + String? newGroupChatNextButton, + String? imagePickerCancelButton, + String? chatSelectImageIconButton, + String? chatSendMessageIconButton, + String? newChatSearchIconButton, + String? newGroupChatSearchIconButton, + String? chatBackButton, + String? chatTitleButton, + String? newGroupChatSelectImage, + String? newGroupChatRemoveImage, + String? newGroupChatRemoveUser, + String Function(int)? profileTapUserButton, + String Function(int)? chatsOpenChatButton, + String Function(int)? userListTapUser, }) => ChatSemantics( profileTitle: profileTitle ?? this.profileTitle, @@ -137,6 +221,38 @@ class ChatSemantics { newChatSearchInput: newChatSearchInput ?? this.newChatSearchInput, newGroupChatSearchInput: newGroupChatSearchInput ?? this.newGroupChatSearchInput, + profileStartChatButton: + profileStartChatButton ?? this.profileStartChatButton, + chatsStartChatButton: chatsStartChatButton ?? this.chatsStartChatButton, + chatsDeleteConfirmButton: + chatsDeleteConfirmButton ?? this.chatsDeleteConfirmButton, + newChatCreateGroupChatButton: + newChatCreateGroupChatButton ?? this.newChatCreateGroupChatButton, + newGroupChatCreateGroupChatButton: newGroupChatCreateGroupChatButton ?? + this.newGroupChatCreateGroupChatButton, + newGroupChatNextButton: + newGroupChatNextButton ?? this.newGroupChatNextButton, + imagePickerCancelButton: + imagePickerCancelButton ?? this.imagePickerCancelButton, + chatSelectImageIconButton: + chatSelectImageIconButton ?? this.chatSelectImageIconButton, + chatSendMessageIconButton: + chatSendMessageIconButton ?? this.chatSendMessageIconButton, + newChatSearchIconButton: + newChatSearchIconButton ?? this.newChatSearchIconButton, + newGroupChatSearchIconButton: + newGroupChatSearchIconButton ?? this.newGroupChatSearchIconButton, + chatBackButton: chatBackButton ?? this.chatBackButton, + chatTitleButton: chatTitleButton ?? this.chatTitleButton, + newGroupChatSelectImage: + newGroupChatSelectImage ?? this.newGroupChatSelectImage, + newGroupChatRemoveImage: + newGroupChatRemoveImage ?? this.newGroupChatRemoveImage, + newGroupChatRemoveUser: + newGroupChatRemoveUser ?? this.newGroupChatRemoveUser, + profileTapUserButton: profileTapUserButton ?? this.profileTapUserButton, + chatsOpenChatButton: chatsOpenChatButton ?? this.chatsOpenChatButton, + userListTapUser: userListTapUser ?? this.userListTapUser, ); } @@ -150,3 +266,6 @@ String _defaultChatsChatSubTitle(int index) => "text_chat_sub_title_$index"; String _defaultChatsChatLastUsed(int index) => "text_chat_last_used_$index"; String _defaultChatsChatUnreadMessages(int index) => "text_chat_unread_messages_$index"; +String _defaultProfileTapUserButton(int index) => "button_tap_user_$index"; +String _defaultChatsOpenChatButton(int index) => "button_open_chat_$index"; +String _defaultUserListTapUser(int index) => "button_tap_user_$index"; diff --git a/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart b/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart index 37bffa5..bcbd329 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart @@ -18,6 +18,7 @@ class FlutterChatEntryWidget extends StatefulWidget { this.counterBackgroundColor = Colors.red, this.textStyle, this.semanticIdUnreadMessages = "text_unread_messages_count", + this.semanticIdOpenButton = "button_open_chat", super.key, }); @@ -51,6 +52,9 @@ class FlutterChatEntryWidget extends StatefulWidget { /// Semantic Id for the unread messages text final String semanticIdUnreadMessages; + /// Semantic Id for the unread messages text + final String semanticIdOpenButton; + @override State createState() => _FlutterChatEntryWidgetState(); } @@ -83,61 +87,65 @@ class _FlutterChatEntryWidgetState extends State { } @override - Widget build(BuildContext context) => InkWell( - onTap: () async => - widget.onTap?.call() ?? - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => FlutterChatNavigatorUserstory( - userId: widget.userId, - options: widget.options ?? ChatOptions(), - ), - ), - ), - child: StreamBuilder( - stream: chatService.getUnreadMessagesCount(), - builder: (BuildContext context, snapshot) => Stack( - alignment: Alignment.center, - children: [ - Container( - width: widget.widgetSize, - height: widget.widgetSize, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: widget.backgroundColor, - ), - child: _AnimatedNotificationIcon( - icon: Icon( - widget.icon, - color: widget.iconColor, - size: widget.widgetSize / 1.5, + Widget build(BuildContext context) => CustomSemantics( + identifier: widget.semanticIdOpenButton, + buttonWithVariableText: true, + child: InkWell( + onTap: () async => + widget.onTap?.call() ?? + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => FlutterChatNavigatorUserstory( + userId: widget.userId, + options: widget.options ?? ChatOptions(), ), - notifications: snapshot.data ?? 0, ), ), - Positioned( - right: 0.0, - top: 0.0, - child: Container( - width: widget.widgetSize / 2, - height: widget.widgetSize / 2, + child: StreamBuilder( + stream: chatService.getUnreadMessagesCount(), + builder: (BuildContext context, snapshot) => Stack( + alignment: Alignment.center, + children: [ + Container( + width: widget.widgetSize, + height: widget.widgetSize, decoration: BoxDecoration( shape: BoxShape.circle, - color: widget.counterBackgroundColor, + color: widget.backgroundColor, ), - child: Center( - child: CustomSemantics( - identifier: widget.semanticIdUnreadMessages, - value: snapshot.data?.toString() ?? "0", - child: Text( - snapshot.data?.toString() ?? "0", - style: widget.textStyle, + child: _AnimatedNotificationIcon( + icon: Icon( + widget.icon, + color: widget.iconColor, + size: widget.widgetSize / 1.5, + ), + notifications: snapshot.data ?? 0, + ), + ), + Positioned( + right: 0.0, + top: 0.0, + child: Container( + width: widget.widgetSize / 2, + height: widget.widgetSize / 2, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: widget.counterBackgroundColor, + ), + child: Center( + child: CustomSemantics( + identifier: widget.semanticIdUnreadMessages, + value: snapshot.data?.toString() ?? "0", + child: Text( + snapshot.data?.toString() ?? "0", + style: widget.textStyle, + ), ), ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart b/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart index 59a396c..04728ef 100644 --- a/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart @@ -3,8 +3,8 @@ import "dart:typed_data"; import "package:chat_repository_interface/chat_repository_interface.dart"; import "package:flutter/material.dart"; -import "package:flutter_chat/src/config/chat_options.dart"; import "package:flutter_accessibility/flutter_accessibility.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; import "package:flutter_chat/src/config/screen_types.dart"; import "package:flutter_chat/src/screens/chat_detail/widgets/chat_bottom.dart"; import "package:flutter_chat/src/screens/chat_detail/widgets/chat_widgets.dart"; @@ -199,9 +199,12 @@ class _ChatAppBar extends StatelessWidget implements PreferredSizeWidget { Widget? appBarIcon; if (onPressBack != null) { - appBarIcon = InkWell( - onTap: onPressBack, - child: const Icon(Icons.arrow_back_ios), + appBarIcon = CustomSemantics( + identifier: options.semantics.chatBackButton, + child: InkWell( + onTap: onPressBack, + child: const Icon(Icons.arrow_back_ios), + ), ); } @@ -209,19 +212,23 @@ class _ChatAppBar extends StatelessWidget implements PreferredSizeWidget { iconTheme: theme.appBarTheme.iconTheme, centerTitle: true, leading: appBarIcon, - title: InkWell( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - onTap: onPressChatTitle, - child: CustomSemantics( - identifier: options.semantics.chatChatTitle, - value: chatTitle ?? "", - child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ?? - Text( - chatTitle ?? "", - overflow: TextOverflow.ellipsis, - ), + title: CustomSemantics( + identifier: options.semantics.chatTitleButton, + buttonWithVariableText: true, + child: InkWell( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + onTap: onPressChatTitle, + child: CustomSemantics( + identifier: options.semantics.chatChatTitle, + value: chatTitle ?? "", + child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ?? + Text( + chatTitle ?? "", + overflow: TextOverflow.ellipsis, + ), + ), ), ), ); diff --git a/packages/flutter_chat/lib/src/screens/chat_detail/widgets/chat_bottom.dart b/packages/flutter_chat/lib/src/screens/chat_detail/widgets/chat_bottom.dart index 54d34e0..9d79144 100644 --- a/packages/flutter_chat/lib/src/screens/chat_detail/widgets/chat_bottom.dart +++ b/packages/flutter_chat/lib/src/screens/chat_detail/widgets/chat_bottom.dart @@ -69,20 +69,26 @@ class ChatBottomInputSection extends HookWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - IconButton( - alignment: Alignment.bottomRight, - onPressed: isLoading ? null : onPressSelectImage, - icon: Icon( - Icons.image_outlined, - color: options.iconEnabledColor, + CustomSemantics( + identifier: options.semantics.chatSelectImageIconButton, + child: IconButton( + alignment: Alignment.bottomRight, + onPressed: isLoading ? null : onPressSelectImage, + icon: Icon( + Icons.image_outlined, + color: options.iconEnabledColor, + ), ), ), - IconButton( - alignment: Alignment.bottomRight, - disabledColor: options.iconDisabledColor, - color: options.iconEnabledColor, - onPressed: isLoading ? null : onClickSendMessage, - icon: const Icon(Icons.send_rounded), + CustomSemantics( + identifier: options.semantics.chatSendMessageIconButton, + child: IconButton( + alignment: Alignment.bottomRight, + disabledColor: options.iconDisabledColor, + color: options.iconEnabledColor, + onPressed: isLoading ? null : onClickSendMessage, + icon: const Icon(Icons.send_rounded), + ), ), ], ), @@ -119,7 +125,8 @@ class ChatBottomInputSection extends HookWidget { top: 16, bottom: 16, ), - // this ensures that that there is space at the end of the textfield + // this ensures that that there is space at the end of the + // textfield suffixIcon: AbsorbPointer( child: Opacity( opacity: 0.0, diff --git a/packages/flutter_chat/lib/src/screens/chat_detail/widgets/default_message_builder.dart b/packages/flutter_chat/lib/src/screens/chat_detail/widgets/default_message_builder.dart index e424360..af26d99 100644 --- a/packages/flutter_chat/lib/src/screens/chat_detail/widgets/default_message_builder.dart +++ b/packages/flutter_chat/lib/src/screens/chat_detail/widgets/default_message_builder.dart @@ -297,7 +297,7 @@ class _DefaultChatImage extends StatelessWidget { options.imageProviderResolver(context, Uri.parse(imageUrl)), fit: BoxFit.fitWidth, errorBuilder: (context, error, stackTrace) => Text( - // TODO: Non-replaceable text + // TODO(Jacques): Non-replaceable text "Something went wrong with loading the image", style: textTheme.bodyLarge?.copyWith( color: messageTheme.textColor, diff --git a/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart b/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart index e7b336f..a0469d9 100644 --- a/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart @@ -130,55 +130,63 @@ class _Body extends StatelessWidget { var chatUserDisplay = Wrap( children: [ if (chat != null) ...[ - ...chat!.users.map( - (tappedUser) => Padding( - padding: const EdgeInsets.only( - bottom: 8, - right: 8, - ), - child: InkWell( - onTap: () => onTapUser?.call(tappedUser), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - FutureBuilder( - future: service.getUser(userId: tappedUser).first, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.waiting) { - return const CircularProgressIndicator(); - } + ...chat!.users.asMap().entries.map( + (entry) { + var index = entry.key; + var tappedUser = entry.value; - var user = snapshot.data; + return Padding( + padding: const EdgeInsets.only( + bottom: 8, + right: 8, + ), + child: CustomSemantics( + identifier: options.semantics.profileTapUserButton(index), + child: InkWell( + onTap: () => onTapUser?.call(tappedUser), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: service.getUser(userId: tappedUser).first, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.waiting) { + return const CircularProgressIndicator(); + } - if (user == null) { - return const SizedBox.shrink(); - } + var user = snapshot.data; - return options.builders.userAvatarBuilder?.call( - context, - user, - 44, - ) ?? - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: user.firstName, - lastName: user.lastName, - imageUrl: - user.imageUrl != null || user.imageUrl != "" + if (user == null) { + return const SizedBox.shrink(); + } + + return options.builders.userAvatarBuilder?.call( + context, + user, + 44, + ) ?? + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: user.imageUrl != null || + user.imageUrl != "" ? user.imageUrl : null, - ), - size: 60, - ); - }, + ), + size: 60, + ); + }, + ), + ], ), - ], + ), ), - ), - ), + ); + }, ), ], ], @@ -286,18 +294,21 @@ class _Body extends StatelessWidget { vertical: 24, horizontal: 80, ), - child: FilledButton( - onPressed: () { - onPressStartChat?.call(user!.id); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - options.translations.newChatButton, - style: theme.textTheme.displayLarge, - ), - ], + child: CustomSemantics( + identifier: options.semantics.profileStartChatButton, + child: FilledButton( + onPressed: () { + onPressStartChat?.call(user!.id); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + options.translations.newChatButton, + style: theme.textTheme.displayLarge, + ), + ], + ), ), ), ), diff --git a/packages/flutter_chat/lib/src/screens/chat_screen.dart b/packages/flutter_chat/lib/src/screens/chat_screen.dart index 2bec923..53ac81e 100644 --- a/packages/flutter_chat/lib/src/screens/chat_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_screen.dart @@ -197,6 +197,8 @@ class _BodyState extends State<_Body> { semantics.chatsChatLastUsed(index), semanticIdUnreadMessages: semantics.chatsChatUnreadMessages(index), + semanticIdButton: + semantics.chatsOpenChatButton(index), ); return !chat.canBeDeleted @@ -275,18 +277,21 @@ class _BodyState extends State<_Body> { vertical: 24, horizontal: 4, ), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - fixedSize: const Size(254, 44), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(56), + child: CustomSemantics( + identifier: options.semantics.chatsStartChatButton, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + fixedSize: const Size(254, 44), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(56), + ), + ), + onPressed: widget.onPressStartChat, + child: Text( + translations.newChatButton, + style: theme.textTheme.displayLarge, ), - ), - onPressed: widget.onPressStartChat, - child: Text( - translations.newChatButton, - style: theme.textTheme.displayLarge, ), ), ), @@ -303,6 +308,7 @@ class _ChatItem extends StatelessWidget { required this.semanticIdSubTitle, required this.semanticIdLastUsed, required this.semanticIdUnreadMessages, + required this.semanticIdButton, }); final ChatModel chat; @@ -311,6 +317,7 @@ class _ChatItem extends StatelessWidget { final String semanticIdSubTitle; final String semanticIdLastUsed; final String semanticIdUnreadMessages; + final String semanticIdButton; @override Widget build(BuildContext context) { @@ -330,29 +337,33 @@ class _ChatItem extends StatelessWidget { semanticIdUnreadMessages: semanticIdUnreadMessages, ); - return InkWell( - onTap: () { - onPressChat(chat); - }, - child: options.builders.chatRowContainerBuilder?.call( - context, - chatListItem, - ) ?? - DecoratedBox( - decoration: BoxDecoration( - color: Colors.transparent, - border: Border( - bottom: BorderSide( - color: theme.dividerColor, - width: 0.5, + return CustomSemantics( + identifier: semanticIdButton, + buttonWithVariableText: true, + child: InkWell( + onTap: () { + onPressChat(chat); + }, + child: options.builders.chatRowContainerBuilder?.call( + context, + chatListItem, + ) ?? + DecoratedBox( + decoration: BoxDecoration( + color: Colors.transparent, + border: Border( + bottom: BorderSide( + color: theme.dividerColor, + width: 0.5, + ), ), ), + child: Padding( + padding: const EdgeInsets.all(12), + child: chatListItem, + ), ), - child: Padding( - padding: const EdgeInsets.all(12), - child: chatListItem, - ), - ), + ), ); } } @@ -530,6 +541,10 @@ Future _deleteDialog( ) async { var theme = Theme.of(context); + var scope = ChatScope.of(context); + + var options = scope.options; + return showModalBottomSheet( context: context, builder: (BuildContext context) => Container( @@ -555,20 +570,23 @@ Future _deleteDialog( ), Padding( padding: const EdgeInsets.symmetric(horizontal: 60), - child: FilledButton( - onPressed: () { - Navigator.of( - context, - ).pop(true); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - translations.deleteChatModalConfirm, - style: theme.textTheme.displayLarge, - ), - ], + child: CustomSemantics( + identifier: options.semantics.chatsDeleteConfirmButton, + child: FilledButton( + onPressed: () { + Navigator.of( + context, + ).pop(true); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translations.deleteChatModalConfirm, + style: theme.textTheme.displayLarge, + ), + ], + ), ), ), ), diff --git a/packages/flutter_chat/lib/src/screens/creation/new_chat_screen.dart b/packages/flutter_chat/lib/src/screens/creation/new_chat_screen.dart index 297f53f..779bfb1 100644 --- a/packages/flutter_chat/lib/src/screens/creation/new_chat_screen.dart +++ b/packages/flutter_chat/lib/src/screens/creation/new_chat_screen.dart @@ -145,6 +145,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { SearchIcon( isSearching: isSearching, onPressed: onPressedSearchIcon, + semanticId: options.semantics.newChatSearchIconButton, ), ], ); @@ -187,23 +188,26 @@ class _Body extends StatelessWidget { right: 32, top: 20, ), - child: FilledButton( - onPressed: onPressCreateGroupChat, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.groups_2, - ), - const SizedBox( - width: 8, - ), - Text( - translations.newGroupChatButton, - style: theme.textTheme.displayLarge, - ), - ], + child: CustomSemantics( + identifier: options.semantics.newChatCreateGroupChatButton, + child: FilledButton( + onPressed: onPressCreateGroupChat, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.groups_2, + ), + const SizedBox( + width: 8, + ), + Text( + translations.newGroupChatButton, + style: theme.textTheme.displayLarge, + ), + ], + ), ), ), ), diff --git a/packages/flutter_chat/lib/src/screens/creation/new_group_chat_overview.dart b/packages/flutter_chat/lib/src/screens/creation/new_group_chat_overview.dart index ae510fb..303809e 100644 --- a/packages/flutter_chat/lib/src/screens/creation/new_group_chat_overview.dart +++ b/packages/flutter_chat/lib/src/screens/creation/new_group_chat_overview.dart @@ -158,31 +158,35 @@ class _BodyState extends State<_Body> { Center( child: Stack( children: [ - InkWell( - onTap: () async => onPressSelectImage( - context, - options, - (image) { - setState(() { - this.image = image; - }); - }, - ), - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: const Color(0xFFD9D9D9), - borderRadius: BorderRadius.circular(40), - image: image != null - ? DecorationImage( - image: MemoryImage(image!), - fit: BoxFit.cover, - ) + CustomSemantics( + identifier: options.semantics.newGroupChatSelectImage, + child: InkWell( + onTap: () async => onPressSelectImage( + context, + options, + (image) { + setState(() { + this.image = image; + }); + }, + ), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: const Color(0xFFD9D9D9), + borderRadius: BorderRadius.circular(40), + image: image != null + ? DecorationImage( + image: MemoryImage(image!), + fit: BoxFit.cover, + ) + : null, + ), + child: image == null + ? const Icon(Icons.image) : null, ), - child: - image == null ? const Icon(Icons.image) : null, ), ), if (image != null) @@ -197,15 +201,19 @@ class _BodyState extends State<_Body> { borderRadius: BorderRadius.circular(40), ), child: Center( - child: InkWell( - onTap: () { - setState(() { - image = null; - }); - }, - child: const Icon( - Icons.close, - size: 12, + child: CustomSemantics( + identifier: + options.semantics.newGroupChatRemoveImage, + child: InkWell( + onTap: () { + setState(() { + image = null; + }); + }, + child: const Icon( + Icons.close, + size: 12, + ), ), ), ), @@ -354,31 +362,34 @@ class _BodyState extends State<_Body> { ), child: ValueListenableBuilder( valueListenable: isButtonEnabled, - builder: (context, isEnabled, child) => FilledButton( - onPressed: users.isNotEmpty - ? () async { - if (!isPressed) { - isPressed = true; - if (formKey.currentState!.validate()) { - await widget.onComplete( - users, - _chatNameController.text, - _bioController.text, - image, - ); + builder: (context, isEnabled, child) => CustomSemantics( + identifier: "", + child: FilledButton( + onPressed: users.isNotEmpty + ? () async { + if (!isPressed) { + isPressed = true; + if (formKey.currentState!.validate()) { + await widget.onComplete( + users, + _chatNameController.text, + _bioController.text, + image, + ); + } + isPressed = false; } - isPressed = false; } - } - : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - translations.createGroupChatButton, - style: theme.textTheme.displayLarge, - ), - ], + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translations.createGroupChatButton, + style: theme.textTheme.displayLarge, + ), + ], + ), ), ), ), @@ -402,38 +413,41 @@ class _SelectedUser extends StatelessWidget { Widget build(BuildContext context) { var chatScope = ChatScope.of(context); var options = chatScope.options; - return InkWell( - onTap: () { - onRemove(user); - }, - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: options.builders.userAvatarBuilder?.call( - context, - user, - 40, - ) ?? - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: user.firstName, - lastName: user.lastName, - imageUrl: user.imageUrl != "" ? user.imageUrl : null, + return CustomSemantics( + identifier: options.semantics.newGroupChatRemoveUser, + child: InkWell( + onTap: () { + onRemove(user); + }, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: options.builders.userAvatarBuilder?.call( + context, + user, + 40, + ) ?? + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: user.imageUrl != "" ? user.imageUrl : null, + ), + size: 40, ), - size: 40, - ), - ), - Positioned.directional( - textDirection: Directionality.of(context), - end: 0, - child: const Icon( - Icons.cancel, - size: 20, ), - ), - ], + Positioned.directional( + textDirection: Directionality.of(context), + end: 0, + child: const Icon( + Icons.cancel, + size: 20, + ), + ), + ], + ), ), ); } diff --git a/packages/flutter_chat/lib/src/screens/creation/new_group_chat_screen.dart b/packages/flutter_chat/lib/src/screens/creation/new_group_chat_screen.dart index 7d082c2..bab755a 100644 --- a/packages/flutter_chat/lib/src/screens/creation/new_group_chat_screen.dart +++ b/packages/flutter_chat/lib/src/screens/creation/new_group_chat_screen.dart @@ -1,5 +1,6 @@ import "package:chat_repository_interface/chat_repository_interface.dart"; import "package:flutter/material.dart"; +import "package:flutter_accessibility/flutter_accessibility.dart"; import "package:flutter_chat/src/config/screen_types.dart"; import "package:flutter_chat/src/screens/creation/widgets/search_field.dart"; import "package:flutter_chat/src/screens/creation/widgets/search_icon.dart"; @@ -155,6 +156,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { SearchIcon( isSearching: isSearching, onPressed: onPressedSearchIcon, + semanticId: options.semantics.newGroupChatSearchIconButton, ), ], ); @@ -272,18 +274,21 @@ class _NextButton extends StatelessWidget { ), child: Visibility( visible: selectedUsers.isNotEmpty, - child: FilledButton( - onPressed: () async { - await onPressGroupChatOverview(selectedUsers); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - options.translations.next, - style: theme.textTheme.displayLarge, - ), - ], + child: CustomSemantics( + identifier: options.semantics.newGroupChatNextButton, + child: FilledButton( + onPressed: () async { + await onPressGroupChatOverview(selectedUsers); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + options.translations.next, + style: theme.textTheme.displayLarge, + ), + ], + ), ), ), ), diff --git a/packages/flutter_chat/lib/src/screens/creation/widgets/default_image_picker.dart b/packages/flutter_chat/lib/src/screens/creation/widgets/default_image_picker.dart index 9cacb9d..c177bb4 100644 --- a/packages/flutter_chat/lib/src/screens/creation/widgets/default_image_picker.dart +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/default_image_picker.dart @@ -1,6 +1,7 @@ import "dart:typed_data"; import "package:flutter/material.dart"; +import "package:flutter_accessibility/flutter_accessibility.dart"; import "package:flutter_chat/src/config/chat_options.dart"; import "package:flutter_chat/src/config/chat_translations.dart"; import "package:flutter_chat/src/util/scope.dart"; @@ -71,13 +72,16 @@ class DefaultImagePickerDialog extends StatelessWidget { Icons.insert_drive_file_rounded, size: 60, ), - closeButtonBuilder: (ontap) => TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text( - translations.cancelImagePickerBtn, - style: textTheme.bodyMedium!.copyWith( - fontSize: 18, - decoration: TextDecoration.underline, + closeButtonBuilder: (ontap) => CustomSemantics( + identifier: options.semantics.imagePickerCancelButton, + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + translations.cancelImagePickerBtn, + style: textTheme.bodyMedium!.copyWith( + fontSize: 18, + decoration: TextDecoration.underline, + ), ), ), ), diff --git a/packages/flutter_chat/lib/src/screens/creation/widgets/search_icon.dart b/packages/flutter_chat/lib/src/screens/creation/widgets/search_icon.dart index 98cbc96..d6c82c3 100644 --- a/packages/flutter_chat/lib/src/screens/creation/widgets/search_icon.dart +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/search_icon.dart @@ -6,6 +6,7 @@ class SearchIcon extends StatelessWidget { const SearchIcon({ required this.isSearching, required this.onPressed, + required this.semanticId, super.key, }); @@ -15,14 +16,20 @@ class SearchIcon extends StatelessWidget { /// Callback function triggered when the search icon is pressed final VoidCallback onPressed; + /// Semantic id for icon button + final String semanticId; + @override Widget build(BuildContext context) { var theme = Theme.of(context); - return IconButton( - onPressed: onPressed, - icon: Icon( - isSearching ? Icons.close : Icons.search, - color: theme.appBarTheme.iconTheme?.color ?? Colors.white, + return Semantics( + identifier: semanticId, + child: IconButton( + onPressed: onPressed, + icon: Icon( + isSearching ? Icons.close : Icons.search, + color: theme.appBarTheme.iconTheme?.color ?? Colors.white, + ), ), ); } diff --git a/packages/flutter_chat/lib/src/screens/creation/widgets/user_list.dart b/packages/flutter_chat/lib/src/screens/creation/widgets/user_list.dart index 209dadc..bea04a9 100644 --- a/packages/flutter_chat/lib/src/screens/creation/widgets/user_list.dart +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/user_list.dart @@ -80,70 +80,20 @@ class _UserListState extends State { itemBuilder: (context, index) { var user = filteredUsers[index]; var isSelected = widget.selectedUsers.any((u) => u.id == user.id); - return InkWell( - onTap: () async { - if (widget.creatingGroup) { - return handleGroupChatTap(user); - } else { - return handlePersonalChatTap(user); - } - }, - child: options.builders.chatRowContainerBuilder?.call( - context, - Row( - children: [ - options.builders.userAvatarBuilder - ?.call(context, user, 44) ?? - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: user.firstName, - lastName: user.lastName, - imageUrl: - user.imageUrl != "" ? user.imageUrl : null, - ), - size: 44, - ), - const SizedBox( - width: 12, - ), - CustomSemantics( - identifier: options.semantics - .newChatUserListUserFullName(index), - value: user.fullname ?? translations.anonymousUser, - child: Text( - user.fullname ?? translations.anonymousUser, - style: theme.textTheme.titleMedium, - ), - ), - if (widget.creatingGroup) ...[ - const Spacer(), - Checkbox( - value: isSelected, - onChanged: (value) { - handleGroupChatTap(user); - }, - ), - const SizedBox( - width: 12, - ), - ], - ], - ), - ) ?? - DecoratedBox( - decoration: BoxDecoration( - color: Colors.transparent, - border: Border( - bottom: BorderSide( - color: theme.dividerColor, - width: 0.5, - ), - ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( + return CustomSemantics( + identifier: options.semantics.userListTapUser(index), + buttonWithVariableText: true, + child: InkWell( + onTap: () async { + if (widget.creatingGroup) { + return handleGroupChatTap(user); + } else { + return handlePersonalChatTap(user); + } + }, + child: options.builders.chatRowContainerBuilder?.call( + context, + Row( children: [ options.builders.userAvatarBuilder ?.call(context, user, 44) ?? @@ -183,8 +133,63 @@ class _UserListState extends State { ], ], ), + ) ?? + DecoratedBox( + decoration: BoxDecoration( + color: Colors.transparent, + border: Border( + bottom: BorderSide( + color: theme.dividerColor, + width: 0.5, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + options.builders.userAvatarBuilder + ?.call(context, user, 44) ?? + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: user.imageUrl != "" + ? user.imageUrl + : null, + ), + size: 44, + ), + const SizedBox( + width: 12, + ), + CustomSemantics( + identifier: options.semantics + .newChatUserListUserFullName(index), + value: user.fullname ?? translations.anonymousUser, + child: Text( + user.fullname ?? translations.anonymousUser, + style: theme.textTheme.titleMedium, + ), + ), + if (widget.creatingGroup) ...[ + const Spacer(), + Checkbox( + value: isSelected, + onChanged: (value) { + handleGroupChatTap(user); + }, + ), + const SizedBox( + width: 12, + ), + ], + ], + ), + ), ), - ), + ), ); }, ), diff --git a/packages/flutter_chat/pubspec.yaml b/packages/flutter_chat/pubspec.yaml index 36d1791..50a6dcd 100644 --- a/packages/flutter_chat/pubspec.yaml +++ b/packages/flutter_chat/pubspec.yaml @@ -15,6 +15,9 @@ dependencies: intl: any flutter_hooks: ^0.20.5 + flutter_accessibility: + hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub + version: ^0.0.2 flutter_image_picker: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub version: ^4.0.0