feat: add semantics for text fields

This commit is contained in:
Jacques 2025-02-27 17:25:20 +01:00 committed by FlutterJoey
parent 30fc7b4368
commit b3b8b1828e
6 changed files with 158 additions and 106 deletions

View file

@ -7,9 +7,9 @@
/// Class that holds all the semantic ids for the chat component view and /// Class that holds all the semantic ids for the chat component view and
/// the corresponding userstory /// the corresponding userstory
class ChatSemantics { class ChatSemantics {
/// ChatTranslations constructor where everything is required use this /// ChatSemantics constructor where everything is required use this
/// if you want to be sure to have all translations specified /// if you want to be sure to have all translations specified
/// If you just want the default values use the empty constructor /// If you just want the default values use the standard constructor
/// and optionally override the values with the copyWith method /// and optionally override the values with the copyWith method
const ChatSemantics({ const ChatSemantics({
required this.profileTitle, required this.profileTitle,
@ -28,6 +28,11 @@ class ChatSemantics {
required this.chatsChatSubTitle, required this.chatsChatSubTitle,
required this.chatsChatLastUsed, required this.chatsChatLastUsed,
required this.chatsChatUnreadMessages, required this.chatsChatUnreadMessages,
required this.chatMessageInput,
required this.newChatNameInput,
required this.newChatBioInput,
required this.newChatSearchInput,
required this.newGroupChatSearchInput,
}); });
/// Default translations for the chat component view /// Default translations for the chat component view
@ -48,6 +53,11 @@ class ChatSemantics {
this.chatsChatSubTitle = _defaultChatsChatSubTitle, this.chatsChatSubTitle = _defaultChatsChatSubTitle,
this.chatsChatLastUsed = _defaultChatsChatLastUsed, this.chatsChatLastUsed = _defaultChatsChatLastUsed,
this.chatsChatUnreadMessages = _defaultChatsChatUnreadMessages, this.chatsChatUnreadMessages = _defaultChatsChatUnreadMessages,
this.chatMessageInput = "input_text_message",
this.newChatNameInput = "input_text_name",
this.newChatBioInput = "input_text_bio",
this.newChatSearchInput = "input_text_search",
this.newGroupChatSearchInput = "input_text_search",
}); });
// Text // Text
@ -70,6 +80,13 @@ class ChatSemantics {
final String Function(int index) chatsChatLastUsed; final String Function(int index) chatsChatLastUsed;
final String Function(int index) chatsChatUnreadMessages; final String Function(int index) chatsChatUnreadMessages;
// Input texts
final String chatMessageInput;
final String newChatNameInput;
final String newChatBioInput;
final String newChatSearchInput;
final String newGroupChatSearchInput;
ChatSemantics copyWith({ ChatSemantics copyWith({
String? profileTitle, String? profileTitle,
String? profileDescription, String? profileDescription,
@ -87,6 +104,11 @@ class ChatSemantics {
String Function(int)? chatsChatSubTitle, String Function(int)? chatsChatSubTitle,
String Function(int)? chatsChatLastUsed, String Function(int)? chatsChatLastUsed,
String Function(int)? chatsChatUnreadMessages, String Function(int)? chatsChatUnreadMessages,
String? chatMessageInput,
String? newChatNameInput,
String? newChatBioInput,
String? newChatSearchInput,
String? newGroupChatSearchInput,
}) => }) =>
ChatSemantics( ChatSemantics(
profileTitle: profileTitle ?? this.profileTitle, profileTitle: profileTitle ?? this.profileTitle,
@ -109,6 +131,12 @@ class ChatSemantics {
chatsChatLastUsed: chatsChatLastUsed ?? this.chatsChatLastUsed, chatsChatLastUsed: chatsChatLastUsed ?? this.chatsChatLastUsed,
chatsChatUnreadMessages: chatsChatUnreadMessages:
chatsChatUnreadMessages ?? this.chatsChatUnreadMessages, chatsChatUnreadMessages ?? this.chatsChatUnreadMessages,
chatMessageInput: chatMessageInput ?? this.chatMessageInput,
newChatNameInput: newChatNameInput ?? this.newChatNameInput,
newChatBioInput: newChatBioInput ?? this.newChatBioInput,
newChatSearchInput: newChatSearchInput ?? this.newChatSearchInput,
newGroupChatSearchInput:
newGroupChatSearchInput ?? this.newGroupChatSearchInput,
); );
} }

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/util/scope.dart"; import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
@ -91,47 +92,51 @@ class ChatBottomInputSection extends HookWidget {
var defaultInputField = Stack( var defaultInputField = Stack(
children: [ children: [
TextField( CustomSemantics(
textAlign: TextAlign.start, identifier: options.semantics.chatMessageInput,
textAlignVertical: TextAlignVertical.center, isTextField: true,
style: theme.textTheme.bodySmall, child: TextField(
textCapitalization: TextCapitalization.sentences, textAlign: TextAlign.start,
textInputAction: TextInputAction.newline, textAlignVertical: TextAlignVertical.center,
keyboardType: TextInputType.multiline, style: theme.textTheme.bodySmall,
maxLines: null, textCapitalization: TextCapitalization.sentences,
controller: textController, textInputAction: TextInputAction.newline,
enabled: !isLoading, keyboardType: TextInputType.multiline,
decoration: InputDecoration( maxLines: null,
enabledBorder: OutlineInputBorder( controller: textController,
borderRadius: BorderRadius.circular(25), enabled: !isLoading,
borderSide: const BorderSide(color: Colors.black), decoration: InputDecoration(
), enabledBorder: OutlineInputBorder(
focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25),
borderRadius: BorderRadius.circular(25), borderSide: const BorderSide(color: Colors.black),
borderSide: const BorderSide(color: Colors.black), ),
), focusedBorder: OutlineInputBorder(
contentPadding: const EdgeInsets.only( borderRadius: BorderRadius.circular(25),
left: 16, borderSide: const BorderSide(color: Colors.black),
top: 16, ),
bottom: 16, contentPadding: const EdgeInsets.only(
), left: 16,
// this ensures that that there is space at the end of the textfield top: 16,
suffixIcon: AbsorbPointer( bottom: 16,
child: Opacity( ),
opacity: 0.0, // this ensures that that there is space at the end of the textfield
child: messageSendButtons, suffixIcon: AbsorbPointer(
child: Opacity(
opacity: 0.0,
child: messageSendButtons,
),
),
hintText: options.translations.messagePlaceholder,
hintStyle: theme.textTheme.bodyMedium,
fillColor: Colors.white,
filled: true,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide.none,
), ),
), ),
hintText: options.translations.messagePlaceholder, onSubmitted: (_) async => onSubmitField(),
hintStyle: theme.textTheme.bodyMedium,
fillColor: Colors.white,
filled: true,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide.none,
),
), ),
onSubmitted: (_) async => onSubmitField(),
), ),
Positioned( Positioned(
right: 0, right: 0,

View file

@ -139,6 +139,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
onSearch: onSearch, onSearch: onSearch,
focusNode: focusNode, focusNode: focusNode,
text: options.translations.newChatTitle, text: options.translations.newChatTitle,
semanticId: options.semantics.newChatSearchInput,
), ),
actions: [ actions: [
SearchIcon( SearchIcon(

View file

@ -226,37 +226,41 @@ class _BodyState extends State<_Body> {
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
TextFormField( CustomSemantics(
style: theme.textTheme.bodySmall, identifier: options.semantics.newChatNameInput,
controller: _chatNameController, isTextField: true,
decoration: InputDecoration( child: TextFormField(
fillColor: Colors.white, style: theme.textTheme.bodySmall,
filled: true, controller: _chatNameController,
hintText: translations.groupNameHintText, decoration: InputDecoration(
hintStyle: theme.textTheme.bodyMedium, fillColor: Colors.white,
enabledBorder: OutlineInputBorder( filled: true,
borderRadius: BorderRadius.circular(12), hintText: translations.groupNameHintText,
borderSide: const BorderSide( hintStyle: theme.textTheme.bodyMedium,
color: Colors.transparent, enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
), ),
), ),
focusedBorder: OutlineInputBorder( validator: (value) {
borderRadius: BorderRadius.circular(12), if (value == null || value.isEmpty) {
borderSide: const BorderSide( return translations.groupNameValidatorEmpty;
color: Colors.transparent, }
), if (value.length > 15) {
), return translations.groupNameValidatorTooLong;
), }
validator: (value) {
if (value == null || value.isEmpty) {
return translations.groupNameValidatorEmpty;
}
if (value.length > 15) {
return translations.groupNameValidatorTooLong;
}
return null; return null;
}, },
),
), ),
const SizedBox( const SizedBox(
height: 16, height: 16,
@ -268,36 +272,40 @@ class _BodyState extends State<_Body> {
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
TextFormField( CustomSemantics(
style: theme.textTheme.bodySmall, identifier: options.semantics.newChatBioInput,
controller: _bioController, isTextField: true,
minLines: null, child: TextFormField(
maxLines: 5, style: theme.textTheme.bodySmall,
decoration: InputDecoration( controller: _bioController,
fillColor: Colors.white, minLines: null,
filled: true, maxLines: 5,
hintText: translations.groupBioHintText, decoration: InputDecoration(
hintStyle: theme.textTheme.bodyMedium, fillColor: Colors.white,
enabledBorder: OutlineInputBorder( filled: true,
borderRadius: BorderRadius.circular(12), hintText: translations.groupBioHintText,
borderSide: const BorderSide( hintStyle: theme.textTheme.bodyMedium,
color: Colors.transparent, enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
), ),
), ),
focusedBorder: OutlineInputBorder( validator: (value) {
borderRadius: BorderRadius.circular(12), if (value == null || value.isEmpty) {
borderSide: const BorderSide( return translations.groupBioValidatorEmpty;
color: Colors.transparent, }
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return translations.groupBioValidatorEmpty;
}
return null; return null;
}, },
),
), ),
const SizedBox( const SizedBox(
height: 16, height: 16,

View file

@ -149,6 +149,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
onSearch: onSearch, onSearch: onSearch,
focusNode: focusNode, focusNode: focusNode,
text: options.translations.newGroupChatTitle, text: options.translations.newGroupChatTitle,
semanticId: options.semantics.newGroupChatSearchInput,
), ),
actions: [ actions: [
SearchIcon( SearchIcon(

View file

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/util/scope.dart"; import "package:flutter_chat/src/util/scope.dart";
/// The search field widget /// The search field widget
@ -9,6 +10,7 @@ class SearchField extends StatelessWidget {
required this.onSearch, required this.onSearch,
required this.focusNode, required this.focusNode,
required this.text, required this.text,
required this.semanticId,
super.key, super.key,
}); });
@ -24,6 +26,9 @@ class SearchField extends StatelessWidget {
/// The text to display in the search field /// The text to display in the search field
final String text; final String text;
/// Semantic id for search field
final String semanticId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var chatScope = ChatScope.of(context); var chatScope = ChatScope.of(context);
@ -32,20 +37,24 @@ class SearchField extends StatelessWidget {
var translations = options.translations; var translations = options.translations;
if (isSearching) { if (isSearching) {
return TextField( return CustomSemantics(
focusNode: focusNode, identifier: semanticId,
onChanged: onSearch, isTextField: true,
decoration: InputDecoration( child: TextField(
hintText: translations.searchPlaceholder, focusNode: focusNode,
hintStyle: theme.textTheme.bodyMedium, onChanged: onSearch,
focusedBorder: UnderlineInputBorder( decoration: InputDecoration(
borderSide: BorderSide( hintText: translations.searchPlaceholder,
color: theme.colorScheme.primary, hintStyle: theme.textTheme.bodyMedium,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: theme.colorScheme.primary,
),
), ),
), ),
style: theme.textTheme.bodySmall,
cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white,
), ),
style: theme.textTheme.bodySmall,
cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white,
); );
} }