feat: add semantics for buttons

This commit is contained in:
Jacques 2025-02-27 17:25:20 +01:00 committed by FlutterJoey
parent b3b8b1828e
commit 371ff6c335
15 changed files with 579 additions and 366 deletions

View file

@ -24,6 +24,7 @@
- Added enabled boolean to the messageInputBuilder and made parameters named - 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 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 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 ## 4.0.0
- Move to the new user story architecture - Move to the new user story architecture

View file

@ -33,6 +33,25 @@ class ChatSemantics {
required this.newChatBioInput, required this.newChatBioInput,
required this.newChatSearchInput, required this.newChatSearchInput,
required this.newGroupChatSearchInput, 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 /// Default translations for the chat component view
@ -58,6 +77,25 @@ class ChatSemantics {
this.newChatBioInput = "input_text_bio", this.newChatBioInput = "input_text_bio",
this.newChatSearchInput = "input_text_search", this.newChatSearchInput = "input_text_search",
this.newGroupChatSearchInput = "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 // Text
@ -87,6 +125,33 @@ class ChatSemantics {
final String newChatSearchInput; final String newChatSearchInput;
final String newGroupChatSearchInput; 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({ ChatSemantics copyWith({
String? profileTitle, String? profileTitle,
String? profileDescription, String? profileDescription,
@ -109,6 +174,25 @@ class ChatSemantics {
String? newChatBioInput, String? newChatBioInput,
String? newChatSearchInput, String? newChatSearchInput,
String? newGroupChatSearchInput, 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( ChatSemantics(
profileTitle: profileTitle ?? this.profileTitle, profileTitle: profileTitle ?? this.profileTitle,
@ -137,6 +221,38 @@ class ChatSemantics {
newChatSearchInput: newChatSearchInput ?? this.newChatSearchInput, newChatSearchInput: newChatSearchInput ?? this.newChatSearchInput,
newGroupChatSearchInput: newGroupChatSearchInput:
newGroupChatSearchInput ?? this.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 _defaultChatsChatLastUsed(int index) => "text_chat_last_used_$index";
String _defaultChatsChatUnreadMessages(int index) => String _defaultChatsChatUnreadMessages(int index) =>
"text_chat_unread_messages_$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";

View file

@ -18,6 +18,7 @@ class FlutterChatEntryWidget extends StatefulWidget {
this.counterBackgroundColor = Colors.red, this.counterBackgroundColor = Colors.red,
this.textStyle, this.textStyle,
this.semanticIdUnreadMessages = "text_unread_messages_count", this.semanticIdUnreadMessages = "text_unread_messages_count",
this.semanticIdOpenButton = "button_open_chat",
super.key, super.key,
}); });
@ -51,6 +52,9 @@ class FlutterChatEntryWidget extends StatefulWidget {
/// Semantic Id for the unread messages text /// Semantic Id for the unread messages text
final String semanticIdUnreadMessages; final String semanticIdUnreadMessages;
/// Semantic Id for the unread messages text
final String semanticIdOpenButton;
@override @override
State<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState(); State<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState();
} }
@ -83,61 +87,65 @@ class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
} }
@override @override
Widget build(BuildContext context) => InkWell( Widget build(BuildContext context) => CustomSemantics(
onTap: () async => identifier: widget.semanticIdOpenButton,
widget.onTap?.call() ?? buttonWithVariableText: true,
Navigator.of(context).push( child: InkWell(
MaterialPageRoute( onTap: () async =>
builder: (context) => FlutterChatNavigatorUserstory( widget.onTap?.call() ??
userId: widget.userId, Navigator.of(context).push(
options: widget.options ?? ChatOptions(), MaterialPageRoute(
), builder: (context) => FlutterChatNavigatorUserstory(
), userId: widget.userId,
), options: widget.options ?? ChatOptions(),
child: StreamBuilder<int>(
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,
), ),
notifications: snapshot.data ?? 0,
), ),
), ),
Positioned( child: StreamBuilder<int>(
right: 0.0, stream: chatService.getUnreadMessagesCount(),
top: 0.0, builder: (BuildContext context, snapshot) => Stack(
child: Container( alignment: Alignment.center,
width: widget.widgetSize / 2, children: [
height: widget.widgetSize / 2, Container(
width: widget.widgetSize,
height: widget.widgetSize,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: widget.counterBackgroundColor, color: widget.backgroundColor,
), ),
child: Center( child: _AnimatedNotificationIcon(
child: CustomSemantics( icon: Icon(
identifier: widget.semanticIdUnreadMessages, widget.icon,
value: snapshot.data?.toString() ?? "0", color: widget.iconColor,
child: Text( size: widget.widgetSize / 1.5,
snapshot.data?.toString() ?? "0", ),
style: widget.textStyle, 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,
),
), ),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
); );

View file

@ -3,8 +3,8 @@ import "dart:typed_data";
import "package:chat_repository_interface/chat_repository_interface.dart"; import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.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_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/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_bottom.dart";
import "package:flutter_chat/src/screens/chat_detail/widgets/chat_widgets.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; Widget? appBarIcon;
if (onPressBack != null) { if (onPressBack != null) {
appBarIcon = InkWell( appBarIcon = CustomSemantics(
onTap: onPressBack, identifier: options.semantics.chatBackButton,
child: const Icon(Icons.arrow_back_ios), 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, iconTheme: theme.appBarTheme.iconTheme,
centerTitle: true, centerTitle: true,
leading: appBarIcon, leading: appBarIcon,
title: InkWell( title: CustomSemantics(
splashColor: Colors.transparent, identifier: options.semantics.chatTitleButton,
highlightColor: Colors.transparent, buttonWithVariableText: true,
hoverColor: Colors.transparent, child: InkWell(
onTap: onPressChatTitle, splashColor: Colors.transparent,
child: CustomSemantics( highlightColor: Colors.transparent,
identifier: options.semantics.chatChatTitle, hoverColor: Colors.transparent,
value: chatTitle ?? "", onTap: onPressChatTitle,
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ?? child: CustomSemantics(
Text( identifier: options.semantics.chatChatTitle,
chatTitle ?? "", value: chatTitle ?? "",
overflow: TextOverflow.ellipsis, child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
), Text(
chatTitle ?? "",
overflow: TextOverflow.ellipsis,
),
),
), ),
), ),
); );

View file

@ -69,20 +69,26 @@ class ChatBottomInputSection extends HookWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( CustomSemantics(
alignment: Alignment.bottomRight, identifier: options.semantics.chatSelectImageIconButton,
onPressed: isLoading ? null : onPressSelectImage, child: IconButton(
icon: Icon( alignment: Alignment.bottomRight,
Icons.image_outlined, onPressed: isLoading ? null : onPressSelectImage,
color: options.iconEnabledColor, icon: Icon(
Icons.image_outlined,
color: options.iconEnabledColor,
),
), ),
), ),
IconButton( CustomSemantics(
alignment: Alignment.bottomRight, identifier: options.semantics.chatSendMessageIconButton,
disabledColor: options.iconDisabledColor, child: IconButton(
color: options.iconEnabledColor, alignment: Alignment.bottomRight,
onPressed: isLoading ? null : onClickSendMessage, disabledColor: options.iconDisabledColor,
icon: const Icon(Icons.send_rounded), color: options.iconEnabledColor,
onPressed: isLoading ? null : onClickSendMessage,
icon: const Icon(Icons.send_rounded),
),
), ),
], ],
), ),
@ -119,7 +125,8 @@ class ChatBottomInputSection extends HookWidget {
top: 16, top: 16,
bottom: 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( suffixIcon: AbsorbPointer(
child: Opacity( child: Opacity(
opacity: 0.0, opacity: 0.0,

View file

@ -297,7 +297,7 @@ class _DefaultChatImage extends StatelessWidget {
options.imageProviderResolver(context, Uri.parse(imageUrl)), options.imageProviderResolver(context, Uri.parse(imageUrl)),
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
errorBuilder: (context, error, stackTrace) => Text( errorBuilder: (context, error, stackTrace) => Text(
// TODO: Non-replaceable text // TODO(Jacques): Non-replaceable text
"Something went wrong with loading the image", "Something went wrong with loading the image",
style: textTheme.bodyLarge?.copyWith( style: textTheme.bodyLarge?.copyWith(
color: messageTheme.textColor, color: messageTheme.textColor,

View file

@ -130,55 +130,63 @@ class _Body extends StatelessWidget {
var chatUserDisplay = Wrap( var chatUserDisplay = Wrap(
children: [ children: [
if (chat != null) ...[ if (chat != null) ...[
...chat!.users.map( ...chat!.users.asMap().entries.map(
(tappedUser) => Padding( (entry) {
padding: const EdgeInsets.only( var index = entry.key;
bottom: 8, var tappedUser = entry.value;
right: 8,
),
child: InkWell(
onTap: () => onTapUser?.call(tappedUser),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FutureBuilder<UserModel>(
future: service.getUser(userId: tappedUser).first,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
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<UserModel>(
future: service.getUser(userId: tappedUser).first,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (user == null) { var user = snapshot.data;
return const SizedBox.shrink();
}
return options.builders.userAvatarBuilder?.call( if (user == null) {
context, return const SizedBox.shrink();
user, }
44,
) ?? return options.builders.userAvatarBuilder?.call(
Avatar( context,
boxfit: BoxFit.cover, user,
user: User( 44,
firstName: user.firstName, ) ??
lastName: user.lastName, Avatar(
imageUrl: boxfit: BoxFit.cover,
user.imageUrl != null || user.imageUrl != "" user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != null ||
user.imageUrl != ""
? user.imageUrl ? user.imageUrl
: null, : null,
), ),
size: 60, size: 60,
); );
}, },
),
],
), ),
], ),
), ),
), );
), },
), ),
], ],
], ],
@ -286,18 +294,21 @@ class _Body extends StatelessWidget {
vertical: 24, vertical: 24,
horizontal: 80, horizontal: 80,
), ),
child: FilledButton( child: CustomSemantics(
onPressed: () { identifier: options.semantics.profileStartChatButton,
onPressStartChat?.call(user!.id); child: FilledButton(
}, onPressed: () {
child: Row( onPressStartChat?.call(user!.id);
mainAxisAlignment: MainAxisAlignment.center, },
children: [ child: Row(
Text( mainAxisAlignment: MainAxisAlignment.center,
options.translations.newChatButton, children: [
style: theme.textTheme.displayLarge, Text(
), options.translations.newChatButton,
], style: theme.textTheme.displayLarge,
),
],
),
), ),
), ),
), ),

View file

@ -197,6 +197,8 @@ class _BodyState extends State<_Body> {
semantics.chatsChatLastUsed(index), semantics.chatsChatLastUsed(index),
semanticIdUnreadMessages: semanticIdUnreadMessages:
semantics.chatsChatUnreadMessages(index), semantics.chatsChatUnreadMessages(index),
semanticIdButton:
semantics.chatsOpenChatButton(index),
); );
return !chat.canBeDeleted return !chat.canBeDeleted
@ -275,18 +277,21 @@ class _BodyState extends State<_Body> {
vertical: 24, vertical: 24,
horizontal: 4, horizontal: 4,
), ),
child: ElevatedButton( child: CustomSemantics(
style: ElevatedButton.styleFrom( identifier: options.semantics.chatsStartChatButton,
backgroundColor: theme.colorScheme.primary, child: ElevatedButton(
fixedSize: const Size(254, 44), style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder( backgroundColor: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(56), 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.semanticIdSubTitle,
required this.semanticIdLastUsed, required this.semanticIdLastUsed,
required this.semanticIdUnreadMessages, required this.semanticIdUnreadMessages,
required this.semanticIdButton,
}); });
final ChatModel chat; final ChatModel chat;
@ -311,6 +317,7 @@ class _ChatItem extends StatelessWidget {
final String semanticIdSubTitle; final String semanticIdSubTitle;
final String semanticIdLastUsed; final String semanticIdLastUsed;
final String semanticIdUnreadMessages; final String semanticIdUnreadMessages;
final String semanticIdButton;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -330,29 +337,33 @@ class _ChatItem extends StatelessWidget {
semanticIdUnreadMessages: semanticIdUnreadMessages, semanticIdUnreadMessages: semanticIdUnreadMessages,
); );
return InkWell( return CustomSemantics(
onTap: () { identifier: semanticIdButton,
onPressChat(chat); buttonWithVariableText: true,
}, child: InkWell(
child: options.builders.chatRowContainerBuilder?.call( onTap: () {
context, onPressChat(chat);
chatListItem, },
) ?? child: options.builders.chatRowContainerBuilder?.call(
DecoratedBox( context,
decoration: BoxDecoration( chatListItem,
color: Colors.transparent, ) ??
border: Border( DecoratedBox(
bottom: BorderSide( decoration: BoxDecoration(
color: theme.dividerColor, color: Colors.transparent,
width: 0.5, 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<bool?> _deleteDialog(
) async { ) async {
var theme = Theme.of(context); var theme = Theme.of(context);
var scope = ChatScope.of(context);
var options = scope.options;
return showModalBottomSheet<bool>( return showModalBottomSheet<bool>(
context: context, context: context,
builder: (BuildContext context) => Container( builder: (BuildContext context) => Container(
@ -555,20 +570,23 @@ Future<bool?> _deleteDialog(
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 60), padding: const EdgeInsets.symmetric(horizontal: 60),
child: FilledButton( child: CustomSemantics(
onPressed: () { identifier: options.semantics.chatsDeleteConfirmButton,
Navigator.of( child: FilledButton(
context, onPressed: () {
).pop(true); Navigator.of(
}, context,
child: Row( ).pop(true);
mainAxisAlignment: MainAxisAlignment.center, },
children: [ child: Row(
Text( mainAxisAlignment: MainAxisAlignment.center,
translations.deleteChatModalConfirm, children: [
style: theme.textTheme.displayLarge, Text(
), translations.deleteChatModalConfirm,
], style: theme.textTheme.displayLarge,
),
],
),
), ),
), ),
), ),

View file

@ -145,6 +145,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
SearchIcon( SearchIcon(
isSearching: isSearching, isSearching: isSearching,
onPressed: onPressedSearchIcon, onPressed: onPressedSearchIcon,
semanticId: options.semantics.newChatSearchIconButton,
), ),
], ],
); );
@ -187,23 +188,26 @@ class _Body extends StatelessWidget {
right: 32, right: 32,
top: 20, top: 20,
), ),
child: FilledButton( child: CustomSemantics(
onPressed: onPressCreateGroupChat, identifier: options.semantics.newChatCreateGroupChatButton,
child: Row( child: FilledButton(
mainAxisAlignment: MainAxisAlignment.center, onPressed: onPressCreateGroupChat,
crossAxisAlignment: CrossAxisAlignment.center, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.center,
const Icon( crossAxisAlignment: CrossAxisAlignment.center,
Icons.groups_2, children: [
), const Icon(
const SizedBox( Icons.groups_2,
width: 8, ),
), const SizedBox(
Text( width: 8,
translations.newGroupChatButton, ),
style: theme.textTheme.displayLarge, Text(
), translations.newGroupChatButton,
], style: theme.textTheme.displayLarge,
),
],
),
), ),
), ),
), ),

View file

@ -158,31 +158,35 @@ class _BodyState extends State<_Body> {
Center( Center(
child: Stack( child: Stack(
children: [ children: [
InkWell( CustomSemantics(
onTap: () async => onPressSelectImage( identifier: options.semantics.newGroupChatSelectImage,
context, child: InkWell(
options, onTap: () async => onPressSelectImage(
(image) { context,
setState(() { options,
this.image = image; (image) {
}); setState(() {
}, this.image = image;
), });
child: Container( },
width: 80, ),
height: 80, child: Container(
decoration: BoxDecoration( width: 80,
color: const Color(0xFFD9D9D9), height: 80,
borderRadius: BorderRadius.circular(40), decoration: BoxDecoration(
image: image != null color: const Color(0xFFD9D9D9),
? DecorationImage( borderRadius: BorderRadius.circular(40),
image: MemoryImage(image!), image: image != null
fit: BoxFit.cover, ? DecorationImage(
) image: MemoryImage(image!),
fit: BoxFit.cover,
)
: null,
),
child: image == null
? const Icon(Icons.image)
: null, : null,
), ),
child:
image == null ? const Icon(Icons.image) : null,
), ),
), ),
if (image != null) if (image != null)
@ -197,15 +201,19 @@ class _BodyState extends State<_Body> {
borderRadius: BorderRadius.circular(40), borderRadius: BorderRadius.circular(40),
), ),
child: Center( child: Center(
child: InkWell( child: CustomSemantics(
onTap: () { identifier:
setState(() { options.semantics.newGroupChatRemoveImage,
image = null; child: InkWell(
}); onTap: () {
}, setState(() {
child: const Icon( image = null;
Icons.close, });
size: 12, },
child: const Icon(
Icons.close,
size: 12,
),
), ),
), ),
), ),
@ -354,31 +362,34 @@ class _BodyState extends State<_Body> {
), ),
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: isButtonEnabled, valueListenable: isButtonEnabled,
builder: (context, isEnabled, child) => FilledButton( builder: (context, isEnabled, child) => CustomSemantics(
onPressed: users.isNotEmpty identifier: "",
? () async { child: FilledButton(
if (!isPressed) { onPressed: users.isNotEmpty
isPressed = true; ? () async {
if (formKey.currentState!.validate()) { if (!isPressed) {
await widget.onComplete( isPressed = true;
users, if (formKey.currentState!.validate()) {
_chatNameController.text, await widget.onComplete(
_bioController.text, users,
image, _chatNameController.text,
); _bioController.text,
image,
);
}
isPressed = false;
} }
isPressed = false;
} }
} : null,
: null, child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
children: [ Text(
Text( translations.createGroupChatButton,
translations.createGroupChatButton, style: theme.textTheme.displayLarge,
style: theme.textTheme.displayLarge, ),
), ],
], ),
), ),
), ),
), ),
@ -402,38 +413,41 @@ class _SelectedUser extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var chatScope = ChatScope.of(context); var chatScope = ChatScope.of(context);
var options = chatScope.options; var options = chatScope.options;
return InkWell( return CustomSemantics(
onTap: () { identifier: options.semantics.newGroupChatRemoveUser,
onRemove(user); child: InkWell(
}, onTap: () {
child: Stack( onRemove(user);
children: [ },
Padding( child: Stack(
padding: const EdgeInsets.all(8), children: [
child: options.builders.userAvatarBuilder?.call( Padding(
context, padding: const EdgeInsets.all(8),
user, child: options.builders.userAvatarBuilder?.call(
40, context,
) ?? user,
Avatar( 40,
boxfit: BoxFit.cover, ) ??
user: User( Avatar(
firstName: user.firstName, boxfit: BoxFit.cover,
lastName: user.lastName, user: User(
imageUrl: user.imageUrl != "" ? user.imageUrl : null, 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,
),
),
],
),
), ),
); );
} }

View file

@ -1,5 +1,6 @@
import "package:chat_repository_interface/chat_repository_interface.dart"; import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.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/config/screen_types.dart";
import "package:flutter_chat/src/screens/creation/widgets/search_field.dart"; import "package:flutter_chat/src/screens/creation/widgets/search_field.dart";
import "package:flutter_chat/src/screens/creation/widgets/search_icon.dart"; import "package:flutter_chat/src/screens/creation/widgets/search_icon.dart";
@ -155,6 +156,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
SearchIcon( SearchIcon(
isSearching: isSearching, isSearching: isSearching,
onPressed: onPressedSearchIcon, onPressed: onPressedSearchIcon,
semanticId: options.semantics.newGroupChatSearchIconButton,
), ),
], ],
); );
@ -272,18 +274,21 @@ class _NextButton extends StatelessWidget {
), ),
child: Visibility( child: Visibility(
visible: selectedUsers.isNotEmpty, visible: selectedUsers.isNotEmpty,
child: FilledButton( child: CustomSemantics(
onPressed: () async { identifier: options.semantics.newGroupChatNextButton,
await onPressGroupChatOverview(selectedUsers); child: FilledButton(
}, onPressed: () async {
child: Row( await onPressGroupChatOverview(selectedUsers);
mainAxisAlignment: MainAxisAlignment.center, },
children: [ child: Row(
Text( mainAxisAlignment: MainAxisAlignment.center,
options.translations.next, children: [
style: theme.textTheme.displayLarge, Text(
), options.translations.next,
], style: theme.textTheme.displayLarge,
),
],
),
), ),
), ),
), ),

View file

@ -1,6 +1,7 @@
import "dart:typed_data"; import "dart:typed_data";
import "package:flutter/material.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/config/chat_options.dart";
import "package:flutter_chat/src/config/chat_translations.dart"; import "package:flutter_chat/src/config/chat_translations.dart";
import "package:flutter_chat/src/util/scope.dart"; import "package:flutter_chat/src/util/scope.dart";
@ -71,13 +72,16 @@ class DefaultImagePickerDialog extends StatelessWidget {
Icons.insert_drive_file_rounded, Icons.insert_drive_file_rounded,
size: 60, size: 60,
), ),
closeButtonBuilder: (ontap) => TextButton( closeButtonBuilder: (ontap) => CustomSemantics(
onPressed: () => Navigator.of(context).pop(), identifier: options.semantics.imagePickerCancelButton,
child: Text( child: TextButton(
translations.cancelImagePickerBtn, onPressed: () => Navigator.of(context).pop(),
style: textTheme.bodyMedium!.copyWith( child: Text(
fontSize: 18, translations.cancelImagePickerBtn,
decoration: TextDecoration.underline, style: textTheme.bodyMedium!.copyWith(
fontSize: 18,
decoration: TextDecoration.underline,
),
), ),
), ),
), ),

View file

@ -6,6 +6,7 @@ class SearchIcon extends StatelessWidget {
const SearchIcon({ const SearchIcon({
required this.isSearching, required this.isSearching,
required this.onPressed, required this.onPressed,
required this.semanticId,
super.key, super.key,
}); });
@ -15,14 +16,20 @@ class SearchIcon extends StatelessWidget {
/// Callback function triggered when the search icon is pressed /// Callback function triggered when the search icon is pressed
final VoidCallback onPressed; final VoidCallback onPressed;
/// Semantic id for icon button
final String semanticId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return IconButton( return Semantics(
onPressed: onPressed, identifier: semanticId,
icon: Icon( child: IconButton(
isSearching ? Icons.close : Icons.search, onPressed: onPressed,
color: theme.appBarTheme.iconTheme?.color ?? Colors.white, icon: Icon(
isSearching ? Icons.close : Icons.search,
color: theme.appBarTheme.iconTheme?.color ?? Colors.white,
),
), ),
); );
} }

View file

@ -80,70 +80,20 @@ class _UserListState extends State<UserList> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
var user = filteredUsers[index]; var user = filteredUsers[index];
var isSelected = widget.selectedUsers.any((u) => u.id == user.id); var isSelected = widget.selectedUsers.any((u) => u.id == user.id);
return InkWell( return CustomSemantics(
onTap: () async { identifier: options.semantics.userListTapUser(index),
if (widget.creatingGroup) { buttonWithVariableText: true,
return handleGroupChatTap(user); child: InkWell(
} else { onTap: () async {
return handlePersonalChatTap(user); if (widget.creatingGroup) {
} return handleGroupChatTap(user);
}, } else {
child: options.builders.chatRowContainerBuilder?.call( return handlePersonalChatTap(user);
context, }
Row( },
children: [ child: options.builders.chatRowContainerBuilder?.call(
options.builders.userAvatarBuilder context,
?.call(context, user, 44) ?? Row(
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(
children: [ children: [
options.builders.userAvatarBuilder options.builders.userAvatarBuilder
?.call(context, user, 44) ?? ?.call(context, user, 44) ??
@ -183,8 +133,63 @@ class _UserListState extends State<UserList> {
], ],
], ],
), ),
) ??
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,
),
],
],
),
),
), ),
), ),
); );
}, },
), ),

View file

@ -15,6 +15,9 @@ dependencies:
intl: any intl: any
flutter_hooks: ^0.20.5 flutter_hooks: ^0.20.5
flutter_accessibility:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^0.0.2
flutter_image_picker: flutter_image_picker:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^4.0.0 version: ^4.0.0