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;
|
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 {
|
export function open_compose_recipient_dropdown(): void {
|
||||||
$("#compose_select_recipient_widget").trigger("click");
|
$("#compose_select_recipient_widget").trigger("click");
|
||||||
}
|
}
|
||||||
@@ -330,11 +303,15 @@ export function initialize(): void {
|
|||||||
get_options: get_options_for_recipient_widget,
|
get_options: get_options_for_recipient_widget,
|
||||||
item_click_callback,
|
item_click_callback,
|
||||||
$events_container: $("body"),
|
$events_container: $("body"),
|
||||||
on_show_callback: compose_recipient_dropdown_on_show,
|
|
||||||
on_exit_with_escape_callback: focus_compose_recipient,
|
on_exit_with_escape_callback: focus_compose_recipient,
|
||||||
// We want to focus on topic box if dropdown was closed via selecting an item.
|
// We want to focus on topic box if dropdown was closed via selecting an item.
|
||||||
focus_target_on_hidden: false,
|
focus_target_on_hidden: false,
|
||||||
on_hidden_callback,
|
on_hidden_callback,
|
||||||
|
dropdown_input_visible_selector: "#compose_select_recipient_widget_wrapper",
|
||||||
|
prefer_top_start_placement: true,
|
||||||
|
tippy_props: {
|
||||||
|
offset: [-10, 5],
|
||||||
|
},
|
||||||
}).setup();
|
}).setup();
|
||||||
|
|
||||||
// `input` isn't relevant for streams since it registers as a change only
|
// `input` isn't relevant for streams since it registers as a change only
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ type DropdownWidgetOptions = {
|
|||||||
hide_search_box?: boolean;
|
hide_search_box?: boolean;
|
||||||
// Disable the widget for spectators.
|
// Disable the widget for spectators.
|
||||||
disable_for_spectators?: boolean;
|
disable_for_spectators?: boolean;
|
||||||
|
dropdown_input_visible_selector?: string;
|
||||||
|
prefer_top_start_placement?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class DropdownWidget {
|
export class DropdownWidget {
|
||||||
@@ -96,6 +98,8 @@ export class DropdownWidget {
|
|||||||
text_if_current_value_not_in_options: string;
|
text_if_current_value_not_in_options: string;
|
||||||
hide_search_box: boolean;
|
hide_search_box: boolean;
|
||||||
disable_for_spectators: boolean;
|
disable_for_spectators: boolean;
|
||||||
|
dropdown_input_visible_selector: string;
|
||||||
|
prefer_top_start_placement: boolean;
|
||||||
|
|
||||||
constructor(options: DropdownWidgetOptions) {
|
constructor(options: DropdownWidgetOptions) {
|
||||||
this.widget_name = options.widget_name;
|
this.widget_name = options.widget_name;
|
||||||
@@ -123,6 +127,9 @@ export class DropdownWidget {
|
|||||||
options.text_if_current_value_not_in_options ?? "";
|
options.text_if_current_value_not_in_options ?? "";
|
||||||
this.hide_search_box = options.hide_search_box ?? false;
|
this.hide_search_box = options.hide_search_box ?? false;
|
||||||
this.disable_for_spectators = options.disable_for_spectators ?? 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 {
|
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 {
|
show_empty_if_no_items($popper: JQuery): void {
|
||||||
assert(this.list_widget !== undefined);
|
assert(this.list_widget !== undefined);
|
||||||
const list_items = this.list_widget.get_current_list();
|
const list_items = this.list_widget.get_current_list();
|
||||||
@@ -361,6 +419,7 @@ export class DropdownWidget {
|
|||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
this.on_show_callback(instance);
|
this.on_show_callback(instance);
|
||||||
|
this.adjust_dropdown_position_post_list_render(instance);
|
||||||
},
|
},
|
||||||
onMount: (instance: tippy.Instance) => {
|
onMount: (instance: tippy.Instance) => {
|
||||||
this.show_empty_if_no_items($(instance.popper));
|
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,
|
get_options: get_options_for_integration_input_dropdown_widget,
|
||||||
item_click_callback: integration_item_click_callback,
|
item_click_callback: integration_item_click_callback,
|
||||||
$events_container: $("#generate-integration-url-modal"),
|
$events_container: $("#generate-integration-url-modal"),
|
||||||
tippy_props: {
|
|
||||||
placement: "bottom-start",
|
|
||||||
},
|
|
||||||
default_id: default_integration_option.unique_id,
|
default_id: default_integration_option.unique_id,
|
||||||
unique_id_type: dropdown_widget.DataTypes.STRING,
|
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,
|
get_options: get_options_for_stream_dropdown_widget,
|
||||||
item_click_callback: stream_item_click_callback,
|
item_click_callback: stream_item_click_callback,
|
||||||
$events_container: $("#generate-integration-url-modal"),
|
$events_container: $("#generate-integration-url-modal"),
|
||||||
tippy_props: {
|
|
||||||
placement: "bottom-start",
|
|
||||||
},
|
|
||||||
default_id: direct_messages_option.unique_id,
|
default_id: direct_messages_option.unique_id,
|
||||||
unique_id_type: dropdown_widget.DataTypes.NUMBER,
|
unique_id_type: dropdown_widget.DataTypes.NUMBER,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -794,9 +794,6 @@ function set_up_dropdown_widget(
|
|||||||
custom_dropdown_widget_callback(this.current_value);
|
custom_dropdown_widget_callback(this.current_value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tippy_props: {
|
|
||||||
placement: "bottom-start",
|
|
||||||
},
|
|
||||||
default_id: realm[setting_name],
|
default_id: realm[setting_name],
|
||||||
unique_id_type,
|
unique_id_type,
|
||||||
text_if_current_value_not_in_options,
|
text_if_current_value_not_in_options,
|
||||||
|
|||||||
@@ -76,9 +76,6 @@ function create_choice_row(): void {
|
|||||||
get_options,
|
get_options,
|
||||||
item_click_callback,
|
item_click_callback,
|
||||||
$events_container: $container,
|
$events_container: $container,
|
||||||
tippy_props: {
|
|
||||||
placement: "bottom-start",
|
|
||||||
},
|
|
||||||
}).setup();
|
}).setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ function create_role_filter_dropdown($events_container, widget_name) {
|
|||||||
item_click_callback: role_selected_handler,
|
item_click_callback: role_selected_handler,
|
||||||
default_id: "0",
|
default_id: "0",
|
||||||
tippy_props: {
|
tippy_props: {
|
||||||
placement: "bottom-start",
|
|
||||||
offset: [0, 0],
|
offset: [0, 0],
|
||||||
},
|
},
|
||||||
}).setup();
|
}).setup();
|
||||||
|
|||||||
@@ -228,9 +228,6 @@ function setup_dropdown(sub, slim_sub) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
$events_container: $("#subscription_overlay .subscription_settings"),
|
$events_container: $("#subscription_overlay .subscription_settings"),
|
||||||
tippy_props: {
|
|
||||||
placement: "bottom-start",
|
|
||||||
},
|
|
||||||
default_id: sub.can_remove_subscribers_group,
|
default_id: sub.can_remove_subscribers_group,
|
||||||
unique_id_type: dropdown_widget.DataTypes.NUMBER,
|
unique_id_type: dropdown_widget.DataTypes.NUMBER,
|
||||||
on_mount_callback(dropdown) {
|
on_mount_callback(dropdown) {
|
||||||
|
|||||||
@@ -600,7 +600,6 @@ export async function build_move_topic_to_stream_popover(
|
|||||||
$events_container: $("#move_topic_modal"),
|
$events_container: $("#move_topic_modal"),
|
||||||
tippy_props: {
|
tippy_props: {
|
||||||
// Overlap dropdown search input with stream selection button.
|
// Overlap dropdown search input with stream selection button.
|
||||||
placement: "bottom-start",
|
|
||||||
offset: [0, -30],
|
offset: [0, -30],
|
||||||
},
|
},
|
||||||
}).setup();
|
}).setup();
|
||||||
|
|||||||
@@ -103,9 +103,6 @@ export function dropdown_setup() {
|
|||||||
new_stream_can_remove_subscribers_group_widget.render();
|
new_stream_can_remove_subscribers_group_widget.render();
|
||||||
},
|
},
|
||||||
$events_container: $("#subscription_overlay"),
|
$events_container: $("#subscription_overlay"),
|
||||||
tippy_props: {
|
|
||||||
placement: "bottom-start",
|
|
||||||
},
|
|
||||||
on_mount_callback(dropdown) {
|
on_mount_callback(dropdown) {
|
||||||
$(dropdown.popper).css("min-width", "300px");
|
$(dropdown.popper).css("min-width", "300px");
|
||||||
$(dropdown.popper).find(".simplebar-content").css("width", "max-content");
|
$(dropdown.popper).find(".simplebar-content").css("width", "max-content");
|
||||||
|
|||||||
@@ -45,9 +45,6 @@ export function setup_permissions_dropdown(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
$events_container: $("#groups_overlay .group-permissions"),
|
$events_container: $("#groups_overlay .group-permissions"),
|
||||||
tippy_props: {
|
|
||||||
placement: "bottom-start",
|
|
||||||
},
|
|
||||||
default_id,
|
default_id,
|
||||||
unique_id_type: dropdown_widget.DataTypes.NUMBER,
|
unique_id_type: dropdown_widget.DataTypes.NUMBER,
|
||||||
on_mount_callback(dropdown) {
|
on_mount_callback(dropdown) {
|
||||||
|
|||||||
@@ -163,9 +163,6 @@ function render_user_profile_subscribe_widget() {
|
|||||||
get_options: get_user_unsub_streams,
|
get_options: get_user_unsub_streams,
|
||||||
item_click_callback: change_state_of_subscribe_button,
|
item_click_callback: change_state_of_subscribe_button,
|
||||||
$events_container: $("#user-profile-modal"),
|
$events_container: $("#user-profile-modal"),
|
||||||
tippy_props: {
|
|
||||||
placement: "bottom-start",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
user_profile_subscribe_widget =
|
user_profile_subscribe_widget =
|
||||||
user_profile_subscribe_widget || new dropdown_widget.DropdownWidget(opts);
|
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,
|
get_options,
|
||||||
item_click_callback,
|
item_click_callback,
|
||||||
$events_container: $("#bot-edit-form"),
|
$events_container: $("#bot-edit-form"),
|
||||||
tippy_props: {
|
|
||||||
placement: "bottom-start",
|
|
||||||
},
|
|
||||||
default_id: owner_id,
|
default_id: owner_id,
|
||||||
unique_id_type: dropdown_widget.DataTypes.NUMBER,
|
unique_id_type: dropdown_widget.DataTypes.NUMBER,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export const FILTERS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TIPPY_PROPS: Partial<tippy.Props> = {
|
const TIPPY_PROPS: Partial<tippy.Props> = {
|
||||||
placement: "bottom-start",
|
|
||||||
offset: [0, 2],
|
offset: [0, 2],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user