diff --git a/packages/flutter_chat/lib/src/config/chat_builders.dart b/packages/flutter_chat/lib/src/config/chat_builders.dart index c741e0d..e5a8c3c 100644 --- a/packages/flutter_chat/lib/src/config/chat_builders.dart +++ b/packages/flutter_chat/lib/src/config/chat_builders.dart @@ -169,6 +169,9 @@ typedef ChatMessageBuilder = Widget? Function( MessageModel? previousMessage, UserModel? sender, Function(UserModel sender) onPressSender, + String semanticIdTitle, + String semanticIdText, + String semanticIdTime, ); /// The group avatar builder diff --git a/packages/flutter_chat/lib/src/config/chat_options.dart b/packages/flutter_chat/lib/src/config/chat_options.dart index 7760256..9dbf314 100644 --- a/packages/flutter_chat/lib/src/config/chat_options.dart +++ b/packages/flutter_chat/lib/src/config/chat_options.dart @@ -2,6 +2,7 @@ import "package:cached_network_image/cached_network_image.dart"; import "package:chat_repository_interface/chat_repository_interface.dart"; import "package:flutter/material.dart"; import "package:flutter_chat/src/config/chat_builders.dart"; +import "package:flutter_chat/src/config/chat_semantics.dart"; import "package:flutter_chat/src/config/chat_translations.dart"; /// The chat options @@ -13,6 +14,7 @@ class ChatOptions { this.groupChatEnabled = true, this.enableLoadingIndicator = true, this.translations = const ChatTranslations.empty(), + this.semantics = const ChatSemantics.standard(), this.builders = const ChatBuilders(), this.spacing = const ChatSpacing(), this.paginationControls = const ChatPaginationControls(), @@ -43,6 +45,9 @@ class ChatOptions { /// [translations] is the chat translations. final ChatTranslations translations; + /// [semantics] is the chat semantics. + final ChatSemantics semantics; + /// [builders] is the chat builders. final ChatBuilders builders; diff --git a/packages/flutter_chat/lib/src/config/chat_semantics.dart b/packages/flutter_chat/lib/src/config/chat_semantics.dart new file mode 100644 index 0000000..1c4240f --- /dev/null +++ b/packages/flutter_chat/lib/src/config/chat_semantics.dart @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2022 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +// ignore_for_file: public_member_api_docs + +/// Class that holds all the semantic ids for the chat component view and +/// the corresponding userstory +class ChatSemantics { + /// ChatTranslations constructor where everything is required use this + /// if you want to be sure to have all translations specified + /// If you just want the default values use the empty constructor + /// and optionally override the values with the copyWith method + const ChatSemantics({ + required this.profileTitle, + required this.profileDescription, + required this.chatUnreadMessages, + required this.chatChatTitle, + required this.chatNoMessages, + required this.newChatGetUsersError, + required this.newGroupChatMemberAmount, + required this.newGroupChatGetUsersError, + required this.newChatUserListUserFullName, + required this.chatBubbleTitle, + required this.chatBubbleTime, + required this.chatBubbleText, + required this.chatsChatTitle, + required this.chatsChatSubTitle, + required this.chatsChatLastUsed, + required this.chatsChatUnreadMessages, + }); + + /// Default translations for the chat component view + const ChatSemantics.standard({ + this.profileTitle = "text_profile_title", + this.profileDescription = "text_profile_description", + this.chatUnreadMessages = "text_unread_messages", + this.chatChatTitle = "text_chat_title", + this.chatNoMessages = "text_no_messages", + this.newChatGetUsersError = "text_get_users_error", + this.newGroupChatMemberAmount = "text_member_amount", + this.newGroupChatGetUsersError = "text_get_users_error", + this.newChatUserListUserFullName = _defaultNewChatUserListUserFullName, + this.chatBubbleTitle = _defaultChatBubbleTitle, + this.chatBubbleTime = _defaultChatBubbleTime, + this.chatBubbleText = _defaultChatBubbleText, + this.chatsChatTitle = _defaultChatsChatTitle, + this.chatsChatSubTitle = _defaultChatsChatSubTitle, + this.chatsChatLastUsed = _defaultChatsChatLastUsed, + this.chatsChatUnreadMessages = _defaultChatsChatUnreadMessages, + }); + + // Text + final String profileTitle; + final String profileDescription; + final String chatUnreadMessages; + final String chatChatTitle; + final String chatNoMessages; + final String newChatGetUsersError; + final String newGroupChatMemberAmount; + final String newGroupChatGetUsersError; + + // Indexed text + final String Function(int index) newChatUserListUserFullName; + final String Function(int index) chatBubbleTitle; + final String Function(int index) chatBubbleTime; + final String Function(int index) chatBubbleText; + final String Function(int index) chatsChatTitle; + final String Function(int index) chatsChatSubTitle; + final String Function(int index) chatsChatLastUsed; + final String Function(int index) chatsChatUnreadMessages; + + ChatSemantics copyWith({ + String? profileTitle, + String? profileDescription, + String? chatUnreadMessages, + String? chatChatTitle, + String? chatNoMessages, + String? newChatGetUsersError, + String? newGroupChatMemberAmount, + String? newGroupChatGetUsersError, + String Function(int)? newChatUserListUserFullName, + String Function(int)? chatBubbleTitle, + String Function(int)? chatBubbleTime, + String Function(int)? chatBubbleText, + String Function(int)? chatsChatTitle, + String Function(int)? chatsChatSubTitle, + String Function(int)? chatsChatLastUsed, + String Function(int)? chatsChatUnreadMessages, + }) => + ChatSemantics( + profileTitle: profileTitle ?? this.profileTitle, + profileDescription: profileDescription ?? this.profileDescription, + chatUnreadMessages: chatUnreadMessages ?? this.chatUnreadMessages, + chatChatTitle: chatChatTitle ?? this.chatChatTitle, + chatNoMessages: chatNoMessages ?? this.chatNoMessages, + newChatGetUsersError: newChatGetUsersError ?? this.newChatGetUsersError, + newGroupChatMemberAmount: + newGroupChatMemberAmount ?? this.newGroupChatMemberAmount, + newGroupChatGetUsersError: + newGroupChatGetUsersError ?? this.newGroupChatGetUsersError, + newChatUserListUserFullName: + newChatUserListUserFullName ?? this.newChatUserListUserFullName, + chatBubbleTitle: chatBubbleTitle ?? this.chatBubbleTitle, + chatBubbleTime: chatBubbleTime ?? this.chatBubbleTime, + chatBubbleText: chatBubbleText ?? this.chatBubbleText, + chatsChatTitle: chatsChatTitle ?? this.chatsChatTitle, + chatsChatSubTitle: chatsChatSubTitle ?? this.chatsChatSubTitle, + chatsChatLastUsed: chatsChatLastUsed ?? this.chatsChatLastUsed, + chatsChatUnreadMessages: + chatsChatUnreadMessages ?? this.chatsChatUnreadMessages, + ); +} + +String _defaultNewChatUserListUserFullName(int index) => + "text_user_fullname_$index"; +String _defaultChatBubbleTitle(int index) => "text_chat_bubble_title_$index"; +String _defaultChatBubbleTime(int index) => "text_chat_bubble_time_$index"; +String _defaultChatBubbleText(int index) => "text_chat_bubble_text_$index"; +String _defaultChatsChatTitle(int index) => "text_chat_title_$index"; +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"; 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 8f0dd8e..37bffa5 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart @@ -1,6 +1,7 @@ import "dart:async"; import "package:flutter/material.dart"; +import "package:flutter_accessibility/flutter_accessibility.dart"; import "package:flutter_chat/flutter_chat.dart"; /// A widget representing an entry point for a chat UI. @@ -16,6 +17,7 @@ class FlutterChatEntryWidget extends StatefulWidget { this.iconColor = Colors.black, this.counterBackgroundColor = Colors.red, this.textStyle, + this.semanticIdUnreadMessages = "text_unread_messages_count", super.key, }); @@ -46,6 +48,9 @@ class FlutterChatEntryWidget extends StatefulWidget { /// The chat options final ChatOptions? options; + /// Semantic Id for the unread messages text + final String semanticIdUnreadMessages; + @override State createState() => _FlutterChatEntryWidgetState(); } @@ -121,9 +126,13 @@ class _FlutterChatEntryWidgetState extends State { color: widget.counterBackgroundColor, ), child: Center( - child: Text( - snapshot.data?.toString() ?? "0", - style: widget.textStyle, + 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 aa7fdc7..59a396c 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 @@ -4,6 +4,7 @@ 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/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"; @@ -213,11 +214,15 @@ class _ChatAppBar extends StatelessWidget implements PreferredSizeWidget { highlightColor: Colors.transparent, hoverColor: Colors.transparent, onTap: onPressChatTitle, - child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ?? - Text( - chatTitle ?? "", - overflow: TextOverflow.ellipsis, - ), + child: CustomSemantics( + identifier: options.semantics.chatChatTitle, + value: chatTitle ?? "", + child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ?? + Text( + chatTitle ?? "", + overflow: TextOverflow.ellipsis, + ), + ), ), ); } @@ -458,6 +463,9 @@ class _ChatBody extends HookWidget { previousMessage: prevMsg, sender: userMap[msg.senderId], onPressSender: onPressUserProfile, + semanticIdTitle: options.semantics.chatBubbleTitle(index), + semanticIdTime: options.semantics.chatBubbleTime(index), + semanticIdText: options.semantics.chatBubbleText(index), ), ); } diff --git a/packages/flutter_chat/lib/src/screens/chat_detail/widgets/chat_widgets.dart b/packages/flutter_chat/lib/src/screens/chat_detail/widgets/chat_widgets.dart index 06408f5..66057a6 100644 --- a/packages/flutter_chat/lib/src/screens/chat_detail/widgets/chat_widgets.dart +++ b/packages/flutter_chat/lib/src/screens/chat_detail/widgets/chat_widgets.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/screens/chat_detail/widgets/default_message_builder.dart"; import "package:flutter_chat/src/util/scope.dart"; import "package:flutter_hooks/flutter_hooks.dart"; @@ -18,15 +19,22 @@ class ChatNoMessages extends HookWidget { @override Widget build(BuildContext context) { var chatScope = ChatScope.of(context); - var translations = chatScope.options.translations; + var options = chatScope.options; + var translations = options.translations; var theme = Theme.of(context); return Center( - child: Text( - isGroupChat + child: CustomSemantics( + identifier: options.semantics.chatNoMessages, + value: isGroupChat ? translations.writeFirstMessageInGroupChat : translations.writeMessageToStartChat, - style: theme.textTheme.bodySmall, + child: Text( + isGroupChat + ? translations.writeFirstMessageInGroupChat + : translations.writeMessageToStartChat, + style: theme.textTheme.bodySmall, + ), ), ); } @@ -39,6 +47,9 @@ class ChatBubble extends HookWidget { required this.message, required this.sender, required this.onPressSender, + required this.semanticIdTitle, + required this.semanticIdText, + required this.semanticIdTime, this.previousMessage, super.key, }); @@ -56,6 +67,15 @@ class ChatBubble extends HookWidget { /// Callback function when a message sender is pressed. final Function(UserModel user) onPressSender; + /// Semantic id for message title + final String semanticIdTitle; + + /// Semantic id for message time + final String semanticIdTime; + + /// Semantic id for message text + final String semanticIdText; + @override Widget build(BuildContext context) { var chatScope = ChatScope.of(context); @@ -67,12 +87,18 @@ class ChatBubble extends HookWidget { previousMessage, sender, onPressSender, + semanticIdTitle, + semanticIdTime, + semanticIdText, ) ?? DefaultChatMessageBuilder( message: message, previousMessage: previousMessage, sender: sender, onPressSender: onPressSender, + semanticIdTitle: semanticIdTitle, + semanticIdTime: semanticIdTime, + semanticIdText: semanticIdText, ); } } 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 8517a25..e424360 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 @@ -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/chat_options.dart"; import "package:flutter_chat/src/services/date_formatter.dart"; import "package:flutter_chat/src/util/scope.dart"; @@ -14,6 +15,9 @@ class DefaultChatMessageBuilder extends StatelessWidget { required this.previousMessage, required this.sender, required this.onPressSender, + required this.semanticIdTitle, + required this.semanticIdText, + required this.semanticIdTime, super.key, }); @@ -30,6 +34,15 @@ class DefaultChatMessageBuilder extends StatelessWidget { /// The function that is called when the sender is clicked final Function(UserModel user) onPressSender; + /// Semantic id for message title + final String semanticIdTitle; + + /// Semantic id for message time + final String semanticIdTime; + + /// Semantic id for message text + final String semanticIdText; + /// implements [ChatMessageBuilder] static Widget builder( BuildContext context, @@ -37,12 +50,18 @@ class DefaultChatMessageBuilder extends StatelessWidget { MessageModel? previousMessage, UserModel? sender, Function(UserModel sender) onPressSender, + String semanticIdTitle, + String semanticIdText, + String semanticIdTime, ) => DefaultChatMessageBuilder( message: message, previousMessage: previousMessage, sender: sender, onPressSender: onPressSender, + semanticIdTitle: semanticIdTitle, + semanticIdTime: semanticIdTime, + semanticIdText: semanticIdText, ); /// Merges the [MessageTheme] from the themeresolver with the [MessageTheme] @@ -87,6 +106,9 @@ class DefaultChatMessageBuilder extends StatelessWidget { message: message, messageTheme: messageTheme, sender: sender, + semanticIdTitle: semanticIdTitle, + semanticIdTime: semanticIdTime, + semanticIdText: semanticIdText, ); var messagePadding = messageTheme.messageSidePadding!; @@ -126,6 +148,9 @@ class _ChatMessageBubble extends StatelessWidget { required this.previousMessage, required this.messageTheme, required this.sender, + required this.semanticIdTitle, + required this.semanticIdTime, + required this.semanticIdText, }); final bool isSameSender; @@ -134,6 +159,9 @@ class _ChatMessageBubble extends StatelessWidget { final MessageModel? previousMessage; final MessageTheme messageTheme; final UserModel? sender; + final String semanticIdTitle; + final String semanticIdTime; + final String semanticIdText; @override Widget build(BuildContext context) { @@ -154,9 +182,13 @@ class _ChatMessageBubble extends StatelessWidget { var senderTitle = options.senderTitleResolver?.call(sender) ?? sender?.firstName ?? ""; - var senderTitleText = Text( - senderTitle, - style: theme.textTheme.titleMedium, + var senderTitleText = CustomSemantics( + identifier: semanticIdTitle, + value: senderTitle, + child: Text( + senderTitle, + style: theme.textTheme.titleMedium, + ), ); var messageTimeRow = Row( @@ -164,12 +196,16 @@ class _ChatMessageBubble extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.only(right: 8, bottom: 4), - child: Text( - messageTime, - style: textTheme.bodySmall?.copyWith( - color: messageTheme.textColor, + child: CustomSemantics( + identifier: semanticIdTime, + value: messageTime, + child: Text( + messageTime, + style: textTheme.bodySmall?.copyWith( + color: messageTheme.textColor, + ), + textAlign: TextAlign.end, ), - textAlign: TextAlign.end, ), ), ], @@ -207,12 +243,16 @@ class _ChatMessageBubble extends StatelessWidget { right: 12, bottom: 4, ), - child: Text( - message.text!, - style: textTheme.bodyLarge?.copyWith( - color: messageTheme.textColor, + child: Semantics( + identifier: semanticIdText, + value: message.text, + child: Text( + message.text!, + style: textTheme.bodyLarge?.copyWith( + color: messageTheme.textColor, + ), + textAlign: messageTheme.textAlignment, ), - textAlign: messageTheme.textAlignment, ), ), ], @@ -257,6 +297,7 @@ class _DefaultChatImage extends StatelessWidget { options.imageProviderResolver(context, Uri.parse(imageUrl)), fit: BoxFit.fitWidth, errorBuilder: (context, error, stackTrace) => Text( + // TODO: 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 f6b8f80..e7b336f 100644 --- a/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_profile_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/util/scope.dart"; import "package:flutter_hooks/flutter_hooks.dart"; @@ -50,7 +51,10 @@ class ChatProfileScreen extends HookWidget { ? chatModel?.chatName ?? options.translations.groupNameEmpty : ""; - var appBar = _AppBar(title: chatTitle); + var appBar = _AppBar( + title: chatTitle, + semanticId: options.semantics.profileTitle, + ); var body = _Body( user: userModel, @@ -79,16 +83,22 @@ class ChatProfileScreen extends HookWidget { class _AppBar extends StatelessWidget implements PreferredSizeWidget { const _AppBar({ required this.title, + required this.semanticId, }); final String title; + final String semanticId; @override Widget build(BuildContext context) { var theme = Theme.of(context); return AppBar( iconTheme: theme.appBarTheme.iconTheme, - title: Text(title), + title: CustomSemantics( + identifier: semanticId, + value: title, + child: Text(title), + ), ); } @@ -247,9 +257,13 @@ class _Body extends StatelessWidget { style: theme.textTheme.titleMedium, ), const SizedBox(height: 12), - Text( - chat!.description ?? "", - style: theme.textTheme.bodyMedium, + CustomSemantics( + identifier: options.semantics.profileDescription, + value: chat!.description ?? "", + child: Text( + chat!.description ?? "", + style: theme.textTheme.bodyMedium, + ), ), const SizedBox(height: 12), Text( diff --git a/packages/flutter_chat/lib/src/screens/chat_screen.dart b/packages/flutter_chat/lib/src/screens/chat_screen.dart index 0e75708..2bec923 100644 --- a/packages/flutter_chat/lib/src/screens/chat_screen.dart +++ b/packages/flutter_chat/lib/src/screens/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/chat_translations.dart"; import "package:flutter_chat/src/config/screen_types.dart"; import "package:flutter_chat/src/services/date_formatter.dart"; @@ -92,9 +93,13 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { visible: (snapshot.data ?? 0) > 0, child: Padding( padding: const EdgeInsets.only(right: 22.0), - child: Text( - "${snapshot.data ?? 0} ${translations.chatsUnread}", - style: theme.textTheme.bodySmall, + child: CustomSemantics( + identifier: options.semantics.chatUnreadMessages, + value: "${snapshot.data ?? 0} ${translations.chatsUnread}", + child: Text( + "${snapshot.data ?? 0} ${translations.chatsUnread}", + style: theme.textTheme.bodySmall, + ), ), ), ), @@ -166,7 +171,8 @@ class _BodyState extends State<_Body> { } return Column( children: [ - for (ChatModel chat in snapshot.data ?? []) ...[ + for (var (index, ChatModel chat) + in (snapshot.data ?? []).indexed) ...[ DecoratedBox( decoration: BoxDecoration( border: Border( @@ -177,66 +183,77 @@ class _BodyState extends State<_Body> { ), ), child: Builder( - builder: (context) => !chat.canBeDeleted - ? Dismissible( - confirmDismiss: (_) async { - await options - .builders.deleteChatDialogBuilder - ?.call(context, chat) ?? - _deleteDialog( - chat, - translations, - // ignore: use_build_context_synchronously - context, - ); - return _deleteDialog( - chat, - translations, - // ignore: use_build_context_synchronously - context, - ); - }, - onDismissed: (_) { - widget.onDeleteChat(chat); - }, - secondaryBackground: const ColoredBox( - color: Colors.red, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.all(8.0), - child: Icon( - Icons.delete, - color: Colors.white, + builder: (context) { + var semantics = options.semantics; + + var chatItem = _ChatItem( + chat: chat, + onPressChat: widget.onPressChat, + semanticIdTitle: + semantics.chatsChatTitle(index), + semanticIdSubTitle: + semantics.chatsChatSubTitle(index), + semanticIdLastUsed: + semantics.chatsChatLastUsed(index), + semanticIdUnreadMessages: + semantics.chatsChatUnreadMessages(index), + ); + + return !chat.canBeDeleted + ? Dismissible( + confirmDismiss: (_) async { + await options.builders + .deleteChatDialogBuilder + ?.call(context, chat) ?? + _deleteDialog( + chat, + translations, + // ignore: use_build_context_synchronously + context, + ); + return _deleteDialog( + chat, + translations, + // ignore: use_build_context_synchronously + context, + ); + }, + onDismissed: (_) { + widget.onDeleteChat(chat); + }, + secondaryBackground: const ColoredBox( + color: Colors.red, + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.delete, + color: Colors.white, + ), ), ), ), - ), - background: const ColoredBox( - color: Colors.red, - child: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.all(8.0), - child: Icon( - Icons.delete, - color: Colors.white, + background: const ColoredBox( + color: Colors.red, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.delete, + color: Colors.white, + ), ), ), ), - ), - key: ValueKey( - chat.id, - ), - child: _ChatItem( - chat: chat, - onPressChat: widget.onPressChat, - ), - ) - : _ChatItem( - chat: chat, - onPressChat: widget.onPressChat, - ), + key: ValueKey( + chat.id, + ), + child: chatItem, + ) + : chatItem; + }, ), ), ], @@ -282,10 +299,18 @@ class _ChatItem extends StatelessWidget { const _ChatItem({ required this.chat, required this.onPressChat, + required this.semanticIdTitle, + required this.semanticIdSubTitle, + required this.semanticIdLastUsed, + required this.semanticIdUnreadMessages, }); final ChatModel chat; final Function(ChatModel chat) onPressChat; + final String semanticIdTitle; + final String semanticIdSubTitle; + final String semanticIdLastUsed; + final String semanticIdUnreadMessages; @override Widget build(BuildContext context) { @@ -295,16 +320,23 @@ class _ChatItem extends StatelessWidget { options: options, ); var theme = Theme.of(context); + + var chatListItem = _ChatListItem( + chat: chat, + dateFormatter: dateFormatter, + semanticIdTitle: semanticIdTitle, + semanticIdSubTitle: semanticIdSubTitle, + semanticIdLastUsed: semanticIdLastUsed, + semanticIdUnreadMessages: semanticIdUnreadMessages, + ); + return InkWell( onTap: () { onPressChat(chat); }, child: options.builders.chatRowContainerBuilder?.call( context, - _ChatListItem( - chat: chat, - dateFormatter: dateFormatter, - ), + chatListItem, ) ?? DecoratedBox( decoration: BoxDecoration( @@ -318,10 +350,7 @@ class _ChatItem extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.all(12), - child: _ChatListItem( - chat: chat, - dateFormatter: dateFormatter, - ), + child: chatListItem, ), ), ); @@ -332,10 +361,18 @@ class _ChatListItem extends StatelessWidget { const _ChatListItem({ required this.chat, required this.dateFormatter, + required this.semanticIdTitle, + required this.semanticIdSubTitle, + required this.semanticIdLastUsed, + required this.semanticIdUnreadMessages, }); final ChatModel chat; final DateFormatter dateFormatter; + final String semanticIdTitle; + final String semanticIdSubTitle; + final String semanticIdLastUsed; + final String semanticIdUnreadMessages; @override Widget build(BuildContext context) { @@ -366,6 +403,10 @@ class _ChatListItem extends StatelessWidget { return _ChatRow( title: chat.chatName ?? translations.groupNameEmpty, + semanticIdTitle: semanticIdTitle, + semanticIdSubTitle: semanticIdSubTitle, + semanticIdLastUsed: semanticIdLastUsed, + semanticIdUnreadMessages: semanticIdUnreadMessages, unreadMessages: showUnreadMessageCount ? chat.unreadMessageCount : 0, subTitle: data != null @@ -441,6 +482,10 @@ class _ChatListItem extends StatelessWidget { return _ChatRow( unreadMessages: showUnreadMessageCount ? chat.unreadMessageCount : 0, + semanticIdTitle: semanticIdTitle, + semanticIdSubTitle: semanticIdSubTitle, + semanticIdLastUsed: semanticIdLastUsed, + semanticIdUnreadMessages: semanticIdUnreadMessages, avatar: options.builders.userAvatarBuilder?.call( context, otherUser, @@ -536,6 +581,10 @@ Future _deleteDialog( class _ChatRow extends StatelessWidget { const _ChatRow({ required this.title, + required this.semanticIdTitle, + required this.semanticIdSubTitle, + required this.semanticIdLastUsed, + required this.semanticIdUnreadMessages, this.unreadMessages = 0, this.lastUsed, this.subTitle, @@ -544,15 +593,19 @@ class _ChatRow extends StatelessWidget { /// The title of the chat. final String title; + final String semanticIdTitle; /// The number of unread messages in the chat. final int unreadMessages; + final String semanticIdUnreadMessages; /// The last time the chat was used. final String? lastUsed; + final String semanticIdLastUsed; /// The subtitle of the chat. final String? subTitle; + final String semanticIdSubTitle; /// The avatar associated with the chat. final Widget? avatar; @@ -573,20 +626,28 @@ class _ChatRow extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleMedium, + CustomSemantics( + identifier: semanticIdTitle, + value: title, + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium, + ), ), if (subTitle != null) ...[ Padding( padding: const EdgeInsets.only(top: 3.0), - child: Text( - subTitle!, - style: theme.textTheme.bodySmall, - overflow: TextOverflow.ellipsis, - maxLines: 2, + child: CustomSemantics( + identifier: semanticIdSubTitle, + value: subTitle, + child: Text( + subTitle!, + style: theme.textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), ), ), ], @@ -601,9 +662,13 @@ class _ChatRow extends StatelessWidget { if (lastUsed != null) ...[ Padding( padding: const EdgeInsets.only(bottom: 4.0), - child: Text( - lastUsed!, - style: theme.textTheme.labelSmall, + child: CustomSemantics( + identifier: semanticIdLastUsed, + value: lastUsed, + child: Text( + lastUsed!, + style: theme.textTheme.labelSmall, + ), ), ), ], @@ -616,10 +681,14 @@ class _ChatRow extends StatelessWidget { shape: BoxShape.circle, ), child: Center( - child: Text( - unreadMessages.toString(), - style: const TextStyle( - fontSize: 14, + child: CustomSemantics( + identifier: semanticIdUnreadMessages, + value: unreadMessages.toString(), + child: Text( + unreadMessages.toString(), + style: const TextStyle( + fontSize: 14, + ), ), ), ), 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 42bed74..d85dda0 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 @@ -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"; @@ -211,10 +212,17 @@ class _Body extends StatelessWidget { // ignore: discarded_futures stream: service.getAllUsers(), builder: (context, snapshot) { + var chatScope = ChatScope.of(context); + var options = chatScope.options; + if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { - return Text("Error: ${snapshot.error}"); + return CustomSemantics( + identifier: options.semantics.newChatGetUsersError, + value: "Error: ${snapshot.error}", + child: Text("Error: ${snapshot.error}"), + ); } else if (snapshot.hasData) { return UserList( users: snapshot.data!, 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 8b14538..626745f 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 @@ -2,6 +2,7 @@ import "dart:typed_data"; 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/default_image_picker.dart"; import "package:flutter_chat/src/util/scope.dart"; @@ -301,10 +302,15 @@ class _BodyState extends State<_Body> { const SizedBox( height: 16, ), - Text( - "${translations.selectedMembersHeader}" - "${users.length}", - style: theme.textTheme.titleMedium, + CustomSemantics( + identifier: options.semantics.newGroupChatMemberAmount, + value: "${translations.selectedMembersHeader}" + "${users.length}", + child: Text( + "${translations.selectedMembersHeader}" + "${users.length}", + style: theme.textTheme.titleMedium, + ), ), const SizedBox( height: 12, 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 fc0abe0..2f75d95 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 @@ -196,10 +196,17 @@ class _Body extends StatelessWidget { // ignore: discarded_futures stream: service.getAllUsers(), builder: (context, snapshot) { + var chatScope = ChatScope.of(context); + var options = chatScope.options; + if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { - return Text("Error: ${snapshot.error}"); + return Semantics( + identifier: options.semantics.newGroupChatGetUsersError, + value: "Error: ${snapshot.error}", + child: Text("Error: ${snapshot.error}"), + ); } else if (snapshot.hasData) { return Stack( children: [ 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 52eb72a..209dadc 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 @@ -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/util/scope.dart"; import "package:flutter_profile/flutter_profile.dart"; @@ -106,9 +107,14 @@ class _UserListState extends State { const SizedBox( width: 12, ), - Text( - user.fullname ?? translations.anonymousUser, - style: theme.textTheme.titleMedium, + 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(), @@ -154,9 +160,14 @@ class _UserListState extends State { const SizedBox( width: 12, ), - Text( - user.fullname ?? translations.anonymousUser, - style: theme.textTheme.titleMedium, + 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(),