typeahead: Make menu scrollable with up to 50 options.

To increase the number of options available for the user to pick from,
we increase the limit of options shown to 50 for all typeaheads, and
make the menu scrollable. The max height is set to original height of
the composebox typeahead menu, which fit 8 options or to 95% of the
window height, whichever is smaller.

Fixes: #20620.
This commit is contained in:
N-Shar-ma 2024-06-27 13:51:46 +05:30 committed by Tim Abbott
parent 4cb925a9c8
commit 82c2da8aae
13 changed files with 29 additions and 29 deletions

View File

@ -165,6 +165,7 @@ import {insertTextIntoField} from "text-field-edit";
import getCaretCoordinates from "textarea-caret";
import * as tippy from "tippy.js";
import * as scroll_util from "./scroll_util";
import {get_string_diff} from "./util";
function get_pseudo_keycode(
@ -197,13 +198,15 @@ export function defaultSorter(items: string[], query: string): string[] {
return [...beginswith, ...caseSensitive, ...caseInsensitive];
}
export const MAX_ITEMS = 50;
/* TYPEAHEAD PUBLIC CLASS DEFINITION
* ================================= */
const HEADER_ELEMENT_HTML =
'<p class="typeahead-header"><span id="typeahead-header-text"></span></p>';
const CONTAINER_HTML = '<div class="typeahead dropdown-menu"></div>';
const MENU_HTML = '<ul class="typeahead-menu"></ul>';
const MENU_HTML = '<ul class="typeahead-menu" data-simplebar></ul>';
const ITEM_HTML = "<li><a></a></li>";
const MIN_LENGTH = 1;
@ -275,7 +278,7 @@ export class Typeahead<ItemType extends string | object> {
} else {
assert(!this.input_element.$element.is("[contenteditable]"));
}
this.items = options.items ?? 8;
this.items = options.items ?? MAX_ITEMS;
this.matcher = options.matcher ?? ((item, query) => this.defaultMatcher(item, query));
this.sorter = options.sorter;
this.highlighter_html = options.highlighter_html;
@ -538,7 +541,10 @@ export class Typeahead<ItemType extends string | object> {
if (this.requireHighlight || this.shouldHighlightFirstResult()) {
$items[0]!.addClass("active");
}
this.$menu.empty().append($items);
// Getting scroll element ensures simplebar has processed the element
// before we render it.
scroll_util.get_scroll_element(this.$menu);
scroll_util.get_content_element(this.$menu).empty().append($items);
return this;
}
@ -558,6 +564,7 @@ export class Typeahead<ItemType extends string | object> {
}
$next.addClass("active");
scroll_util.scroll_element_into_container($next, this.$menu);
}
prev(): void {
@ -576,6 +583,7 @@ export class Typeahead<ItemType extends string | object> {
}
$prev.addClass("active");
scroll_util.scroll_element_into_container($prev, this.$menu);
}
listen(): void {
@ -831,7 +839,7 @@ export class Typeahead<ItemType extends string | object> {
type TypeaheadOptions<ItemType> = {
highlighter_html: (item: ItemType, query: string) => string | undefined;
items: number;
items?: number;
source: (query: string, input_element: TypeaheadInputElement) => ItemType[];
// optional options
advanceKeyCodes?: number[];

View File

@ -6,7 +6,7 @@ import * as typeahead from "../shared/src/typeahead";
import type {Emoji, EmojiSuggestion} from "../shared/src/typeahead";
import render_topic_typeahead_hint from "../templates/topic_typeahead_hint.hbs";
import {Typeahead} from "./bootstrap_typeahead";
import {MAX_ITEMS, Typeahead} from "./bootstrap_typeahead";
import type {TypeaheadInputElement} from "./bootstrap_typeahead";
import * as bulleted_numbered_list_util from "./bulleted_numbered_list_util";
import * as compose_pm_pill from "./compose_pm_pill";
@ -103,9 +103,8 @@ export type TypeaheadSuggestion =
| EmojiSuggestion
| SlashCommandSuggestion;
// This is what we use for direct message/compose typeaheads.
// We export it to allow tests to mock it.
export const max_num_items = 8;
export const max_num_items = MAX_ITEMS;
export let emoji_collection: Emoji[] = [];
@ -1239,7 +1238,7 @@ export function initialize_topic_edit_typeahead(
const stream_id = stream_data.get_stream_id(stream_name);
return topics_seen_for(stream_id);
},
items: 5,
items: max_num_items,
});
}
@ -1317,7 +1316,7 @@ export function initialize({
source(): string[] {
return topics_seen_for(compose_state.stream_id());
},
items: 3,
items: max_num_items,
highlighter_html(item: string): string {
return typeahead_helper.render_typeahead_item({primary: item});
},

View File

@ -202,7 +202,6 @@ export function initialize_custom_pronouns_type_fields(element_id: string): void
type: "input" as const,
};
new Typeahead(bootstrap_typeahead_input, {
items: 3,
helpOnEmptyStrings: true,
source() {
return commonly_used_pronouns;

View File

@ -41,7 +41,6 @@ export function set_up_user(
type: "contenteditable",
};
new Typeahead(bootstrap_typeahead_input, {
items: 5,
dropup: true,
source(_query: string): UserPillData[] {
return user_pill.typeahead_source(pills, exclude_bots);
@ -87,7 +86,6 @@ export function set_up_stream(
};
opts.help_on_empty_strings ||= false;
new Typeahead(bootstrap_typeahead_input, {
items: 12,
dropup: true,
helpOnEmptyStrings: true,
source(_query: string): StreamPillData[] {
@ -153,7 +151,6 @@ export function set_up_combined(
type: "contenteditable",
};
new Typeahead(bootstrap_typeahead_input, {
items: 5,
dropup: true,
source(query: string): TypeaheadItem[] {
let source: TypeaheadItem[] = [];

View File

@ -3,6 +3,7 @@ import assert from "minimalistic-assert";
import render_user_pill from "../templates/user_pill.hbs";
import {MAX_ITEMS} from "./bootstrap_typeahead";
import * as common from "./common";
import * as direct_message_group_data from "./direct_message_group_data";
import {Filter, create_user_pill_context} from "./filter";
@ -41,7 +42,7 @@ export type Suggestion = {
}
);
export const max_num_of_search_results = 12;
export const max_num_of_search_results = MAX_ITEMS;
function channel_matches_query(channel_name: string, q: string): boolean {
return common.phrase_match(q, channel_name);

View File

@ -169,7 +169,6 @@ function build_page(): void {
language_labels = realm_playground.get_pygments_typeahead_list_for_settings(query);
return [...language_labels.keys()];
},
items: 5,
helpOnEmptyStrings: true,
highlighter_html: (item: string): string =>
render_typeahead_item({primary: language_labels.get(item)}),

View File

@ -6,6 +6,7 @@ import * as typeahead from "../shared/src/typeahead";
import type {EmojiSuggestion} from "../shared/src/typeahead";
import render_typeahead_list_item from "../templates/typeahead_list_item.hbs";
import {MAX_ITEMS} from "./bootstrap_typeahead";
import * as buddy_data from "./buddy_data";
import * as compose_state from "./compose_state";
import type {LanguageSuggestion, SlashCommandSuggestion} from "./composebox_typeahead";
@ -416,7 +417,7 @@ export function sort_recipients<UserType extends UserOrMentionPillData | UserPil
current_stream_id,
current_topic,
groups = [],
max_num_items = 20,
max_num_items = MAX_ITEMS,
}: {
users: UserType[];
query: string;

View File

@ -1404,6 +1404,12 @@ textarea.new_message_textarea {
.typeahead-menu {
list-style: none;
margin: 4px 0;
max-height: min(248px, 95vh);
overflow-y: auto;
.simplebar-content {
min-width: max-content;
}
}
.typeahead-header {

View File

@ -166,7 +166,7 @@
color: var(--color-text-search-hover);
}
.typeahead-menu > li > a {
.typeahead-menu .simplebar-content > li > a {
max-width: none;
}
}
@ -331,7 +331,7 @@
}
}
.typeahead-menu > li > a {
.typeahead-menu .simplebar-content > li > a {
padding: 3px 30px;
/* Override white-space: nowrap from zulip.css */
white-space: normal;

View File

@ -14,7 +14,7 @@
z-index: 1051;
}
.typeahead.dropdown-menu .typeahead-menu {
.typeahead.dropdown-menu .typeahead-menu .simplebar-content {
& > li {
word-break: break-word;

View File

@ -59,11 +59,6 @@ const settings_config = zrequire("settings_config");
const ct = composebox_typeahead;
// Use a slightly larger value than what's user-facing
// to facilitate testing different combinations of
// broadcast-mentions/persons/groups.
ct.__Rewire__("max_num_items", 15);
function user_item(user) {
return {type: "user", user};
}

View File

@ -162,7 +162,6 @@ run_test("set_up_user", ({mock_template, override, override_rewire}) => {
override(bootstrap_typeahead, "Typeahead", (input_element, config) => {
assert.equal(input_element.$element, $fake_input);
assert.equal(config.items, 5);
assert.ok(config.dropup);
assert.ok(config.stopAdvance);
@ -255,7 +254,6 @@ run_test("set_up_stream", ({mock_template, override, override_rewire}) => {
override(bootstrap_typeahead, "Typeahead", (input_element, config) => {
assert.equal(input_element.$element, $fake_input);
assert.equal(config.items, 12);
assert.ok(config.dropup);
assert.ok(config.stopAdvance);
@ -345,7 +343,6 @@ run_test("set_up_combined", ({mock_template, override, override_rewire}) => {
let opts = {};
override(bootstrap_typeahead, "Typeahead", (input_element, config) => {
assert.equal(input_element.$element, $fake_input);
assert.equal(config.items, 5);
assert.ok(config.dropup);
assert.ok(config.stopAdvance);

View File

@ -16,8 +16,6 @@ const stream_topic_history = zrequire("stream_topic_history");
const people = zrequire("people");
const search = zrequire("search_suggestion");
search.__Rewire__("max_num_of_search_results", 15);
const me = {
email: "myself@zulip.com",
full_name: "Me Myself",