diff --git a/web/src/compose_recipient.ts b/web/src/compose_recipient.ts index 0f6b4c3fea..04ab675280 100644 --- a/web/src/compose_recipient.ts +++ b/web/src/compose_recipient.ts @@ -267,33 +267,6 @@ function get_options_for_recipient_widget(): Option[] { return options; } -function compose_recipient_dropdown_on_show(dropdown: tippy.Instance): void { - // Offset to display dropdown above compose. - let top_offset = 5; - const window_height = window.innerHeight; - const search_box_and_padding_height = 50; - // pixels above compose box. - const recipient_input_top = $("#compose_select_recipient_widget_wrapper").get_offset_to_window() - .top; - const top_space = recipient_input_top - top_offset - search_box_and_padding_height; - // pixels below compose starting from top of compose box. - const bottom_space = window_height - recipient_input_top - search_box_and_padding_height; - // Show dropdown on top / bottom based on available space. - let placement: tippy.Placement = "top-start"; - if (bottom_space > top_space) { - placement = "bottom-start"; - top_offset = -30; - } - const offset: [number, number] = [-10, top_offset]; - dropdown.setProps({placement, offset}); - const height = Math.min( - dropdown_widget.DEFAULT_DROPDOWN_HEIGHT, - Math.max(top_space, bottom_space), - ); - const $popper = $(dropdown.popper); - $popper.find(".dropdown-list-wrapper").css("max-height", height + "px"); -} - export function open_compose_recipient_dropdown(): void { $("#compose_select_recipient_widget").trigger("click"); } @@ -330,11 +303,15 @@ export function initialize(): void { get_options: get_options_for_recipient_widget, item_click_callback, $events_container: $("body"), - on_show_callback: compose_recipient_dropdown_on_show, on_exit_with_escape_callback: focus_compose_recipient, // We want to focus on topic box if dropdown was closed via selecting an item. focus_target_on_hidden: false, on_hidden_callback, + dropdown_input_visible_selector: "#compose_select_recipient_widget_wrapper", + prefer_top_start_placement: true, + tippy_props: { + offset: [-10, 5], + }, }).setup(); // `input` isn't relevant for streams since it registers as a change only diff --git a/web/src/dropdown_widget.ts b/web/src/dropdown_widget.ts index 3ef5ef9d17..46074c54f3 100644 --- a/web/src/dropdown_widget.ts +++ b/web/src/dropdown_widget.ts @@ -67,6 +67,8 @@ type DropdownWidgetOptions = { hide_search_box?: boolean; // Disable the widget for spectators. disable_for_spectators?: boolean; + dropdown_input_visible_selector?: string; + prefer_top_start_placement?: boolean; }; export class DropdownWidget { @@ -96,6 +98,8 @@ export class DropdownWidget { text_if_current_value_not_in_options: string; hide_search_box: boolean; disable_for_spectators: boolean; + dropdown_input_visible_selector: string; + prefer_top_start_placement: boolean; constructor(options: DropdownWidgetOptions) { this.widget_name = options.widget_name; @@ -123,6 +127,9 @@ export class DropdownWidget { options.text_if_current_value_not_in_options ?? ""; this.hide_search_box = options.hide_search_box ?? false; this.disable_for_spectators = options.disable_for_spectators ?? false; + this.dropdown_input_visible_selector = + options.dropdown_input_visible_selector ?? this.widget_selector; + this.prefer_top_start_placement = options.prefer_top_start_placement ?? false; } init(): void { @@ -159,6 +166,57 @@ export class DropdownWidget { } } + adjust_dropdown_position_post_list_render(tippy_instance: tippy.Instance): void { + let top_offset = 0; + let left_offset = 0; + + // Use offset if provided by the widget callers. + if (typeof this.tippy_props?.offset === "object" && this.tippy_props?.offset.length === 2) { + left_offset = this.tippy_props.offset[0]; + top_offset = this.tippy_props.offset[1]; + } + + const window_height = window.innerHeight; + let dropdown_search_box_and_padding_height = 50; + if (this.hide_search_box) { + dropdown_search_box_and_padding_height = 0; + } + const dropdown_input_props = $(this.dropdown_input_visible_selector).get_offset_to_window(); + const dropdown_input_top = dropdown_input_props.top; + + // Pixels above the dropdown input. + const top_space = dropdown_input_top - top_offset - dropdown_search_box_and_padding_height; + // Pixels below the top of dropdown input. + const bottom_space = + window_height - dropdown_input_top - dropdown_search_box_and_padding_height; + + // Show dropdown at bottom by default if we `DEFAULT_DROPDOWN_HEIGHT` + // space available unless the dropdown caller prefers to show at top. + // If we don't have `DEFAULT_DROPDOWN_HEIGHT` space available above + // or below the dropdown input, show the dropdown at maximum space. + let placement: tippy.Placement = "top-start"; + let height: number = Math.min(DEFAULT_DROPDOWN_HEIGHT, Math.max(top_space, bottom_space)); + if (this.prefer_top_start_placement && top_space > DEFAULT_DROPDOWN_HEIGHT) { + height = DEFAULT_DROPDOWN_HEIGHT; + } else if (!this.prefer_top_start_placement && bottom_space > DEFAULT_DROPDOWN_HEIGHT) { + placement = "bottom-start"; + height = DEFAULT_DROPDOWN_HEIGHT; + // Use the provided offset if we have enough space. Otherwise, + // we overlap the top of dropdown with top of dropdown input. + if (this.tippy_props?.offset === undefined) { + top_offset = -1 * dropdown_input_props.height; + } + } else if (bottom_space > top_space) { + placement = "bottom-start"; + top_offset = -1 * dropdown_input_props.height; + } + + const offset: [number, number] = [left_offset, top_offset]; + tippy_instance.setProps({placement, offset}); + const $popper = $(tippy_instance.popper); + $popper.find(".dropdown-list-wrapper").css("max-height", height + "px"); + } + show_empty_if_no_items($popper: JQuery): void { assert(this.list_widget !== undefined); const list_items = this.list_widget.get_current_list(); @@ -361,6 +419,7 @@ export class DropdownWidget { }, 0); this.on_show_callback(instance); + this.adjust_dropdown_position_post_list_render(instance); }, onMount: (instance: tippy.Instance) => { this.show_empty_if_no_items($(instance.popper)); diff --git a/web/src/integration_url_modal.ts b/web/src/integration_url_modal.ts index 480fa419db..7a9e2f3e49 100644 --- a/web/src/integration_url_modal.ts +++ b/web/src/integration_url_modal.ts @@ -156,9 +156,6 @@ export function show_generate_integration_url_modal(api_key: string): void { get_options: get_options_for_integration_input_dropdown_widget, item_click_callback: integration_item_click_callback, $events_container: $("#generate-integration-url-modal"), - tippy_props: { - placement: "bottom-start", - }, default_id: default_integration_option.unique_id, unique_id_type: dropdown_widget.DataTypes.STRING, }); @@ -194,9 +191,6 @@ export function show_generate_integration_url_modal(api_key: string): void { get_options: get_options_for_stream_dropdown_widget, item_click_callback: stream_item_click_callback, $events_container: $("#generate-integration-url-modal"), - tippy_props: { - placement: "bottom-start", - }, default_id: direct_messages_option.unique_id, unique_id_type: dropdown_widget.DataTypes.NUMBER, }); diff --git a/web/src/settings_org.js b/web/src/settings_org.js index 2712a4aa40..e4deb8c4cc 100644 --- a/web/src/settings_org.js +++ b/web/src/settings_org.js @@ -794,9 +794,6 @@ function set_up_dropdown_widget( custom_dropdown_widget_callback(this.current_value); } }, - tippy_props: { - placement: "bottom-start", - }, default_id: realm[setting_name], unique_id_type, text_if_current_value_not_in_options, diff --git a/web/src/settings_streams.ts b/web/src/settings_streams.ts index f566afb6fb..0420928aca 100644 --- a/web/src/settings_streams.ts +++ b/web/src/settings_streams.ts @@ -76,9 +76,6 @@ function create_choice_row(): void { get_options, item_click_callback, $events_container: $container, - tippy_props: { - placement: "bottom-start", - }, }).setup(); } diff --git a/web/src/settings_users.js b/web/src/settings_users.js index e691d0510f..aaf5a3110f 100644 --- a/web/src/settings_users.js +++ b/web/src/settings_users.js @@ -170,7 +170,6 @@ function create_role_filter_dropdown($events_container, widget_name) { item_click_callback: role_selected_handler, default_id: "0", tippy_props: { - placement: "bottom-start", offset: [0, 0], }, }).setup(); diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index 8ed0ce5076..149229860f 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -228,9 +228,6 @@ function setup_dropdown(sub, slim_sub) { ); }, $events_container: $("#subscription_overlay .subscription_settings"), - tippy_props: { - placement: "bottom-start", - }, default_id: sub.can_remove_subscribers_group, unique_id_type: dropdown_widget.DataTypes.NUMBER, on_mount_callback(dropdown) { diff --git a/web/src/stream_popover.js b/web/src/stream_popover.js index c9b2130e17..5dec8e4ce4 100644 --- a/web/src/stream_popover.js +++ b/web/src/stream_popover.js @@ -600,7 +600,6 @@ export async function build_move_topic_to_stream_popover( $events_container: $("#move_topic_modal"), tippy_props: { // Overlap dropdown search input with stream selection button. - placement: "bottom-start", offset: [0, -30], }, }).setup(); diff --git a/web/src/stream_settings_components.js b/web/src/stream_settings_components.js index 239c4f1a67..b470fef3d7 100644 --- a/web/src/stream_settings_components.js +++ b/web/src/stream_settings_components.js @@ -103,9 +103,6 @@ export function dropdown_setup() { new_stream_can_remove_subscribers_group_widget.render(); }, $events_container: $("#subscription_overlay"), - tippy_props: { - placement: "bottom-start", - }, on_mount_callback(dropdown) { $(dropdown.popper).css("min-width", "300px"); $(dropdown.popper).find(".simplebar-content").css("width", "max-content"); diff --git a/web/src/user_group_components.ts b/web/src/user_group_components.ts index 4bc35afb4d..966ac53f8e 100644 --- a/web/src/user_group_components.ts +++ b/web/src/user_group_components.ts @@ -45,9 +45,6 @@ export function setup_permissions_dropdown( } }, $events_container: $("#groups_overlay .group-permissions"), - tippy_props: { - placement: "bottom-start", - }, default_id, unique_id_type: dropdown_widget.DataTypes.NUMBER, on_mount_callback(dropdown) { diff --git a/web/src/user_profile.js b/web/src/user_profile.js index fdea9e98b9..12ae4e8673 100644 --- a/web/src/user_profile.js +++ b/web/src/user_profile.js @@ -163,9 +163,6 @@ function render_user_profile_subscribe_widget() { get_options: get_user_unsub_streams, item_click_callback: change_state_of_subscribe_button, $events_container: $("#user-profile-modal"), - tippy_props: { - placement: "bottom-start", - }, }; user_profile_subscribe_widget = user_profile_subscribe_widget || new dropdown_widget.DropdownWidget(opts); @@ -703,9 +700,6 @@ export function show_edit_bot_info_modal(user_id, $container) { get_options, item_click_callback, $events_container: $("#bot-edit-form"), - tippy_props: { - placement: "bottom-start", - }, default_id: owner_id, unique_id_type: dropdown_widget.DataTypes.NUMBER, }); diff --git a/web/src/views_util.ts b/web/src/views_util.ts index 9202751796..630df6c59e 100644 --- a/web/src/views_util.ts +++ b/web/src/views_util.ts @@ -23,7 +23,6 @@ export const FILTERS = { }; const TIPPY_PROPS: Partial = { - placement: "bottom-start", offset: [0, 2], };