dropdown: Fix dropdown partially hidden at small heights.

We generalize the approach used for compose box to apply it for
all the dropdowns.
This commit is contained in:
Aman Agrawal
2024-07-15 12:59:48 +00:00
committed by Tim Abbott
parent 51eebf84e7
commit b91b643448
12 changed files with 64 additions and 58 deletions

View File

@@ -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

View File

@@ -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));

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -76,9 +76,6 @@ function create_choice_row(): void {
get_options,
item_click_callback,
$events_container: $container,
tippy_props: {
placement: "bottom-start",
},
}).setup();
}

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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");

View File

@@ -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) {

View File

@@ -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,
});

View File

@@ -23,7 +23,6 @@ export const FILTERS = {
};
const TIPPY_PROPS: Partial<tippy.Props> = {
placement: "bottom-start",
offset: [0, 2],
};