diff --git a/web/src/bootstrap_typeahead.ts b/web/src/bootstrap_typeahead.ts index d99b73ecde..e3da16d096 100644 --- a/web/src/bootstrap_typeahead.ts +++ b/web/src/bootstrap_typeahead.ts @@ -346,6 +346,27 @@ export class Typeahead { maxWidth: "none", theme: "popover-menu", placement: this.dropup ? "top-start" : "bottom-start", + popperOptions: { + modifiers: [ + { + // This will only work if there is enough space on the fallback placement, otherwise + // `preventOverflow` will be used to position it in the visible space. + name: "flip", + options: { + fallbackPlacements: ["top-start", "bottom-start"], + }, + }, + { + name: "preventOverflow", + options: { + // This seems required to prevent overflow, maybe because our placements are + // not the usual top, bottom, left, right. + // https://popper.js.org/docs/v2/modifiers/prevent-overflow/#altaxis + altAxis: true, + }, + }, + ], + }, interactive: true, appendTo: () => document.body, showOnCreate: true, diff --git a/web/src/compose_ui.ts b/web/src/compose_ui.ts index 7da357ec6b..64e4cf359e 100644 --- a/web/src/compose_ui.ts +++ b/web/src/compose_ui.ts @@ -10,6 +10,7 @@ import { wrapFieldSelection, } from "text-field-edit"; +import type {Typeahead} from "./bootstrap_typeahead"; import * as bulleted_numbered_list_util from "./bulleted_numbered_list_util"; import * as common from "./common"; import {$t} from "./i18n"; @@ -58,8 +59,13 @@ type SelectedLinesSections = { export let compose_spinner_visible = false; export let shift_pressed = false; // true or false export let code_formatting_button_triggered = false; // true or false +export let compose_textarea_typeahead: Typeahead | undefined; let full_size_status = false; // true or false +export function set_compose_textarea_typeahead(typeahead: Typeahead): void { + compose_textarea_typeahead = typeahead; +} + export function set_code_formatting_button_triggered(value: boolean): void { code_formatting_button_triggered = value; } @@ -67,6 +73,10 @@ export function set_code_formatting_button_triggered(value: boolean): void { // Some functions to handle the full size status explicitly export function set_full_size(is_full: boolean): void { full_size_status = is_full; + // Show typeahead at bottom of textarea on compose full size. + if (compose_textarea_typeahead) { + compose_textarea_typeahead.dropup = !is_full; + } } export function is_full_size(): boolean { diff --git a/web/src/composebox_typeahead.js b/web/src/composebox_typeahead.js index c554c5884b..9e53718c2a 100644 --- a/web/src/composebox_typeahead.js +++ b/web/src/composebox_typeahead.js @@ -1157,27 +1157,30 @@ export function initialize_compose_typeahead(selector) { $element: $(selector), type: "textarea", }; - new Typeahead(bootstrap_typeahead_input, { - items: max_num_items, - dropup: true, - // Performance note: We have trivial matcher/sorters to do - // matching and sorting inside the `source` field to avoid - // O(n) behavior in the number of users in the organization - // inside the typeahead library. - source: get_sorted_filtered_items, - highlighter_html: content_highlighter_html, - matcher() { - return true; - }, - sorter(items) { - return items; - }, - updater: content_typeahead_selected, - stopAdvance: true, // Do not advance to the next field on a Tab or Enter - automated: compose_automated_selection, - trigger_selection: compose_trigger_selection, - header_html: get_header_html, - }); + + compose_ui.set_compose_textarea_typeahead( + new Typeahead(bootstrap_typeahead_input, { + items: max_num_items, + dropup: true, + // Performance note: We have trivial matcher/sorters to do + // matching and sorting inside the `source` field to avoid + // O(n) behavior in the number of users in the organization + // inside the typeahead library. + source: get_sorted_filtered_items, + highlighter_html: content_highlighter_html, + matcher() { + return true; + }, + sorter(items) { + return items; + }, + updater: content_typeahead_selected, + stopAdvance: true, // Do not advance to the next field on a Tab or Enter + automated: compose_automated_selection, + trigger_selection: compose_trigger_selection, + header_html: get_header_html, + }), + ); } export function initialize({on_enter_send}) { diff --git a/web/tests/composebox_typeahead.test.js b/web/tests/composebox_typeahead.test.js index 065d144a32..a7fe03d553 100644 --- a/web/tests/composebox_typeahead.test.js +++ b/web/tests/composebox_typeahead.test.js @@ -17,6 +17,7 @@ const compose_ui = mock_esm("../src/compose_ui", { }, cursor_inside_code_block: () => false, set_code_formatting_button_triggered: noop, + set_compose_textarea_typeahead: noop, }); const compose_validate = mock_esm("../src/compose_validate", { validate_message_length: () => true,