diff --git a/static/js/popover_menus.js b/static/js/popover_menus.js index 8daa918a3a..0854d8a1fe 100644 --- a/static/js/popover_menus.js +++ b/static/js/popover_menus.js @@ -3,7 +3,7 @@ popovers system in popovers.js. */ import $ from "jquery"; -import {delegate} from "tippy.js"; +import tippy, {delegate} from "tippy.js"; import render_compose_control_buttons_popover from "../templates/compose_control_buttons_popover.hbs"; import render_compose_select_enter_behaviour_popover from "../templates/compose_select_enter_behaviour_popover.hbs"; @@ -67,10 +67,39 @@ function on_show_prep(instance) { popovers.hide_all_except_sidebars(instance); } +function tippy_no_propagation(target, popover_props) { + // For some elements, such as the click target to open the message + // actions menu, we want to avoid propagating the click event to + // parent elements. Tippy's built-in `delegate` method does not + // have an option to do stopPropagation, so we use this method to + // open the Tippy popovers associated with such elements. + // + // A click on the click target will close the menu; for this to + // work correctly without leaking, all callers need call + // `instance.destroy()` inside their `onHidden` handler. + // + // TODO: Should we instead we wrap the caller's `onHidden` hook, + // if any, to add `instance.destroy()`? + $("body").on("click", target, (e) => { + e.preventDefault(); + e.stopPropagation(); + const instance = e.currentTarget._tippy; + + if (instance) { + instance.hide(); + return; + } + + tippy(e.currentTarget, { + ...default_popover_props, + showOnCreate: true, + ...popover_props, + }); + }); +} + export function initialize() { - delegate("body", { - ...default_popover_props, - target: "#streams_inline_icon", + tippy_no_propagation("#streams_inline_icon", { onShow(instance) { const can_create_streams = settings_data.user_can_create_private_streams() || @@ -90,12 +119,15 @@ export function initialize() { left_sidebar_stream_setting_popover_displayed = true; return true; }, - onHidden() { + onHidden(instance) { + instance.destroy(); left_sidebar_stream_setting_popover_displayed = false; }, }); // compose box buttons popover shown on mobile widths. + // We want this click event to propagate and hide other popovers + // that could possibly obstruct user from using this popover. delegate("body", { ...default_popover_props, target: ".compose_mobile_button", @@ -135,9 +167,7 @@ export function initialize() { // Click event handlers for it are handled in `compose_ui` and // we don't want to close this popover on click inside it but // only if user clicked outside it. - delegate("body", { - ...default_popover_props, - target: ".compose_control_menu_wrapper", + tippy_no_propagation(".compose_control_menu_wrapper", { placement: "top", onShow(instance) { instance.setContent( @@ -150,14 +180,13 @@ export function initialize() { compose_control_buttons_popover_instance = instance; popovers.hide_all_except_sidebars(instance); }, - onHidden() { + onHidden(instance) { + instance.destroy(); compose_control_buttons_popover_instance = undefined; }, }); - delegate("body", { - ...default_popover_props, - target: ".enter_sends", + tippy_no_propagation(".enter_sends", { placement: "top", onShow(instance) { on_show_prep(instance);