diff --git a/web/e2e-tests/lib/common.ts b/web/e2e-tests/lib/common.ts index aadf8518ce..fad20aa123 100644 --- a/web/e2e-tests/lib/common.ts +++ b/web/e2e-tests/lib/common.ts @@ -210,7 +210,10 @@ export async function check_compose_state( const form_params: Record = {content: params.content}; if (params.stream) { assert.equal( - await get_text_from_selector(page, "#compose_select_recipient_name"), + await get_text_from_selector( + page, + "#compose_select_recipient_widget .dropdown_widget_value", + ), params.stream, ); } diff --git a/web/src/click_handlers.js b/web/src/click_handlers.js index e354be1632..b927db2f36 100644 --- a/web/src/click_handlers.js +++ b/web/src/click_handlers.js @@ -709,7 +709,7 @@ export function initialize() { } // The dropdown menu needs to process clicks to open and close. - if ($target.parents("#compose_recipient_selection_dropdown").length > 0) { + if ($target.parents("#compose_select_recipient_widget_wrapper").length > 0) { return; } diff --git a/web/src/compose_actions.js b/web/src/compose_actions.js index f269f4065a..66ad0e9325 100644 --- a/web/src/compose_actions.js +++ b/web/src/compose_actions.js @@ -225,7 +225,7 @@ export function start(msg_type, opts) { } const $stream_header_colorblock = $( - "#compose_recipient_selection_dropdown .stream_header_colorblock", + "#compose_select_recipient_widget_wrapper .stream_header_colorblock", ); stream_bar.decorate(opts.stream, $stream_header_colorblock); diff --git a/web/src/compose_recipient.js b/web/src/compose_recipient.js index a75e4bf833..fc90a740bb 100644 --- a/web/src/compose_recipient.js +++ b/web/src/compose_recipient.js @@ -165,9 +165,11 @@ function switch_message_type(message_type) { function update_recipient_label(stream_name) { const stream = stream_data.get_sub_by_name(stream_name); if (stream === undefined) { - $("#compose_select_recipient_name").text($t({defaultMessage: "Select a stream"})); + $("#compose_select_recipient_widget .dropdown_widget_value").text( + $t({defaultMessage: "Select a stream"}), + ); } else { - $("#compose_select_recipient_name").html( + $("#compose_select_recipient_widget .dropdown_widget_value").html( render_inline_decorated_stream_name({stream, show_colored_icon: true}), ); } @@ -192,7 +194,7 @@ export function update_compose_for_message_type(message_type, opts) { // the "DM" button display string so we wouldn't have to manually change // it here. const direct_message_label = $t({defaultMessage: "DM"}); - $("#compose_select_recipient_name").html( + $("#compose_select_recipient_widget .dropdown_widget_value").html( ` ${direct_message_label}`, ); } @@ -216,7 +218,7 @@ export function on_compose_select_recipient_update() { if (curr_message_type === "stream") { // Update stream name in the recipient box. const $stream_header_colorblock = $( - "#compose_recipient_selection_dropdown .stream_header_colorblock", + "#compose_select_recipient_widget_wrapper .stream_header_colorblock", ); const stream_name = compose_state.stream_name(); update_recipient_label(stream_name); @@ -270,7 +272,7 @@ function compose_recipient_dropdown_on_show(dropdown) { const window_height = window.innerHeight; const search_box_and_padding_height = 50; // pixels above compose box. - const recipient_input_top = $("#compose_recipient_selection_dropdown").get_offset_to_window() + 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. @@ -296,7 +298,7 @@ export function open_compose_recipient_dropdown() { } function focus_compose_recipient() { - $("#compose_recipient_selection_dropdown").trigger("focus"); + $("#compose_select_recipient_widget_wrapper").trigger("focus"); } // NOTE: Since tippy triggers this on `mousedown` it is always triggered before say a `click` on `textarea`. @@ -316,28 +318,17 @@ function on_hidden_callback() { } export function initialize() { - dropdown_widget.setup( - { - target: "#compose_select_recipient_widget", - }, - get_options_for_recipient_widget, + new dropdown_widget.DropdownWidget({ + widget_name: "compose_select_recipient", + get_options: get_options_for_recipient_widget, item_click_callback, - { - 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, - }, - ); - - $("#compose_recipient_selection_dropdown").on("keydown", (e) => { - if (e.key === "Enter") { - open_compose_recipient_dropdown(); - e.stopPropagation(); - e.preventDefault(); - } - }); + $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, + }).setup(); // `keyup` isn't relevant for streams since it registers as a change only // when an item in the dropdown is selected. diff --git a/web/src/compose_state.js b/web/src/compose_state.js index 7e258847be..6ea7f3709e 100644 --- a/web/src/compose_state.js +++ b/web/src/compose_state.js @@ -162,7 +162,7 @@ export function focus_in_empty_compose(consider_start_of_whitespace_message_empt return private_message_recipient().length === 0; case "stream_message_recipient_topic": return topic() === ""; - case "compose_select_recipient_name": + case "compose_select_recipient_widget_wrapper": return stream_name() === ""; } diff --git a/web/src/compose_ui.js b/web/src/compose_ui.js index 9ce109f169..73c9064a23 100644 --- a/web/src/compose_ui.js +++ b/web/src/compose_ui.js @@ -48,7 +48,7 @@ function get_focus_area(msg_type, opts) { } if (msg_type === "stream") { - return "#compose_select_recipient_widget"; + return "#compose_select_recipient_widget_wrapper"; } return "#private_message_recipient"; } diff --git a/web/src/compose_validate.js b/web/src/compose_validate.js index 61ec72161c..7bad3a7866 100644 --- a/web/src/compose_validate.js +++ b/web/src/compose_validate.js @@ -436,7 +436,7 @@ export function validation_error(error_type, stream_name) { $t({defaultMessage: "Error checking subscription."}), compose_banner.CLASSNAMES.subscription_error, $banner_container, - $("#compose_select_recipient_widget"), + $("#compose_select_recipient_widget_wrapper"), ); return false; case "not-subscribed": { @@ -485,7 +485,7 @@ function validate_stream_message(scheduling_message) { $t({defaultMessage: "Please specify a stream."}), compose_banner.CLASSNAMES.missing_stream, $banner_container, - $("#compose_select_recipient_widget"), + $("#compose_select_recipient_widget_wrapper"), ); return false; } diff --git a/web/src/dialog_widget.ts b/web/src/dialog_widget.ts index 558f87b28d..13c733902d 100644 --- a/web/src/dialog_widget.ts +++ b/web/src/dialog_widget.ts @@ -159,6 +159,8 @@ export function launch(conf: DialogWidgetConfig): void { if ($(this).is("input[type='file']") && $(this).prop("files")?.length) { // If the input is a file input and a file has been selected, set value to file object current_values[property_name] = $(this).prop("files")[0]; + } else if (property_name === "edit_bot_owner") { + current_values[property_name] = $(this).find(".dropdown_widget_value").text(); } else { current_values[property_name] = $(this).val(); } diff --git a/web/src/dropdown_widget.js b/web/src/dropdown_widget.js index 041620126f..cf4a6847fd 100644 --- a/web/src/dropdown_widget.js +++ b/web/src/dropdown_widget.js @@ -1,9 +1,12 @@ import $ from "jquery"; import * as tippy from "tippy.js"; +import render_dropdown_disabled_state from "../templates/dropdown_disabled_state.hbs"; import render_dropdown_list from "../templates/dropdown_list.hbs"; import render_dropdown_list_container from "../templates/dropdown_list_container.hbs"; +import render_inline_decorated_stream_name from "../templates/inline_decorated_stream_name.hbs"; +import * as blueslip from "./blueslip"; import * as ListWidget from "./list_widget"; import {default_popover_props} from "./popover_menus"; import {parse_html} from "./ui_util"; @@ -11,166 +14,258 @@ import {parse_html} from "./ui_util"; /* Sync with max-height set in zulip.css */ export const DEFAULT_DROPDOWN_HEIGHT = 210; const noop = () => {}; +export const DATA_TYPES = { + NUMBER: "number", + STRING: "string", +}; -export function setup(tippy_props, get_options, item_click_callback, dropdown_props = {}) { - // Define all possible `dropdown_props` here so that they are easy to track. - const on_show_callback = dropdown_props.on_show_callback || noop; - const on_hidden_callback = dropdown_props.on_hidden_callback || noop; - const on_exit_with_escape_callback = dropdown_props.on_exit_with_escape_callback || noop; - // Used to focus the `target` after dropdown is closed. This is important since the dropdown is - // appended to `body` and hence `body` is focused when the dropdown is closed, which makes - // it hard for the user to get focus back to the `target`. - const focus_target_on_hidden = dropdown_props.focus_target_on_hidden || true; - // Should enter keypress on target show the dropdown. - const show_on_target_enter_keypress = dropdown_props.show_on_target_enter_keypress || false; +export class DropdownWidget { + constructor({ + widget_name, + get_options, + item_click_callback, + // Provide an parent element to widget which will be re-rendered if the widget is setup again. + // It is important to not pass `$("body")` here for widgets that would be `setup()` + // multiple times, so that we don't have duplicate event handlers. + $events_container, + on_show_callback = noop, + on_mount_callback = noop, + on_hidden_callback = noop, + on_exit_with_escape_callback = noop, + render_selected_option = noop, + // Used to focus the `target` after dropdown is closed. This is important since the dropdown is + // appended to `body` and hence `body` is focused when the dropdown is closed, which makes + // it hard for the user to get focus back to the `target`. + focus_target_on_hidden = true, + tippy_props = {}, + // NOTE: Any value other than `null` will be rendered when class is initialized. + default_id = null, + unique_id_type = null, + }) { + this.widget_name = widget_name; + this.widget_id = `#${CSS.escape(widget_name)}_widget`; + // A widget wrapper may not exist based on the UI requirement. + this.widget_wrapper_id = `${this.widget_id}_wrapper`; + this.widget_value_selector = `${this.widget_id} .dropdown_widget_value`; + this.get_options = get_options; + this.item_click_callback = item_click_callback; + this.focus_target_on_hidden = focus_target_on_hidden; + this.on_show_callback = on_show_callback; + this.on_mount_callback = on_mount_callback; + this.on_hidden_callback = on_hidden_callback; + this.on_exit_with_escape_callback = on_exit_with_escape_callback; + this.render_selected_option = render_selected_option; + this.tippy_props = tippy_props; + this.list_widget = null; + this.instance = null; + this.default_id = default_id; + this.current_value = default_id; + this.unique_id_type = unique_id_type; + this.$events_container = $events_container; + } - if (show_on_target_enter_keypress) { - $("body").on("keypress", tippy_props.target, (e) => { - if (e.key === "Enter") { - $(tippy_props.target).trigger("click"); - e.stopPropagation(); - e.preventDefault(); - } + init() { + if (this.current_value !== null) { + this.render(); + } + + this.$events_container.on( + "keydown", + `${this.widget_id}, ${this.widget_wrapper_id}`, + (e) => { + if (e.key === "Enter") { + $(`${this.widget_id}`).trigger("click"); + e.stopPropagation(); + e.preventDefault(); + } + }, + ); + } + + setup() { + this.init(); + const delegate_container = this.$events_container.get(0); + if (!delegate_container) { + blueslip.error( + "Cannot initialize dropdown. `$events_container` empty.", + this.$events_container, + ); + } + this.instance = tippy.delegate(delegate_container, { + ...default_popover_props, + target: this.widget_id, + // Custom theme defined in popovers.css + theme: "dropdown-widget", + arrow: false, + onShow: function (instance) { + instance.setContent(parse_html(render_dropdown_list_container())); + const $popper = $(instance.popper); + const $dropdown_list_body = $popper.find(".dropdown-list"); + const $search_input = $popper.find(".dropdown-list-search-input"); + + this.list_widget = ListWidget.create($dropdown_list_body, this.get_options(), { + name: `${CSS.escape(this.widget_name)}-list-widget`, + get_item: ListWidget.default_get_item, + modifier(item) { + return render_dropdown_list({item}); + }, + filter: { + $element: $search_input, + predicate(item, value) { + return item.name.toLowerCase().includes(value); + }, + }, + $simplebar_container: $popper.find(".dropdown-list-wrapper"), + }); + + $search_input.on("input.list_widget_filter", () => { + const list_items = this.list_widget.get_current_list(); + const $no_search_results = $popper.find(".no-dropdown-items"); + if (list_items.length === 0) { + $no_search_results.show(); + } else { + $no_search_results.hide(); + } + }); + + // Keyboard handler + $popper.on("keydown", (e) => { + function trigger_element_focus($element) { + e.preventDefault(); + e.stopPropagation(); + // When bringing a non-visible element into view, scroll as minimum as possible. + $element[0]?.scrollIntoView({block: "nearest"}); + $element.trigger("focus"); + } + + const $search_input = $popper.find(".dropdown-list-search-input"); + const list_items = this.list_widget.get_current_list(); + if (list_items.length === 0 && !(e.key === "Escape")) { + // Let the browser handle it. + return; + } + + function first_item() { + const first_item = list_items[0]; + return $popper.find(`.list-item[data-unique-id="${first_item.unique_id}"]`); + } + + function last_item() { + const last_item = list_items.at(-1); + return $popper.find(`.list-item[data-unique-id="${last_item.unique_id}"]`); + } + + const render_all_items_and_focus_last_item = function () { + // List widget doesn't render all items by default, so we need to render all + // the items and focus on the last element. + const list_items = this.list_widget.get_current_list(); + this.list_widget.render(list_items.length); + trigger_element_focus(last_item()); + }.bind(this); + + switch (e.key) { + case "Enter": + if (e.target === $search_input.get(0)) { + // Select first item if in search input. + first_item().trigger("click"); + } else if (list_items.length !== 0) { + $(e.target).trigger("click"); + } + e.stopPropagation(); + e.preventDefault(); + break; + + case "Escape": + instance.hide(); + this.on_exit_with_escape_callback(); + e.stopPropagation(); + e.preventDefault(); + break; + + case "Tab": + case "ArrowDown": + switch (e.target) { + case last_item().get(0): + trigger_element_focus($search_input); + break; + case $search_input.get(0): + trigger_element_focus(first_item()); + break; + default: + trigger_element_focus($(e.target).next()); + } + break; + + case "ArrowUp": + switch (e.target) { + case first_item().get(0): + trigger_element_focus($search_input); + break; + case $search_input.get(0): + render_all_items_and_focus_last_item(); + break; + default: + trigger_element_focus($(e.target).prev()); + } + break; + } + }); + + // Click on item. + $popper.one("click", ".list-item", (event) => { + this.current_value = $(event.currentTarget).attr("data-unique-id"); + if (this.unique_id_type === DATA_TYPES.NUMBER) { + this.current_value = Number.parseInt(this.current_value, 10); + } + this.item_click_callback(event, instance); + }); + + // Set focus on search input when dropdown opens. + setTimeout(() => { + $(".dropdown-list-search-input").trigger("focus"); + }); + + this.on_show_callback(instance); + }.bind(this), + onMount: function (instance) { + this.on_mount_callback(instance); + }.bind(this), + onHidden: function (instance) { + if (this.focus_target_on_hidden) { + $(this.widget_id).trigger("focus"); + } + this.on_hidden_callback(instance); + this.instance = null; + }.bind(this), + ...this.tippy_props, }); } - tippy.delegate("body", { - ...default_popover_props, - // Custom theme defined in popovers.css - theme: "dropdown-widget", - arrow: false, - onShow(instance) { - instance.setContent(parse_html(render_dropdown_list_container())); - const $popper = $(instance.popper); - const $dropdown_list_body = $popper.find(".dropdown-list"); - const $search_input = $popper.find(".dropdown-list-search-input"); + value() { + return this.current_value; + } - const list_widget = ListWidget.create($dropdown_list_body, get_options(), { - name: `${CSS.escape(tippy_props.target)}-list-widget`, - get_item: ListWidget.default_get_item, - modifier(item) { - return render_dropdown_list({item}); - }, - filter: { - $element: $search_input, - predicate(item, value) { - return item.name.toLowerCase().includes(value); - }, - }, - $simplebar_container: $popper.find(".dropdown-list-wrapper"), - }); + // NOTE: This function needs to be explicitly called when you want to update the + // current value of the widget. We don't call this automatically since some of our + // dropdowns don't need it. Maybe we can follow a reverse approach in the future. + render(value) { + // Check if the value is valid otherwise just render previous value. + if (typeof value === typeof this.current_value) { + this.current_value = value; + } - $search_input.on("input.list_widget_filter", () => { - const list_items = list_widget.get_current_list(); - const $no_search_results = $popper.find(".no-dropdown-items"); - if (list_items.length === 0) { - $no_search_results.show(); - } else { - $no_search_results.hide(); - } - }); - - // Keyboard handler - $popper.on("keydown", (e) => { - function trigger_element_focus($element) { - e.preventDefault(); - e.stopPropagation(); - // When brining a non-visible element into view, scroll as minimum as possible. - $element[0]?.scrollIntoView({block: "nearest"}); - $element.trigger("focus"); - } - - const $search_input = $popper.find(".dropdown-list-search-input"); - const list_items = list_widget.get_current_list(); - if (list_items.length === 0 && !(e.key === "Escape")) { - // Let the browser handle it. - return; - } - - function first_item() { - const first_item = list_items[0]; - return $popper.find(`.list-item[data-unique-id="${first_item.unique_id}"]`); - } - - function last_item() { - const last_item = list_items.at(-1); - return $popper.find(`.list-item[data-unique-id="${last_item.unique_id}"]`); - } - - function render_all_items_and_focus_last_item() { - // List widget doesn't render all items by default, so we need to render all - // the items and focus on the last element. - const list_items = list_widget.get_current_list(); - list_widget.render(list_items.length); - trigger_element_focus(last_item()); - } - - switch (e.key) { - case "Enter": - if (e.target === $search_input.get(0)) { - // Select first item if in search input. - first_item().trigger("click"); - } else if (list_items.length !== 0) { - $(e.target).trigger("click"); - } - e.stopPropagation(); - e.preventDefault(); - break; - - case "Escape": - instance.hide(); - on_exit_with_escape_callback(); - e.stopPropagation(); - e.preventDefault(); - break; - - case "Tab": - case "ArrowDown": - switch (e.target) { - case last_item().get(0): - trigger_element_focus($search_input); - break; - case $search_input.get(0): - trigger_element_focus(first_item()); - break; - default: - trigger_element_focus($(e.target).next()); - } - break; - - case "ArrowUp": - switch (e.target) { - case first_item().get(0): - trigger_element_focus($search_input); - break; - case $search_input.get(0): - render_all_items_and_focus_last_item(); - break; - default: - trigger_element_focus($(e.target).prev()); - } - break; - } - }); - - // Click on item. - $popper.one("click", ".list-item", (event) => { - item_click_callback(event, instance); - }); - - // Set focus on search input when dropdown opens. - setTimeout(() => { - $(".dropdown-list-search-input").trigger("focus"); - }); - - on_show_callback(instance); - }, - onHidden(instance) { - if (focus_target_on_hidden) { - $(tippy_props.target).trigger("focus"); - } - on_hidden_callback(instance); - }, - ...tippy_props, - }); + const option = this.get_options().find((option) => option.unique_id === this.current_value); + if (option.is_setting_disabled) { + $(this.widget_value_selector).html(render_dropdown_disabled_state({name: option.name})); + } else if (option.stream) { + $(this.widget_value_selector).html( + render_inline_decorated_stream_name({ + stream: option.stream, + show_colored_icon: true, + }), + ); + } else { + $(this.widget_value_selector).text(option.name); + } + } } diff --git a/web/src/settings_bots.js b/web/src/settings_bots.js index 3769c74e2e..212115b1b2 100644 --- a/web/src/settings_bots.js +++ b/web/src/settings_bots.js @@ -26,6 +26,7 @@ import * as user_profile from "./user_profile"; const OUTGOING_WEBHOOK_BOT_TYPE = "3"; const EMBEDDED_BOT_TYPE = "4"; +export let bot_owner_dropdown_widget; const focus_tab = { active_bots_tab() { @@ -371,9 +372,7 @@ export function show_edit_bot_info_modal(user_id, from_user_info_popover) { formData.append("csrfmiddlewaretoken", csrf_token); formData.append("full_name", $full_name.val()); formData.append("role", JSON.stringify(role)); - const new_bot_owner_id = $("#bot_owner_dropdown_widget .bot_owner_name").attr( - "data-user-id", - ); + const new_bot_owner_id = bot_owner_dropdown_widget.value(); if (new_bot_owner_id) { formData.append("bot_owner_id", new_bot_owner_id); } @@ -424,29 +423,26 @@ export function show_edit_bot_info_modal(user_id, from_user_info_popover) { } function item_click_callback(event, dropdown) { - const $user = $(event.currentTarget); - const user_full_name = $user.attr("data-name"); - const new_bot_owner_id = Number.parseInt($user.attr("data-unique-id"), 10); - const $bot_owner = $("#bot_owner_dropdown_widget .bot_owner_name"); - $bot_owner.text(user_full_name); - $bot_owner.attr("data-user-id", new_bot_owner_id); - $("#edit_bot_modal .bot_owner_id").val(new_bot_owner_id).trigger("input"); + bot_owner_dropdown_widget.render(); + // Let dialog_wigdet know that there was a change in value. + $(bot_owner_dropdown_widget.widget_id).trigger("input"); dropdown.hide(); event.stopPropagation(); event.preventDefault(); } - dropdown_widget.setup( - { - target: "#bot_owner_dropdown_widget", - placement: "bottom-start", - }, + bot_owner_dropdown_widget = new dropdown_widget.DropdownWidget({ + widget_name: "edit_bot_owner", get_options, item_click_callback, - { - show_on_target_enter_keypress: true, + $events_container: $("#bot-edit-form"), + tippy_props: { + placement: "bottom-start", }, - ); + default_id: owner_id, + unique_id_type: dropdown_widget.DATA_TYPES.NUMBER, + }); + bot_owner_dropdown_widget.setup(); $("#bot-role-select").val(bot.role); if (!page_params.is_owner) { diff --git a/web/src/settings_streams.js b/web/src/settings_streams.js index 5ff78fbc9b..18e610265a 100644 --- a/web/src/settings_streams.js +++ b/web/src/settings_streams.js @@ -30,7 +30,7 @@ function add_choice_row($widget) { function get_chosen_default_streams() { // Return the set of stream id's of streams chosen in the default stream modal. return new Set( - $("#default-stream-choices .choice-row .stream_name") + $("#default-stream-choices .choice-row .dropdown_widget_value") .map((_i, elem) => $(elem).data("stream-id")?.toString()) .get(), ); @@ -39,7 +39,7 @@ function get_chosen_default_streams() { function create_choice_row() { const $container = $("#default-stream-choices"); const value = settings_profile_fields.get_value_for_new_option("#default-stream-choices"); - const stream_dropdown_widget_name = `select_default_stream_${value}_widget`; + const stream_dropdown_widget_name = `select_default_stream_${value}`; const row = render_default_stream_choice({value, stream_dropdown_widget_name}); $container.append(row); @@ -57,8 +57,8 @@ function create_choice_row() { const selected_stream_name = $selected_stream.attr("data-name"); const selected_stream_id = Number.parseInt($selected_stream.data("unique-id"), 10); - const $stream_dropdown_widget = $(`#${CSS.escape(stream_dropdown_widget_name)}`); - const $stream_name = $stream_dropdown_widget.find(".stream_name"); + const $stream_dropdown_widget = $(`#${CSS.escape(stream_dropdown_widget_name)}_widget`); + const $stream_name = $stream_dropdown_widget.find(".dropdown_widget_value"); $stream_name.text(selected_stream_name); $stream_name.data("stream-id", selected_stream_id); @@ -69,17 +69,15 @@ function create_choice_row() { event.preventDefault(); } - dropdown_widget.setup( - { - target: `#${stream_dropdown_widget_name}`, - placement: "bottom-start", - }, + new dropdown_widget.DropdownWidget({ + widget_name: stream_dropdown_widget_name, get_options, item_click_callback, - { - show_on_target_enter_keypress: true, + $events_container: $container, + tippy_props: { + placement: "bottom-start", }, - ); + }).setup(); } const meta = { diff --git a/web/src/stream_popover.js b/web/src/stream_popover.js index 1733d6c6b6..beb5f47089 100644 --- a/web/src/stream_popover.js +++ b/web/src/stream_popover.js @@ -340,9 +340,11 @@ export function build_move_topic_to_stream_popover( stream_bar.decorate(stream_name, $stream_header_colorblock); const stream = stream_data.get_sub_by_name(stream_name); if (stream === undefined) { - $("#move_topic_stream_name").text($t({defaultMessage: "Select a stream"})); + $("#move_topic_to_stream_widget .dropdown_widget_value").text( + $t({defaultMessage: "Select a stream"}), + ); } else { - $("#move_topic_stream_name").html( + $("#move_topic_to_stream_widget .dropdown_widget_value").html( render_inline_decorated_stream_name({stream, show_colored_icon: true}), ); } @@ -391,29 +393,24 @@ export function build_move_topic_to_stream_popover( } return stream_data.can_post_messages_in_stream(stream); }); - dropdown_widget.setup( - { - target: "#move_topic_stream_widget", + + new dropdown_widget.DropdownWidget({ + widget_name: "move_topic_to_stream", + get_options: streams_list_options, + item_click_callback: move_topic_on_update, + $events_container: $("#move_topic_modal"), + tippy_props: { // Overlap dropdown search input with stream selection button. placement: "bottom-start", offset: [0, -30], }, - streams_list_options, - move_topic_on_update, - ); + }).setup(); render_selected_stream(); $("#select_stream_widget .dropdown-toggle").prop("disabled", disable_stream_input); $("#move_topic_modal .move_messages_edit_topic").on("input", () => { update_submit_button_disabled_state(stream_widget_value); }); - $(".move-topic-dropdown").on("keydown", (e) => { - if (e.key === "Enter") { - $("#move_topic_stream_widget").trigger("click"); - e.stopPropagation(); - e.preventDefault(); - } - }); } function focus_on_move_modal_render() { diff --git a/web/styles/app_components.css b/web/styles/app_components.css index 4083ffaa2c..2f16027838 100644 --- a/web/styles/app_components.css +++ b/web/styles/app_components.css @@ -1023,6 +1023,7 @@ div.overlay { align-items: center; justify-content: space-between; outline: none; + color: var(--color-text-default); .fa-chevron-down { position: relative; @@ -1030,6 +1031,15 @@ div.overlay { } } +.setting-disabled-option { + color: hsl(38deg 46% 54%); + + & i { + /* Values set to match text alignment in stream dropdown. */ + padding: 0 5px 0 1px; + } +} + .filter_text_input { padding: 4px 6px; color: hsl(0deg 0% 33%); diff --git a/web/styles/compose.css b/web/styles/compose.css index 8b941436a9..69bfccc23c 100644 --- a/web/styles/compose.css +++ b/web/styles/compose.css @@ -805,7 +805,7 @@ input.recipient_box { margin: auto; } -#compose_recipient_selection_dropdown { +#compose_select_recipient_widget_wrapper { display: flex; justify-content: flex-start; height: var(--compose-recipient-box-min-height); @@ -814,26 +814,7 @@ input.recipient_box { margin: 0; } - .dropdown-toggle { - border-radius: 0 4px 4px 0 !important; - display: flex; - min-width: 0; - } - - .dropdown-menu { - /* The Bootstrap default of 160px wraps too early. - TODO: Replace this with a max-width and natural scaling? */ - width: 200px; - top: auto; - left: -10px; - } - - .dropup .dropdown-menu { - bottom: 100%; - margin-bottom: 17px; - } - - #compose_select_recipient_name { + .dropdown_widget_value { flex-grow: 1; overflow: hidden; text-overflow: ellipsis; @@ -849,10 +830,6 @@ input.recipient_box { } } - .dropdown-list-body .list_item a { - white-space: normal; - } - .fa-chevron-down { padding-left: 5px; color: hsl(0deg 0% 58%); diff --git a/web/styles/popovers.css b/web/styles/popovers.css index a3df5c7280..b0d2a0ee74 100644 --- a/web/styles/popovers.css +++ b/web/styles/popovers.css @@ -852,7 +852,7 @@ ul { margin: 0; } - .move-topic-dropdown { + #move_topic_to_stream_widget_wrapper { display: flex; margin-bottom: 10px; @@ -870,7 +870,7 @@ ul { margin: 0; } - #move_topic_stream_name { + .dropdown_widget_value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/web/styles/settings.css b/web/styles/settings.css index 2f4b034b7c..1515221e1b 100644 --- a/web/styles/settings.css +++ b/web/styles/settings.css @@ -648,7 +648,6 @@ input[type="checkbox"] { #add-default-stream-modal { .dropdown-widget-button { - width: 116px; display: inline-flex; } diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css index 1b9e19cbba..474bdd9d07 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -1032,12 +1032,12 @@ div.settings-radio-input-parent { margin-bottom: 10px; } - .dropdown-list-widget { - .dropdown-toggle { - margin-bottom: 10px; - min-width: 325px; - max-width: 100%; - } + .dropdown-widget-button { + /* Match the margin with other input groups around. */ + margin-bottom: 20px; + min-width: 325px; + max-width: 100%; + height: 30px; } } diff --git a/web/templates/compose.hbs b/web/templates/compose.hbs index 0374f94e45..b33ee7dc17 100644 --- a/web/templates/compose.hbs +++ b/web/templates/compose.hbs @@ -58,15 +58,8 @@
-
-
- -
+ {{> dropdown_widget_with_stream_colorblock + widget_name="compose_select_recipient"}}
diff --git a/web/templates/dropdown_disabled_state.hbs b/web/templates/dropdown_disabled_state.hbs new file mode 100644 index 0000000000..73d2c59f6e --- /dev/null +++ b/web/templates/dropdown_disabled_state.hbs @@ -0,0 +1 @@ +{{name}} diff --git a/web/templates/dropdown_list.hbs b/web/templates/dropdown_list.hbs index fa5d28ee0f..ab0215d7a9 100644 --- a/web/templates/dropdown_list.hbs +++ b/web/templates/dropdown_list.hbs @@ -5,6 +5,8 @@ {{> inline_decorated_stream_name stream=stream show_colored_icon=true}} {{else if is_direct_message}} {{name}} + {{else if is_setting_disabled}} + {{t "Disable" }} {{else}} {{name}} {{/if}} diff --git a/web/templates/dropdown_widget.hbs b/web/templates/dropdown_widget.hbs new file mode 100644 index 0000000000..8312c148c6 --- /dev/null +++ b/web/templates/dropdown_widget.hbs @@ -0,0 +1,4 @@ + diff --git a/web/templates/dropdown_widget_with_label.hbs b/web/templates/dropdown_widget_with_label.hbs new file mode 100644 index 0000000000..2bc8e62a2d --- /dev/null +++ b/web/templates/dropdown_widget_with_label.hbs @@ -0,0 +1,5 @@ +
+ + + {{> dropdown_widget}} +
diff --git a/web/templates/dropdown_widget_with_stream_colorblock.hbs b/web/templates/dropdown_widget_with_stream_colorblock.hbs new file mode 100644 index 0000000000..b041180063 --- /dev/null +++ b/web/templates/dropdown_widget_with_stream_colorblock.hbs @@ -0,0 +1,4 @@ +
+
+ {{> dropdown_widget disable_keyboard_focus="true"}} +
diff --git a/web/templates/move_topic_to_stream.hbs b/web/templates/move_topic_to_stream.hbs index 6917cad65e..69b44a73c5 100644 --- a/web/templates/move_topic_to_stream.hbs +++ b/web/templates/move_topic_to_stream.hbs @@ -9,15 +9,7 @@ {{/if}}
{{#unless only_topic_edit}} -
-
- -
+ {{> dropdown_widget_with_stream_colorblock widget_name="move_topic_to_stream"}} {{/unless}} diff --git a/web/templates/settings/default_stream_choice.hbs b/web/templates/settings/default_stream_choice.hbs index c07de98094..b046ceb5cb 100644 --- a/web/templates/settings/default_stream_choice.hbs +++ b/web/templates/settings/default_stream_choice.hbs @@ -1,10 +1,5 @@
- + {{> ../dropdown_widget widget_name=stream_dropdown_widget_name default_text=(t 'Select stream')}} diff --git a/web/templates/settings/edit_bot_form.hbs b/web/templates/settings/edit_bot_form.hbs index 965bf093c0..3cb55696a9 100644 --- a/web/templates/settings/edit_bot_form.hbs +++ b/web/templates/settings/edit_bot_form.hbs @@ -21,16 +21,10 @@ {{> dropdown_options_widget option_values=user_role_values}}
-
- - -
+ {{> ../dropdown_widget_with_label + widget_name="edit_bot_owner" + label=(t 'Owner')}} +
diff --git a/web/tests/compose_ui.test.js b/web/tests/compose_ui.test.js index bc23f21737..4a21d2da4b 100644 --- a/web/tests/compose_ui.test.js +++ b/web/tests/compose_ui.test.js @@ -747,7 +747,7 @@ run_test("get_focus_area", () => { }), "#compose-textarea", ); - assert.equal(get_focus_area("stream", {}), "#compose_select_recipient_widget"); + assert.equal(get_focus_area("stream", {}), "#compose_select_recipient_widget_wrapper"); assert.equal(get_focus_area("stream", {stream: "fun"}), "#stream_message_recipient_topic"); assert.equal(get_focus_area("stream", {stream: "fun", topic: "more"}), "#compose-textarea"); assert.equal( diff --git a/web/tests/lib/compose.js b/web/tests/lib/compose.js index b450286890..c98f551e8c 100644 --- a/web/tests/lib/compose.js +++ b/web/tests/lib/compose.js @@ -3,9 +3,9 @@ const $ = require("./zjquery"); exports.mock_stream_header_colorblock = () => { - const $stream_selection_dropdown = $("#compose_recipient_selection_dropdown"); + const $stream_selection_dropdown = $("#compose_select_recipient_widget_wrapper"); const $stream_header_colorblock = $(".stream_header_colorblock"); - $("#compose_recipient_selection_dropdown .stream_header_colorblock").css = () => {}; + $("#compose_select_recipient_widget_wrapper .stream_header_colorblock").css = () => {}; $stream_selection_dropdown.set_find_results( ".stream_header_colorblock", $stream_header_colorblock,