Skip to content

Commit c08ec8a

Browse files
apoorvapendsePIG208
andcommitted
autocomplete: Put best matches near input field.
This commit reverses the list that was originally presented to the user while showing the typeahead menu. This makes sense since on mobile its easier to click on options closer to the input box, i.e. where your fingers are currently present, instead of pressing arrow keys on a keyboard which is true on a desktop setup. Hence we place the best matching options not at the top of the typeahead menu, but instead put them at the bottom for better reachability and convenience of the user. Tests have been added to verify the emoji and mention render behavior. Also mentions dependencies on zulip#226 where required. Co-authored-by: Zixuan James Li <[email protected]> Fixes zulip#1121.
1 parent 24215f6 commit c08ec8a

File tree

3 files changed

+106
-16
lines changed

3 files changed

+106
-16
lines changed

lib/widgets/autocomplete.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ class _AutocompleteFieldState<QueryT extends AutocompleteQuery, ResultT extends
134134
constraints: const BoxConstraints(maxHeight: 300), // TODO not hard-coded
135135
child: ListView.builder(
136136
padding: EdgeInsets.zero,
137+
reverse: true,
137138
shrinkWrap: true,
138139
itemCount: _resultsToDisplay.length,
139140
itemBuilder: _buildItem))));

pubspec.lock

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,10 @@ packages:
138138
dependency: transitive
139139
description:
140140
name: characters
141-
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
141+
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
142142
url: "https://pub.dev"
143143
source: hosted
144-
version: "1.3.0"
144+
version: "1.4.0"
145145
charcode:
146146
dependency: transitive
147147
description:
@@ -194,10 +194,10 @@ packages:
194194
dependency: "direct main"
195195
description:
196196
name: collection
197-
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
197+
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
198198
url: "https://pub.dev"
199199
source: hosted
200-
version: "1.19.0"
200+
version: "1.19.1"
201201
color_models:
202202
dependency: "direct overridden"
203203
description:
@@ -711,10 +711,10 @@ packages:
711711
dependency: transitive
712712
description:
713713
name: matcher
714-
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
714+
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
715715
url: "https://pub.dev"
716716
source: hosted
717-
version: "0.12.16+1"
717+
version: "0.12.17"
718718
material_color_utilities:
719719
dependency: transitive
720720
description:
@@ -727,10 +727,10 @@ packages:
727727
dependency: transitive
728728
description:
729729
name: meta
730-
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
730+
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
731731
url: "https://pub.dev"
732732
source: hosted
733-
version: "1.15.0"
733+
version: "1.16.0"
734734
mime:
735735
dependency: "direct main"
736736
description:
@@ -1044,18 +1044,18 @@ packages:
10441044
dependency: "direct dev"
10451045
description:
10461046
name: stack_trace
1047-
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
1047+
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
10481048
url: "https://pub.dev"
10491049
source: hosted
1050-
version: "1.12.0"
1050+
version: "1.12.1"
10511051
stream_channel:
10521052
dependency: transitive
10531053
description:
10541054
name: stream_channel
1055-
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
1055+
sha256: "4ac0537115a24d772c408a2520ecd0abb99bca2ea9c4e634ccbdbfae64fe17ec"
10561056
url: "https://pub.dev"
10571057
source: hosted
1058-
version: "2.1.2"
1058+
version: "2.1.3"
10591059
stream_transform:
10601060
dependency: transitive
10611061
description:
@@ -1068,10 +1068,10 @@ packages:
10681068
dependency: transitive
10691069
description:
10701070
name: string_scanner
1071-
sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6"
1071+
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
10721072
url: "https://pub.dev"
10731073
source: hosted
1074-
version: "1.4.0"
1074+
version: "1.4.1"
10751075
sync_http:
10761076
dependency: transitive
10771077
description:
@@ -1084,10 +1084,10 @@ packages:
10841084
dependency: transitive
10851085
description:
10861086
name: term_glyph
1087-
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
1087+
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
10881088
url: "https://pub.dev"
10891089
source: hosted
1090-
version: "1.2.1"
1090+
version: "1.2.2"
10911091
test:
10921092
dependency: "direct dev"
10931093
description:

test/widgets/autocomplete_test.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,36 @@ void main() {
177177

178178
debugNetworkImageHttpClientProvider = null;
179179
});
180+
181+
testWidgets('options are shown in reversed order', (tester) async {
182+
final users = List.generate(8, (i) => eg.user(fullName: 'A$i', avatarUrl: 'user$i.png'));
183+
final composeInputFinder = await setupToComposeInput(tester, users: users);
184+
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
185+
186+
// TODO(#226): Remove this extra edit when this bug is fixed.
187+
await tester.enterText(composeInputFinder, 'hello @');
188+
await tester.enterText(composeInputFinder, 'hello @A');
189+
await tester.pump();
190+
191+
final initialPosition = tester.getTopLeft(find.text(users.first.fullName)).dy;
192+
// Initially, all but the last autocomplete options are visible.
193+
checkUserShown(users.last, store, expected: false);
194+
users.take(7).forEach((user) => checkUserShown(user, store, expected: true));
195+
196+
// Can't scroll down because the options grow from the bottom.
197+
await tester.drag(find.byType(ListView), const Offset(0, -50));
198+
await tester.pump();
199+
check(tester.getTopLeft(find.text(users.first.fullName)).dy)
200+
.equals(initialPosition);
201+
202+
// The last autocomplete option becomes visible after scrolling up.
203+
await tester.drag(find.byType(ListView), const Offset(0, 200));
204+
await tester.pump();
205+
users.skip(1).forEach((user) => checkUserShown(user, store, expected: true));
206+
checkUserShown(users.first, store, expected: false);
207+
208+
debugNetworkImageHttpClientProvider = null;
209+
});
180210
});
181211

182212
group('emoji', () {
@@ -247,6 +277,65 @@ void main() {
247277
debugNetworkImageHttpClientProvider = null;
248278
});
249279

280+
testWidgets('emoji options appear in the reverse order and do not scroll down', (tester) async {
281+
final composeInputFinder = await setupToComposeInput(tester);
282+
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
283+
284+
store.setServerEmojiData(
285+
ServerEmojiData(
286+
codeToNames: {
287+
'1f4a4': ['zzz', 'sleepy'], // Unicode emoji for "zzz"
288+
'1f52a': ['biohazard'],
289+
'1f92a': ['zany_face'],
290+
'1f993': ['zebra'],
291+
'0030-fe0f-20e3': ['zero'],
292+
'1f9d0': ['zombie'],
293+
}));
294+
295+
await store.handleEvent(
296+
RealmEmojiUpdateEvent(
297+
id: 1,
298+
realmEmoji: {
299+
'1': eg.realmEmojiItem(emojiCode: '1', emojiName: 'buzzing')}));
300+
301+
final emojiSequence = ['zulip', 'zany_face', 'zebra', 'zzz', '💤', 'zombie', 'zero', 'buzzing', 'biohazard'];
302+
303+
// Enter a query; options appear, of all three emoji types.
304+
// TODO(#226): Remove this extra edit when this bug is fixed.
305+
await tester.enterText(composeInputFinder, 'hi :');
306+
await tester.enterText(composeInputFinder, 'hi :z');
307+
await tester.pump();
308+
309+
final firstEmojiInitialPosition = tester.getTopLeft(find.text(emojiSequence[0])).dy;
310+
final listViewFinder = find.byType(ListView);
311+
312+
await tester.drag(listViewFinder, const Offset(0, -50));
313+
await tester.pump();
314+
final firstEmojiPositionAfterScrollDown = tester.getTopLeft(find.text(emojiSequence[0])).dy;
315+
check(
316+
because: "ListView options should not scroll down further than initial position",
317+
firstEmojiInitialPosition
318+
).equals(firstEmojiPositionAfterScrollDown);
319+
320+
final biohazardFinder = find.text(emojiSequence.last);
321+
check(
322+
because: "The biohazard emoji should not be visible before scrolling up",
323+
biohazardFinder
324+
).findsNothing();
325+
326+
// Scroll up
327+
await tester.drag(listViewFinder, const Offset(0, 50));
328+
await tester.pump();
329+
330+
check(because: "The biohazard emoji should be visible after scrolling up",biohazardFinder).findsOne();
331+
332+
final firstEmojiPositionAfterScrollUp = tester.getTopLeft(find.text(emojiSequence[0])).dy;
333+
check(because: "Scrolling up should reveal other emoji matches",firstEmojiPositionAfterScrollUp).isGreaterOrEqual(firstEmojiInitialPosition);
334+
335+
debugNetworkImageHttpClientProvider = null;
336+
337+
});
338+
250339
testWidgets('text emoji means just show text', (tester) async {
251340
final composeInputFinder = await setupToComposeInput(tester);
252341
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);

0 commit comments

Comments
 (0)