mirror of https://github.com/zulip/zulip.git
typeahead: Always rank exact string match first.
Previously, exact matches could be pushed off the typeahead list in the case where there were more prefix matches that happened to rank first, which is confusing to the user: if an emoji, for instance, falls into this category, it will never show up in typeahead, which is easy to confuse with the emoji not existing. This isn't a perfect fix — there are still cases where it's hard to find emojis because the prefix-space is very crowded, but it does fix a category of surprising and frustrating behaviour. This doesn't come completely without downside - it means that the exact match emoji will jump to the front of the list, which changes what is currently conceptually a "filtering" operation to a "filtering and sorting" operation, but it seems on the whole to be a more ideal experience. This is particularly notable in the non-typeahead emoji picker, which uses the same codepath, but this change seems somewhat desirable even there, since it allows the user to type the name of an emoji and press enter and have that emoji show up, without having to visually confirm that they aren't inadvertently selecting a prefix-matching emoji. A better solution to this in the long term might be ordering emoji results by shortest-first as a tiebreaker for alphabetical ordering, since that should provide the same behaviour while keeping the mental model as "filtering" (since the sort order won't change as the user types), but this seems like a reasonable first pass, and changing to shortest-first ordering after making this change won't break any muscle memory for existing users.
This commit is contained in:
parent
0d01e7070f
commit
673af19a4d
|
@ -57,16 +57,18 @@ run_test("get_emoji_matcher", () => {
|
|||
run_test("triage", () => {
|
||||
const alice = {name: "alice"};
|
||||
const alicia = {name: "Alicia"};
|
||||
const joan = {name: "Joan"};
|
||||
const jo = {name: "Jo"};
|
||||
const steve = {name: "steve"};
|
||||
const stephanie = {name: "Stephanie"};
|
||||
|
||||
const names = [alice, alicia, steve, stephanie];
|
||||
const names = [alice, alicia, joan, jo, steve, stephanie];
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("a", names, (r) => r.name),
|
||||
{
|
||||
matches: [alice, alicia],
|
||||
rest: [steve, stephanie],
|
||||
rest: [joan, jo, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -74,7 +76,7 @@ run_test("triage", () => {
|
|||
typeahead.triage("A", names, (r) => r.name),
|
||||
{
|
||||
matches: [alicia, alice],
|
||||
rest: [steve, stephanie],
|
||||
rest: [joan, jo, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -82,7 +84,7 @@ run_test("triage", () => {
|
|||
typeahead.triage("S", names, (r) => r.name),
|
||||
{
|
||||
matches: [stephanie, steve],
|
||||
rest: [alice, alicia],
|
||||
rest: [alice, alicia, joan, jo],
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -90,6 +92,22 @@ run_test("triage", () => {
|
|||
typeahead.triage("fred", names, (r) => r.name),
|
||||
{
|
||||
matches: [],
|
||||
rest: [alice, alicia, joan, jo, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("Jo", names, (r) => r.name),
|
||||
{
|
||||
matches: [jo, joan],
|
||||
rest: [alice, alicia, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("jo", names, (r) => r.name),
|
||||
{
|
||||
matches: [jo, joan],
|
||||
rest: [alice, alicia, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
|
|
@ -110,13 +110,14 @@ export function get_emoji_matcher(query) {
|
|||
|
||||
export function triage(query, objs, get_item) {
|
||||
/*
|
||||
We split objs into three groups:
|
||||
We split objs into four groups:
|
||||
|
||||
- entire string exact match
|
||||
- match prefix exactly with `query`
|
||||
- match prefix case-insensitively
|
||||
- other
|
||||
|
||||
Then we concat the first two groups into
|
||||
Then we concat the first three groups into
|
||||
`matches` and then call the rest `rest`.
|
||||
*/
|
||||
|
||||
|
@ -124,6 +125,7 @@ export function triage(query, objs, get_item) {
|
|||
get_item = (x) => x;
|
||||
}
|
||||
|
||||
const exactMatch = [];
|
||||
const beginswithCaseSensitive = [];
|
||||
const beginswithCaseInsensitive = [];
|
||||
const noMatch = [];
|
||||
|
@ -131,17 +133,20 @@ export function triage(query, objs, get_item) {
|
|||
|
||||
for (const obj of objs) {
|
||||
const item = get_item(obj);
|
||||
const lowerItem = item.toLowerCase();
|
||||
|
||||
if (item.startsWith(query)) {
|
||||
if (lowerItem === lowerQuery) {
|
||||
exactMatch.push(obj);
|
||||
} else if (item.startsWith(query)) {
|
||||
beginswithCaseSensitive.push(obj);
|
||||
} else if (item.toLowerCase().startsWith(lowerQuery)) {
|
||||
} else if (lowerItem.startsWith(lowerQuery)) {
|
||||
beginswithCaseInsensitive.push(obj);
|
||||
} else {
|
||||
noMatch.push(obj);
|
||||
}
|
||||
}
|
||||
return {
|
||||
matches: beginswithCaseSensitive.concat(beginswithCaseInsensitive),
|
||||
matches: exactMatch.concat(beginswithCaseSensitive.concat(beginswithCaseInsensitive)),
|
||||
rest: noMatch,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue