popovers: Extract `user_group_popover` into separate module.

This is a preparatory commit before we migrate `user_group_popover`
from Bootstrap to Tippy library.

The previous implementation was weirdly sharing the logic around
`current_message_info_popover_elem` with the user info popovers based
on a message; very likely an unfortunate latent bug caused by
copy/paste.

To address that, we need to add dedicated functions like
get_user_group_popover_items to avoid breaking keyboard navigation
with this extraction.
This commit is contained in:
Daniil Fadeev 2023-09-08 18:44:48 +04:00 committed by Tim Abbott
parent 1765ce23b0
commit 7777c55b22
5 changed files with 151 additions and 91 deletions

View File

@ -222,6 +222,7 @@ EXEMPT_FILES = make_set(
"web/src/user_group_create_members_data.ts", "web/src/user_group_create_members_data.ts",
"web/src/user_group_edit.js", "web/src/user_group_edit.js",
"web/src/user_group_edit_members.js", "web/src/user_group_edit_members.js",
"web/src/user_group_popover.js",
"web/src/user_group_ui_updates.js", "web/src/user_group_ui_updates.js",
"web/src/user_groups.ts", "web/src/user_groups.ts",
"web/src/user_groups_settings_ui.js", "web/src/user_groups_settings_ui.js",

View File

@ -48,6 +48,7 @@ import * as stream_popover from "./stream_popover";
import * as stream_settings_ui from "./stream_settings_ui"; import * as stream_settings_ui from "./stream_settings_ui";
import * as topic_zoom from "./topic_zoom"; import * as topic_zoom from "./topic_zoom";
import * as unread_ops from "./unread_ops"; import * as unread_ops from "./unread_ops";
import * as user_group_popover from "./user_group_popover";
import {user_settings} from "./user_settings"; import {user_settings} from "./user_settings";
import * as user_topics_ui from "./user_topics_ui"; import * as user_topics_ui from "./user_topics_ui";
@ -386,6 +387,11 @@ function handle_popover_events(event_name) {
return true; return true;
} }
if (user_group_popover.is_open()) {
user_group_popover.handle_keyboard(event_name);
return true;
}
return false; return false;
} }

View File

@ -6,8 +6,6 @@ import url_template_lib from "url-template";
import render_no_arrow_popover from "../templates/no_arrow_popover.hbs"; import render_no_arrow_popover from "../templates/no_arrow_popover.hbs";
import render_playground_links_popover_content from "../templates/playground_links_popover_content.hbs"; import render_playground_links_popover_content from "../templates/playground_links_popover_content.hbs";
import render_user_group_info_popover from "../templates/user_group_info_popover.hbs";
import render_user_group_info_popover_content from "../templates/user_group_info_popover_content.hbs";
import render_user_info_popover_content from "../templates/user_info_popover_content.hbs"; import render_user_info_popover_content from "../templates/user_info_popover_content.hbs";
import render_user_info_popover_manage_menu from "../templates/user_info_popover_manage_menu.hbs"; import render_user_info_popover_manage_menu from "../templates/user_info_popover_manage_menu.hbs";
import render_user_info_popover_title from "../templates/user_info_popover_title.hbs"; import render_user_info_popover_title from "../templates/user_info_popover_title.hbs";
@ -40,12 +38,11 @@ import * as settings_users from "./settings_users";
import * as stream_popover from "./stream_popover"; import * as stream_popover from "./stream_popover";
import * as timerender from "./timerender"; import * as timerender from "./timerender";
import * as ui_report from "./ui_report"; import * as ui_report from "./ui_report";
import * as user_groups from "./user_groups"; import * as user_group_popover from "./user_group_popover";
import * as user_profile from "./user_profile"; import * as user_profile from "./user_profile";
import {user_settings} from "./user_settings"; import {user_settings} from "./user_settings";
import * as user_status from "./user_status"; import * as user_status from "./user_status";
import * as user_status_ui from "./user_status_ui"; import * as user_status_ui from "./user_status_ui";
import * as util from "./util";
let $current_message_info_popover_elem; let $current_message_info_popover_elem;
let $current_user_info_popover_elem; let $current_user_info_popover_elem;
@ -162,20 +159,6 @@ function load_medium_avatar(user, $elt) {
}); });
} }
function calculate_info_popover_placement(size, $elt) {
const ypos = $elt.get_offset_to_window().top;
if (!(ypos + size / 2 < message_viewport.height() && ypos > size / 2)) {
if (ypos + size < message_viewport.height()) {
return "bottom";
} else if (ypos > size) {
return "top";
}
}
return undefined;
}
export function hide_user_info_popover_manage_menu() { export function hide_user_info_popover_manage_menu() {
if ($current_user_info_popover_manage_menu !== undefined) { if ($current_user_info_popover_manage_menu !== undefined) {
$current_user_info_popover_manage_menu.popover("destroy"); $current_user_info_popover_manage_menu.popover("destroy");
@ -342,9 +325,6 @@ function render_user_info_popover(
load_medium_avatar(user, $(".popover-avatar")); load_medium_avatar(user, $(".popover-avatar"));
} }
// exporting for testability
export const _test_calculate_info_popover_placement = calculate_info_popover_placement;
// element is the target element to pop off of // element is the target element to pop off of
// user is the user whose profile to show // user is the user whose profile to show
// message is the message containing it, which should be selected // message is the message containing it, which should be selected
@ -448,62 +428,6 @@ function get_user_info_popover_manage_menu_items() {
return $(".user_info_popover_manage_menu li:not(.divider):visible a", popover_data.$tip); return $(".user_info_popover_manage_menu li:not(.divider):visible a", popover_data.$tip);
} }
function fetch_group_members(member_ids) {
return member_ids
.map((m) => people.maybe_get_user_by_id(m))
.filter((m) => m !== undefined)
.map((p) => ({
...p,
user_circle_class: buddy_data.get_user_circle_class(p.user_id),
is_active: people.is_active_user_for_popover(p.user_id),
user_last_seen_time_status: buddy_data.user_last_seen_time_status(p.user_id),
}));
}
function sort_group_members(members) {
return members.sort((a, b) => util.strcmp(a.full_name, b.fullname));
}
// exporting these functions for testing purposes
export const _test_fetch_group_members = fetch_group_members;
export const _test_sort_group_members = sort_group_members;
// element is the target element to pop off of
// user is the user whose profile to show
// message is the message containing it, which should be selected
function show_user_group_info_popover(element, group, message) {
const $last_popover_elem = $current_message_info_popover_elem;
// hardcoded pixel height of the popover
// note that the actual size varies (in group size), but this is about as big as it gets
const popover_size = 390;
hide_all();
if ($last_popover_elem !== undefined && $last_popover_elem.get()[0] === element) {
// We want it to be the case that a user can dismiss a popover
// by clicking on the same element that caused the popover.
return;
}
message_lists.current.select_id(message.id);
const $elt = $(element);
if ($elt.data("popover") === undefined) {
const args = {
group_name: group.name,
group_description: group.description,
members: sort_group_members(fetch_group_members([...group.members])),
};
$elt.popover({
placement: calculate_info_popover_placement(popover_size, $elt),
template: render_user_group_info_popover({class: "message-info-popover"}),
content: render_user_group_info_popover_content(args),
html: true,
trigger: "manual",
fixed: true,
});
$elt.popover("show");
$current_message_info_popover_elem = $elt;
}
}
function get_action_menu_menu_items() { function get_action_menu_menu_items() {
const $current_actions_popover_elem = $("[data-tippy-root] .actions_popover"); const $current_actions_popover_elem = $("[data-tippy-root] .actions_popover");
if (!$current_actions_popover_elem) { if (!$current_actions_popover_elem) {
@ -760,20 +684,6 @@ export function register_click_handlers() {
show_user_info_popover_for_message(this, user, message); show_user_info_popover_for_message(this, user, message);
}); });
$("#main_div").on("click", ".user-group-mention", function (e) {
const user_group_id = Number.parseInt($(this).attr("data-user-group-id"), 10);
const $row = $(this).closest(".message_row");
e.stopPropagation();
const message = message_lists.current.get(rows.id($row));
try {
const group = user_groups.get_user_group_from_id(user_group_id);
show_user_group_info_popover(this, group, message);
} catch {
// This user group has likely been deleted.
blueslip.info("Unable to find user group in message" + message.sender_id);
}
});
$("#main_div, #preview_content, #message-history").on( $("#main_div, #preview_content, #message-history").on(
"click", "click",
".code_external_link", ".code_external_link",
@ -1066,6 +976,7 @@ export function any_active() {
// Expanded sidebars on mobile view count as popovers as well. // Expanded sidebars on mobile view count as popovers as well.
return ( return (
popover_menus.any_active() || popover_menus.any_active() ||
user_group_popover.is_open() ||
user_sidebar_popped() || user_sidebar_popped() ||
stream_popover.stream_popped() || stream_popover.stream_popped() ||
message_info_popped() || message_info_popped() ||
@ -1086,6 +997,7 @@ export function hide_all_except_sidebars(opts) {
} }
emoji_picker.hide_emoji_popover(); emoji_picker.hide_emoji_popover();
stream_popover.hide_stream_popover(); stream_popover.hide_stream_popover();
user_group_popover.hide();
hide_all_user_info_popovers(); hide_all_user_info_popovers();
hide_playground_links_popover(); hide_playground_links_popover();

View File

@ -117,6 +117,7 @@ import * as unread_ui from "./unread_ui";
import * as upload from "./upload"; import * as upload from "./upload";
import * as user_group_edit from "./user_group_edit"; import * as user_group_edit from "./user_group_edit";
import * as user_group_edit_members from "./user_group_edit_members"; import * as user_group_edit_members from "./user_group_edit_members";
import * as user_group_popover from "./user_group_popover";
import * as user_groups from "./user_groups"; import * as user_groups from "./user_groups";
import * as user_group_settings_ui from "./user_groups_settings_ui"; import * as user_group_settings_ui from "./user_groups_settings_ui";
import {initialize_user_settings, user_settings} from "./user_settings"; import {initialize_user_settings, user_settings} from "./user_settings";
@ -761,6 +762,7 @@ export function initialize_everything() {
initialize_unread_ui(); initialize_unread_ui();
activity.initialize(); activity.initialize();
emoji_picker.initialize(); emoji_picker.initialize();
user_group_popover.initialize();
pm_list.initialize(); pm_list.initialize();
topic_list.initialize({ topic_list.initialize({
on_topic_click(stream_id, topic) { on_topic_click(stream_id, topic) {

View File

@ -0,0 +1,139 @@
import $ from "jquery";
import render_user_group_info_popover from "../templates/user_group_info_popover.hbs";
import render_user_group_info_popover_content from "../templates/user_group_info_popover_content.hbs";
import * as blueslip from "./blueslip";
import * as buddy_data from "./buddy_data";
import * as message_lists from "./message_lists";
import * as message_viewport from "./message_viewport";
import * as people from "./people";
import {hide_all, popover_items_handle_keyboard} from "./popovers";
import * as rows from "./rows";
import * as user_groups from "./user_groups";
import * as util from "./util";
let $current_user_group_popover_elem;
export function hide() {
if (is_open()) {
$current_user_group_popover_elem.popover("destroy");
$current_user_group_popover_elem = undefined;
}
}
export function is_open() {
return $current_user_group_popover_elem !== undefined;
}
function get_user_group_popover_items() {
if (!$current_user_group_popover_elem) {
blueslip.error("Trying to get menu items when action popover is closed.");
return undefined;
}
const popover_data = $current_user_group_popover_elem.data("popover");
if (!popover_data) {
blueslip.error("Cannot find popover data for actions menu.");
return undefined;
}
return $("li:not(.divider):visible a", popover_data.$tip);
}
export function handle_keyboard(key) {
const $items = get_user_group_popover_items();
popover_items_handle_keyboard(key, $items);
}
// element is the target element to pop off of
// user is the user whose profile to show
// message is the message containing it, which should be selected
export function show_user_group_info_popover(element, group, message) {
const $last_popover_elem = $current_user_group_popover_elem;
// hardcoded pixel height of the popover
// note that the actual size varies (in group size), but this is about as big as it gets
const popover_size = 390;
hide_all();
if ($last_popover_elem !== undefined && $last_popover_elem.get()[0] === element) {
// We want it to be the case that a user can dismiss a popover
// by clicking on the same element that caused the popover.
return;
}
message_lists.current.select_id(message.id);
const $elt = $(element);
if ($elt.data("popover") === undefined) {
const args = {
group_name: group.name,
group_description: group.description,
members: sort_group_members(fetch_group_members([...group.members])),
};
$elt.popover({
placement: calculate_info_popover_placement(popover_size, $elt),
template: render_user_group_info_popover({class: "message-info-popover"}),
content: render_user_group_info_popover_content(args),
html: true,
trigger: "manual",
fixed: true,
});
$elt.popover("show");
$current_user_group_popover_elem = $elt;
}
}
export function register_click_handlers() {
$("#main_div").on("click", ".user-group-mention", function (e) {
const user_group_id = Number.parseInt($(this).attr("data-user-group-id"), 10);
const $row = $(this).closest(".message_row");
e.stopPropagation();
const message = message_lists.current.get(rows.id($row));
try {
const group = user_groups.get_user_group_from_id(user_group_id);
show_user_group_info_popover(this, group, message);
} catch {
// This user group has likely been deleted.
blueslip.info("Unable to find user group in message" + message.sender_id);
}
});
}
function fetch_group_members(member_ids) {
return member_ids
.map((m) => people.maybe_get_user_by_id(m))
.filter((m) => m !== undefined)
.map((p) => ({
...p,
user_circle_class: buddy_data.get_user_circle_class(p.user_id),
is_active: people.is_active_user_for_popover(p.user_id),
user_last_seen_time_status: buddy_data.user_last_seen_time_status(p.user_id),
}));
}
function sort_group_members(members) {
return members.sort((a, b) => util.strcmp(a.full_name, b.fullname));
}
function calculate_info_popover_placement(size, $elt) {
const ypos = $elt.get_offset_to_window().top;
if (!(ypos + size / 2 < message_viewport.height() && ypos > size / 2)) {
if (ypos + size < message_viewport.height()) {
return "bottom";
} else if (ypos > size) {
return "top";
}
}
return undefined;
}
// exporting these functions for testing purposes
export const _test_fetch_group_members = fetch_group_members;
export const _test_sort_group_members = sort_group_members;
export const _test_calculate_info_popover_placement = calculate_info_popover_placement;
export function initialize() {
register_click_handlers();
}