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 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

View file

@ -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";

View file

@ -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<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState();
}
@ -83,7 +87,10 @@ class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
}
@override
Widget build(BuildContext context) => InkWell(
Widget build(BuildContext context) => CustomSemantics(
identifier: widget.semanticIdOpenButton,
buttonWithVariableText: true,
child: InkWell(
onTap: () async =>
widget.onTap?.call() ??
Navigator.of(context).push(
@ -140,6 +147,7 @@ class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
],
),
),
),
);
}

View file

@ -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(
appBarIcon = CustomSemantics(
identifier: options.semantics.chatBackButton,
child: InkWell(
onTap: onPressBack,
child: const Icon(Icons.arrow_back_ios),
),
);
}
@ -209,7 +212,10 @@ class _ChatAppBar extends StatelessWidget implements PreferredSizeWidget {
iconTheme: theme.appBarTheme.iconTheme,
centerTitle: true,
leading: appBarIcon,
title: InkWell(
title: CustomSemantics(
identifier: options.semantics.chatTitleButton,
buttonWithVariableText: true,
child: InkWell(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
@ -224,6 +230,7 @@ class _ChatAppBar extends StatelessWidget implements PreferredSizeWidget {
),
),
),
),
);
}

View file

@ -69,7 +69,9 @@ class ChatBottomInputSection extends HookWidget {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
CustomSemantics(
identifier: options.semantics.chatSelectImageIconButton,
child: IconButton(
alignment: Alignment.bottomRight,
onPressed: isLoading ? null : onPressSelectImage,
icon: Icon(
@ -77,13 +79,17 @@ class ChatBottomInputSection extends HookWidget {
color: options.iconEnabledColor,
),
),
IconButton(
),
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,

View file

@ -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,

View file

@ -130,12 +130,18 @@ class _Body extends StatelessWidget {
var chatUserDisplay = Wrap(
children: [
if (chat != null) ...[
...chat!.users.map(
(tappedUser) => Padding(
...chat!.users.asMap().entries.map(
(entry) {
var index = entry.key;
var tappedUser = entry.value;
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(
@ -166,8 +172,8 @@ class _Body extends StatelessWidget {
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl:
user.imageUrl != null || user.imageUrl != ""
imageUrl: user.imageUrl != null ||
user.imageUrl != ""
? user.imageUrl
: null,
),
@ -179,6 +185,8 @@ class _Body extends StatelessWidget {
),
),
),
);
},
),
],
],
@ -286,6 +294,8 @@ class _Body extends StatelessWidget {
vertical: 24,
horizontal: 80,
),
child: CustomSemantics(
identifier: options.semantics.profileStartChatButton,
child: FilledButton(
onPressed: () {
onPressStartChat?.call(user!.id);
@ -302,6 +312,7 @@ class _Body extends StatelessWidget {
),
),
),
),
],
],
);

View file

@ -197,6 +197,8 @@ class _BodyState extends State<_Body> {
semantics.chatsChatLastUsed(index),
semanticIdUnreadMessages:
semantics.chatsChatUnreadMessages(index),
semanticIdButton:
semantics.chatsOpenChatButton(index),
);
return !chat.canBeDeleted
@ -275,6 +277,8 @@ class _BodyState extends State<_Body> {
vertical: 24,
horizontal: 4,
),
child: CustomSemantics(
identifier: options.semantics.chatsStartChatButton,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
@ -290,6 +294,7 @@ class _BodyState extends State<_Body> {
),
),
),
),
],
);
}
@ -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,7 +337,10 @@ class _ChatItem extends StatelessWidget {
semanticIdUnreadMessages: semanticIdUnreadMessages,
);
return InkWell(
return CustomSemantics(
identifier: semanticIdButton,
buttonWithVariableText: true,
child: InkWell(
onTap: () {
onPressChat(chat);
},
@ -353,6 +363,7 @@ class _ChatItem extends StatelessWidget {
child: chatListItem,
),
),
),
);
}
}
@ -530,6 +541,10 @@ Future<bool?> _deleteDialog(
) async {
var theme = Theme.of(context);
var scope = ChatScope.of(context);
var options = scope.options;
return showModalBottomSheet<bool>(
context: context,
builder: (BuildContext context) => Container(
@ -555,6 +570,8 @@ Future<bool?> _deleteDialog(
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: CustomSemantics(
identifier: options.semantics.chatsDeleteConfirmButton,
child: FilledButton(
onPressed: () {
Navigator.of(
@ -572,6 +589,7 @@ Future<bool?> _deleteDialog(
),
),
),
),
],
),
),

View file

@ -145,6 +145,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
SearchIcon(
isSearching: isSearching,
onPressed: onPressedSearchIcon,
semanticId: options.semantics.newChatSearchIconButton,
),
],
);
@ -187,6 +188,8 @@ class _Body extends StatelessWidget {
right: 32,
top: 20,
),
child: CustomSemantics(
identifier: options.semantics.newChatCreateGroupChatButton,
child: FilledButton(
onPressed: onPressCreateGroupChat,
child: Row(
@ -207,6 +210,7 @@ class _Body extends StatelessWidget {
),
),
),
),
],
Expanded(
child: StreamBuilder<List<UserModel>>(

View file

@ -158,7 +158,9 @@ class _BodyState extends State<_Body> {
Center(
child: Stack(
children: [
InkWell(
CustomSemantics(
identifier: options.semantics.newGroupChatSelectImage,
child: InkWell(
onTap: () async => onPressSelectImage(
context,
options,
@ -181,8 +183,10 @@ class _BodyState extends State<_Body> {
)
: null,
),
child:
image == null ? const Icon(Icons.image) : null,
child: image == null
? const Icon(Icons.image)
: null,
),
),
),
if (image != null)
@ -197,6 +201,9 @@ class _BodyState extends State<_Body> {
borderRadius: BorderRadius.circular(40),
),
child: Center(
child: CustomSemantics(
identifier:
options.semantics.newGroupChatRemoveImage,
child: InkWell(
onTap: () {
setState(() {
@ -210,6 +217,7 @@ class _BodyState extends State<_Body> {
),
),
),
),
)
else
const SizedBox.shrink(),
@ -354,7 +362,9 @@ class _BodyState extends State<_Body> {
),
child: ValueListenableBuilder(
valueListenable: isButtonEnabled,
builder: (context, isEnabled, child) => FilledButton(
builder: (context, isEnabled, child) => CustomSemantics(
identifier: "",
child: FilledButton(
onPressed: users.isNotEmpty
? () async {
if (!isPressed) {
@ -384,6 +394,7 @@ class _BodyState extends State<_Body> {
),
),
),
),
],
);
}
@ -402,7 +413,9 @@ class _SelectedUser extends StatelessWidget {
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
return InkWell(
return CustomSemantics(
identifier: options.semantics.newGroupChatRemoveUser,
child: InkWell(
onTap: () {
onRemove(user);
},
@ -435,6 +448,7 @@ class _SelectedUser extends StatelessWidget {
),
],
),
),
);
}
}

View file

@ -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,6 +274,8 @@ class _NextButton extends StatelessWidget {
),
child: Visibility(
visible: selectedUsers.isNotEmpty,
child: CustomSemantics(
identifier: options.semantics.newGroupChatNextButton,
child: FilledButton(
onPressed: () async {
await onPressGroupChatOverview(selectedUsers);
@ -288,6 +292,7 @@ class _NextButton extends StatelessWidget {
),
),
),
),
);
}
}

View file

@ -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,7 +72,9 @@ class DefaultImagePickerDialog extends StatelessWidget {
Icons.insert_drive_file_rounded,
size: 60,
),
closeButtonBuilder: (ontap) => TextButton(
closeButtonBuilder: (ontap) => CustomSemantics(
identifier: options.semantics.imagePickerCancelButton,
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
translations.cancelImagePickerBtn,
@ -83,6 +86,7 @@ class DefaultImagePickerDialog extends StatelessWidget {
),
),
),
),
);
}
}

View file

@ -6,6 +6,7 @@ class SearchIcon extends StatelessWidget {
const SearchIcon({
required this.isSearching,
required this.onPressed,
required this.semanticId,
super.key,
});
@ -15,15 +16,21 @@ 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(
return Semantics(
identifier: semanticId,
child: IconButton(
onPressed: onPressed,
icon: Icon(
isSearching ? Icons.close : Icons.search,
color: theme.appBarTheme.iconTheme?.color ?? Colors.white,
),
),
);
}
}

View file

@ -80,7 +80,10 @@ class _UserListState extends State<UserList> {
itemBuilder: (context, index) {
var user = filteredUsers[index];
var isSelected = widget.selectedUsers.any((u) => u.id == user.id);
return InkWell(
return CustomSemantics(
identifier: options.semantics.userListTapUser(index),
buttonWithVariableText: true,
child: InkWell(
onTap: () async {
if (widget.creatingGroup) {
return handleGroupChatTap(user);
@ -152,8 +155,9 @@ class _UserListState extends State<UserList> {
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl:
user.imageUrl != "" ? user.imageUrl : null,
imageUrl: user.imageUrl != ""
? user.imageUrl
: null,
),
size: 44,
),
@ -185,6 +189,7 @@ class _UserListState extends State<UserList> {
),
),
),
),
);
},
),

View file

@ -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