mirror of
https://github.com/zulip/zulip.git
synced 2025-11-10 08:56:10 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -76,9 +76,6 @@ function create_choice_row(): void {
|
||||
get_options,
|
||||
item_click_callback,
|
||||
$events_container: $container,
|
||||
tippy_props: {
|
||||
placement: "bottom-start",
|
||||
},
|
||||
}).setup();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -23,7 +23,6 @@ export const FILTERS = {
|
||||
};
|
||||
|
||||
const TIPPY_PROPS: Partial<tippy.Props> = {
|
||||
placement: "bottom-start",
|
||||
offset: [0, 2],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user