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
/// the corresponding userstory
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 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
const ChatSemantics({
required this.profileTitle,
@ -28,6 +28,11 @@ class ChatSemantics {
required this.chatsChatSubTitle,
required this.chatsChatLastUsed,
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
@ -48,6 +53,11 @@ class ChatSemantics {
this.chatsChatSubTitle = _defaultChatsChatSubTitle,
this.chatsChatLastUsed = _defaultChatsChatLastUsed,
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
@ -70,6 +80,13 @@ class ChatSemantics {
final String Function(int index) chatsChatLastUsed;
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({
String? profileTitle,
String? profileDescription,
@ -87,6 +104,11 @@ class ChatSemantics {
String Function(int)? chatsChatSubTitle,
String Function(int)? chatsChatLastUsed,
String Function(int)? chatsChatUnreadMessages,
String? chatMessageInput,
String? newChatNameInput,
String? newChatBioInput,
String? newChatSearchInput,
String? newGroupChatSearchInput,
}) =>
ChatSemantics(
profileTitle: profileTitle ?? this.profileTitle,
@ -109,6 +131,12 @@ class ChatSemantics {
chatsChatLastUsed: chatsChatLastUsed ?? this.chatsChatLastUsed,
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:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart";
@ -91,47 +92,51 @@ class ChatBottomInputSection extends HookWidget {
var defaultInputField = Stack(
children: [
TextField(
textAlign: TextAlign.start,
textAlignVertical: TextAlignVertical.center,
style: theme.textTheme.bodySmall,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
maxLines: null,
controller: textController,
enabled: !isLoading,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide(color: Colors.black),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide(color: Colors.black),
),
contentPadding: const EdgeInsets.only(
left: 16,
top: 16,
bottom: 16,
),
// this ensures that that there is space at the end of the textfield
suffixIcon: AbsorbPointer(
child: Opacity(
opacity: 0.0,
child: messageSendButtons,
CustomSemantics(
identifier: options.semantics.chatMessageInput,
isTextField: true,
child: TextField(
textAlign: TextAlign.start,
textAlignVertical: TextAlignVertical.center,
style: theme.textTheme.bodySmall,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
maxLines: null,
controller: textController,
enabled: !isLoading,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide(color: Colors.black),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide(color: Colors.black),
),
contentPadding: const EdgeInsets.only(
left: 16,
top: 16,
bottom: 16,
),
// this ensures that that there is space at the end of the textfield
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,
hintStyle: theme.textTheme.bodyMedium,
fillColor: Colors.white,
filled: true,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide.none,
),
onSubmitted: (_) async => onSubmitField(),
),
onSubmitted: (_) async => onSubmitField(),
),
Positioned(
right: 0,

View file

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

View file

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

View file

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

View file

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