Skip to content

Commit 3273eaf

Browse files
fombalanggnprice
authored andcommitted
autocomplete: Implement new design for @-mention autocomplete items
Implemented new design for @-mention autocomplete items. Added new `contextMenuItemLabel` and `contextMenuItemMeta` color variables to `designVariables` class. Updated autocomplete tests. [greg: rebased atop _EmojiAutocompleteItem; added TODO items there] Fixes: zulip#913
1 parent f0a75d9 commit 3273eaf

File tree

3 files changed

+73
-21
lines changed

3 files changed

+73
-21
lines changed

lib/widgets/autocomplete.dart

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import '../model/autocomplete.dart';
88
import '../model/compose.dart';
99
import '../model/narrow.dart';
1010
import 'compose_box.dart';
11+
import 'text.dart';
12+
import 'theme.dart';
1113

1214
abstract class AutocompleteField<QueryT extends AutocompleteQuery, ResultT extends AutocompleteResult> extends StatefulWidget {
1315
const AutocompleteField({
@@ -210,6 +212,7 @@ class ComposeAutocomplete extends AutocompleteField<ComposeAutocompleteQuery, Co
210212

211213
@override
212214
Widget buildItem(BuildContext context, int index, ComposeAutocompleteResult option) {
215+
final designVariables = DesignVariables.of(context);
213216
final child = switch (option) {
214217
MentionAutocompleteResult() => _MentionAutocompleteItem(option: option),
215218
EmojiAutocompleteResult() => _EmojiAutocompleteItem(option: option),
@@ -218,6 +221,9 @@ class ComposeAutocomplete extends AutocompleteField<ComposeAutocompleteQuery, Co
218221
onTap: () {
219222
_onTapOption(context, option);
220223
},
224+
highlightColor: designVariables.editorButtonPressedBg,
225+
splashFactory: NoSplash.splashFactory,
226+
borderRadius: BorderRadius.circular(5),
221227
child: child);
222228
}
223229
}
@@ -229,20 +235,49 @@ class _MentionAutocompleteItem extends StatelessWidget {
229235

230236
@override
231237
Widget build(BuildContext context) {
238+
final designVariables = DesignVariables.of(context);
239+
232240
Widget avatar;
233241
String label;
242+
String? subLabel;
234243
switch (option) {
235244
case UserMentionAutocompleteResult(:var userId):
236-
avatar = Avatar(userId: userId, size: 32, borderRadius: 3);
237-
label = PerAccountStoreWidget.of(context).users[userId]!.fullName;
245+
final store = PerAccountStoreWidget.of(context);
246+
final user = store.users[userId]!;
247+
avatar = Avatar(userId: userId, size: 36, borderRadius: 4);
248+
label = user.fullName;
249+
subLabel = store.userDisplayEmail(user, store: store);
238250
}
239251

240252
return Padding(
241-
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
253+
padding: const EdgeInsetsDirectional.fromSTEB(4, 4, 8, 4),
242254
child: Row(children: [
243255
avatar,
244-
const SizedBox(width: 8),
245-
Text(label),
256+
const SizedBox(width: 6),
257+
Expanded(child: Column(
258+
mainAxisSize: MainAxisSize.min,
259+
crossAxisAlignment: CrossAxisAlignment.start,
260+
children: [
261+
Text(
262+
style: TextStyle(
263+
fontSize: 18,
264+
height: 20 / 18,
265+
color: designVariables.contextMenuItemLabel,
266+
)
267+
.merge(weightVariableTextStyle(context, wght: 600)),
268+
overflow: TextOverflow.ellipsis,
269+
maxLines: 1,
270+
label),
271+
if (subLabel != null) Text(
272+
style: TextStyle(
273+
fontSize: 14,
274+
height: 16 / 14,
275+
color: designVariables.contextMenuItemMeta,
276+
),
277+
overflow: TextOverflow.ellipsis,
278+
maxLines: 1,
279+
subLabel),
280+
])),
246281
]));
247282
}
248283
}
@@ -252,6 +287,7 @@ class _EmojiAutocompleteItem extends StatelessWidget {
252287

253288
final EmojiAutocompleteResult option;
254289

290+
// TODO adjust sizes to match _MentionAutocompleteItem avatar
255291
static const _size = 32.0;
256292
static const _notoColorEmojiTextSize = 25.7;
257293

@@ -276,12 +312,13 @@ class _EmojiAutocompleteItem extends StatelessWidget {
276312
: [candidate.emojiName, ...candidate.aliases].join(", "); // TODO(#1080)
277313

278314
return Padding(
279-
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
315+
padding: const EdgeInsetsDirectional.fromSTEB(4, 4, 8, 4),
280316
child: Row(children: [
281317
if (glyph != null) ...[
282318
glyph,
283-
const SizedBox(width: 8),
319+
const SizedBox(width: 6),
284320
],
321+
// TODO adjust text style to match _MentionAutocompleteItem
285322
Expanded(
286323
child: Text(
287324
maxLines: 2,

lib/widgets/theme.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
121121
composeBoxBg: const Color(0xffffffff),
122122
contextMenuCancelText: const Color(0xff222222),
123123
contextMenuItemBg: const Color(0xff6159e1),
124+
contextMenuItemLabel: const Color(0xff242631),
125+
contextMenuItemMeta: const Color(0xff626573),
124126
contextMenuItemText: const Color(0xff381da7),
125127
editorButtonPressedBg: Colors.black.withValues(alpha: 0.06),
126128
foreground: const Color(0xff000000),
@@ -161,6 +163,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
161163
composeBoxBg: const Color(0xff0f0f0f),
162164
contextMenuCancelText: const Color(0xffffffff).withValues(alpha: 0.75),
163165
contextMenuItemBg: const Color(0xff7977fe),
166+
contextMenuItemLabel: const Color(0xffdfe1e8),
167+
contextMenuItemMeta: const Color(0xff9194a3),
164168
contextMenuItemText: const Color(0xff9398fd),
165169
editorButtonPressedBg: Colors.white.withValues(alpha: 0.06),
166170
foreground: const Color(0xffffffff),
@@ -208,6 +212,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
208212
required this.composeBoxBg,
209213
required this.contextMenuCancelText,
210214
required this.contextMenuItemBg,
215+
required this.contextMenuItemLabel,
216+
required this.contextMenuItemMeta,
211217
required this.contextMenuItemText,
212218
required this.editorButtonPressedBg,
213219
required this.foreground,
@@ -256,6 +262,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
256262
final Color composeBoxBg;
257263
final Color contextMenuCancelText;
258264
final Color contextMenuItemBg;
265+
final Color contextMenuItemLabel;
266+
final Color contextMenuItemMeta;
259267
final Color contextMenuItemText;
260268
final Color editorButtonPressedBg;
261269
final Color foreground;
@@ -299,6 +307,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
299307
Color? composeBoxBg,
300308
Color? contextMenuCancelText,
301309
Color? contextMenuItemBg,
310+
Color? contextMenuItemLabel,
311+
Color? contextMenuItemMeta,
302312
Color? contextMenuItemText,
303313
Color? editorButtonPressedBg,
304314
Color? foreground,
@@ -337,6 +347,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
337347
composeBoxBg: composeBoxBg ?? this.composeBoxBg,
338348
contextMenuCancelText: contextMenuCancelText ?? this.contextMenuCancelText,
339349
contextMenuItemBg: contextMenuItemBg ?? this.contextMenuItemBg,
350+
contextMenuItemLabel: contextMenuItemLabel ?? this.contextMenuItemLabel,
351+
contextMenuItemMeta: contextMenuItemMeta ?? this.contextMenuItemMeta,
340352
contextMenuItemText: contextMenuItemText ?? this.contextMenuItemBg,
341353
editorButtonPressedBg: editorButtonPressedBg ?? this.editorButtonPressedBg,
342354
foreground: foreground ?? this.foreground,
@@ -382,6 +394,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
382394
composeBoxBg: Color.lerp(composeBoxBg, other.composeBoxBg, t)!,
383395
contextMenuCancelText: Color.lerp(contextMenuCancelText, other.contextMenuCancelText, t)!,
384396
contextMenuItemBg: Color.lerp(contextMenuItemBg, other.contextMenuItemBg, t)!,
397+
contextMenuItemLabel: Color.lerp(contextMenuItemLabel, other.contextMenuItemLabel, t)!,
398+
contextMenuItemMeta: Color.lerp(contextMenuItemMeta, other.contextMenuItemMeta, t)!,
385399
contextMenuItemText: Color.lerp(contextMenuItemText, other.contextMenuItemBg, t)!,
386400
editorButtonPressedBg: Color.lerp(editorButtonPressedBg, other.editorButtonPressedBg, t)!,
387401
foreground: Color.lerp(foreground, other.foreground, t)!,

test/widgets/autocomplete_test.dart

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,17 @@ void main() {
131131
Finder findAvatarImage(int userId) =>
132132
find.byWidgetPredicate((widget) => widget is AvatarImage && widget.userId == userId);
133133

134-
void checkUserShown(User user, PerAccountStore store, {required bool expected}) {
134+
void checkUserShown(User user, {required bool expected}) {
135135
check(find.text(user.fullName).evaluate().length).equals(expected ? 1 : 0);
136+
check(find.text(user.deliveryEmail!).evaluate().length).equals(expected ? 1 : 0);
136137
final avatarFinder = findAvatarImage(user.userId);
137138
check(avatarFinder.evaluate().length).equals(expected ? 1 : 0);
138139
}
139140

140141
testWidgets('options appear, disappear, and change correctly', (tester) async {
141-
final user1 = eg.user(userId: 1, fullName: 'User One', avatarUrl: 'user1.png');
142-
final user2 = eg.user(userId: 2, fullName: 'User Two', avatarUrl: 'user2.png');
143-
final user3 = eg.user(userId: 3, fullName: 'User Three', avatarUrl: 'user3.png');
142+
final user1 = eg.user(userId: 1, fullName: 'User One', avatarUrl: 'user1.png',deliveryEmail: '[email protected]');
143+
final user2 = eg.user(userId: 2, fullName: 'User Two', avatarUrl: 'user2.png', deliveryEmail: '[email protected]');
144+
final user3 = eg.user(userId: 3, fullName: 'User Three', avatarUrl: 'user3.png', deliveryEmail: '[email protected]');
144145
final composeInputFinder = await setupToComposeInput(tester, users: [user1, user2, user3]);
145146
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
146147

@@ -151,33 +152,33 @@ void main() {
151152
await tester.pumpAndSettle(); // async computation; options appear
152153

153154
// "User Two" and "User Three" appear, but not "User One"
154-
checkUserShown(user1, store, expected: false);
155-
checkUserShown(user2, store, expected: true);
156-
checkUserShown(user3, store, expected: true);
155+
checkUserShown(user1, expected: false);
156+
checkUserShown(user2, expected: true);
157+
checkUserShown(user3, expected: true);
157158

158159
// Finishing autocomplete updates compose box; causes options to disappear
159160
await tester.tap(find.text('User Three'));
160161
await tester.pump();
161162
check(tester.widget<TextField>(composeInputFinder).controller!.text)
162163
.contains(mention(user3, users: store.users));
163-
checkUserShown(user1, store, expected: false);
164-
checkUserShown(user2, store, expected: false);
165-
checkUserShown(user3, store, expected: false);
164+
checkUserShown(user1, expected: false);
165+
checkUserShown(user2, expected: false);
166+
checkUserShown(user3, expected: false);
166167

167168
// Then a new autocomplete intent brings up options again
168169
// TODO(#226): Remove this extra edit when this bug is fixed.
169170
await tester.enterText(composeInputFinder, 'hello @user tw');
170171
await tester.enterText(composeInputFinder, 'hello @user two');
171172
await tester.pumpAndSettle(); // async computation; options appear
172-
checkUserShown(user2, store, expected: true);
173+
checkUserShown(user2, expected: true);
173174

174175
// Removing autocomplete intent causes options to disappear
175176
// TODO(#226): Remove one of these edits when this bug is fixed.
176177
await tester.enterText(composeInputFinder, '');
177178
await tester.enterText(composeInputFinder, ' ');
178-
checkUserShown(user1, store, expected: false);
179-
checkUserShown(user2, store, expected: false);
180-
checkUserShown(user3, store, expected: false);
179+
checkUserShown(user1, expected: false);
180+
checkUserShown(user2, expected: false);
181+
checkUserShown(user3, expected: false);
181182

182183
debugNetworkImageHttpClientProvider = null;
183184
});

0 commit comments

Comments
 (0)