Skip to content

Commit

Permalink
autocomplete: Put best matches near input field.
Browse files Browse the repository at this point in the history
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 #226 where
required.

Co-authored-by: Zixuan James Li <[email protected]>
Fixes #1121.
  • Loading branch information
apoorvapendse committed Jan 9, 2025
1 parent 24215f6 commit c08ec8a
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 16 deletions.
1 change: 1 addition & 0 deletions lib/widgets/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class _AutocompleteFieldState<QueryT extends AutocompleteQuery, ResultT extends
constraints: const BoxConstraints(maxHeight: 300), // TODO not hard-coded
child: ListView.builder(
padding: EdgeInsets.zero,
reverse: true,
shrinkWrap: true,
itemCount: _resultsToDisplay.length,
itemBuilder: _buildItem))));
Expand Down
32 changes: 16 additions & 16 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
charcode:
dependency: transitive
description:
Expand Down Expand Up @@ -194,10 +194,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.19.1"
color_models:
dependency: "direct overridden"
description:
Expand Down Expand Up @@ -711,10 +711,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
Expand All @@ -727,10 +727,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.16.0"
mime:
dependency: "direct main"
description:
Expand Down Expand Up @@ -1044,18 +1044,18 @@ packages:
dependency: "direct dev"
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "4ac0537115a24d772c408a2520ecd0abb99bca2ea9c4e634ccbdbfae64fe17ec"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
stream_transform:
dependency: transitive
description:
Expand All @@ -1068,10 +1068,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6"
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
sync_http:
dependency: transitive
description:
Expand All @@ -1084,10 +1084,10 @@ packages:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
test:
dependency: "direct dev"
description:
Expand Down
89 changes: 89 additions & 0 deletions test/widgets/autocomplete_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,36 @@ void main() {

debugNetworkImageHttpClientProvider = null;
});

testWidgets('options are shown in reversed order', (tester) async {
final users = List.generate(8, (i) => eg.user(fullName: 'A$i', avatarUrl: 'user$i.png'));
final composeInputFinder = await setupToComposeInput(tester, users: users);
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);

// TODO(#226): Remove this extra edit when this bug is fixed.
await tester.enterText(composeInputFinder, 'hello @');
await tester.enterText(composeInputFinder, 'hello @A');
await tester.pump();

final initialPosition = tester.getTopLeft(find.text(users.first.fullName)).dy;
// Initially, all but the last autocomplete options are visible.
checkUserShown(users.last, store, expected: false);
users.take(7).forEach((user) => checkUserShown(user, store, expected: true));

// Can't scroll down because the options grow from the bottom.
await tester.drag(find.byType(ListView), const Offset(0, -50));
await tester.pump();
check(tester.getTopLeft(find.text(users.first.fullName)).dy)
.equals(initialPosition);

// The last autocomplete option becomes visible after scrolling up.
await tester.drag(find.byType(ListView), const Offset(0, 200));
await tester.pump();
users.skip(1).forEach((user) => checkUserShown(user, store, expected: true));
checkUserShown(users.first, store, expected: false);

debugNetworkImageHttpClientProvider = null;
});
});

group('emoji', () {
Expand Down Expand Up @@ -247,6 +277,65 @@ void main() {
debugNetworkImageHttpClientProvider = null;
});

testWidgets('emoji options appear in the reverse order and do not scroll down', (tester) async {
final composeInputFinder = await setupToComposeInput(tester);
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);

store.setServerEmojiData(
ServerEmojiData(
codeToNames: {
'1f4a4': ['zzz', 'sleepy'], // Unicode emoji for "zzz"
'1f52a': ['biohazard'],
'1f92a': ['zany_face'],
'1f993': ['zebra'],
'0030-fe0f-20e3': ['zero'],
'1f9d0': ['zombie'],
}));

await store.handleEvent(
RealmEmojiUpdateEvent(
id: 1,
realmEmoji: {
'1': eg.realmEmojiItem(emojiCode: '1', emojiName: 'buzzing')}));

final emojiSequence = ['zulip', 'zany_face', 'zebra', 'zzz', '💤', 'zombie', 'zero', 'buzzing', 'biohazard'];

// Enter a query; options appear, of all three emoji types.
// TODO(#226): Remove this extra edit when this bug is fixed.
await tester.enterText(composeInputFinder, 'hi :');
await tester.enterText(composeInputFinder, 'hi :z');
await tester.pump();

final firstEmojiInitialPosition = tester.getTopLeft(find.text(emojiSequence[0])).dy;
final listViewFinder = find.byType(ListView);

await tester.drag(listViewFinder, const Offset(0, -50));
await tester.pump();
final firstEmojiPositionAfterScrollDown = tester.getTopLeft(find.text(emojiSequence[0])).dy;
check(
because: "ListView options should not scroll down further than initial position",
firstEmojiInitialPosition
).equals(firstEmojiPositionAfterScrollDown);

final biohazardFinder = find.text(emojiSequence.last);
check(
because: "The biohazard emoji should not be visible before scrolling up",
biohazardFinder
).findsNothing();

// Scroll up
await tester.drag(listViewFinder, const Offset(0, 50));
await tester.pump();

check(because: "The biohazard emoji should be visible after scrolling up",biohazardFinder).findsOne();

final firstEmojiPositionAfterScrollUp = tester.getTopLeft(find.text(emojiSequence[0])).dy;
check(because: "Scrolling up should reveal other emoji matches",firstEmojiPositionAfterScrollUp).isGreaterOrEqual(firstEmojiInitialPosition);

debugNetworkImageHttpClientProvider = null;

});

testWidgets('text emoji means just show text', (tester) async {
final composeInputFinder = await setupToComposeInput(tester);
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
Expand Down

0 comments on commit c08ec8a

Please sign in to comment.