Files
zulip/static/js/dropdown_list_widget.js
aryanshridhar 0d20a20b07 dropdown_list_widget: Improve keyboard focus events.
Previously, our dropdown list widget was lacking several
keyUp/keyDown keyboard focus events due to which the user
couldn't shift the focus from the dropdown item to it's
search input.

This commit essentially binds several additional keyboard events
by defining a `dropdown_focus_events` function which helps
in enhancing our keyboard focus events.

Due to the above changes, it removes the custom event binded
to dropdown list widget because of which we fallback to our
deprecated keydown event handler functions present in /third/bootstrap.
2021-07-29 10:09:03 -07:00

241 lines
7.8 KiB
JavaScript

import $ from "jquery";
import render_dropdown_list from "../templates/settings/dropdown_list.hbs";
import * as blueslip from "./blueslip";
import * as ListWidget from "./list_widget";
export function DropdownListWidget({
widget_name,
data,
default_text,
render_text = (item_name) => item_name,
null_value = null,
include_current_item = true,
value,
on_update = () => {},
}) {
// Initializing values
this.widget_name = widget_name;
this.data = data;
this.default_text = default_text;
this.render_text = render_text;
this.null_value = null_value;
this.include_current_item = include_current_item;
this.initial_value = value;
this.on_update = on_update;
this.container_id = `${widget_name}_widget`;
this.value_id = `id_${widget_name}`;
if (value === undefined) {
this.initial_value = null_value;
blueslip.warn("dropdown-list-widget: Called without a default value; using null value");
}
// Setting up dropdown_list_widget
this.setup();
}
DropdownListWidget.prototype.render_default_text = function (elem) {
elem.text(this.default_text);
elem.addClass("text-warning");
elem.closest(".input-group").find(".dropdown_list_reset_button:enabled").hide();
};
DropdownListWidget.prototype.render = function (value) {
$(`#${CSS.escape(this.container_id)} #${CSS.escape(this.value_id)}`).data("value", value);
const elem = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.widget_name)}_name`);
if (!value || value === this.null_value) {
this.render_default_text(elem);
return;
}
// Happy path
const item = this.data.find((x) => x.value === value.toString());
if (item === undefined) {
this.render_default_text(elem);
return;
}
const text = this.render_text(item.name);
elem.text(text);
elem.removeClass("text-warning");
elem.closest(".input-group").find(".dropdown_list_reset_button:enabled").show();
};
DropdownListWidget.prototype.update = function (value) {
this.render(value);
this.on_update(value);
};
DropdownListWidget.prototype.register_event_handlers = function () {
$(`#${CSS.escape(this.container_id)} .dropdown-list-body`).on(
"click keypress",
".list_item",
(e) => {
const setting_elem = $(e.currentTarget).closest(
`.${CSS.escape(this.widget_name)}_setting`,
);
if (e.type === "keypress") {
if (e.key === "Enter") {
setting_elem.find(".dropdown-menu").dropdown("toggle");
} else {
return;
}
}
const value = $(e.currentTarget).attr("data-value");
this.update(value);
},
);
$(`#${CSS.escape(this.container_id)} .dropdown_list_reset_button`).on("click", (e) => {
this.update(this.null_value);
e.preventDefault();
});
};
DropdownListWidget.prototype.setup_dropdown_widget = function (data) {
const dropdown_list_body = $(
`#${CSS.escape(this.container_id)} .dropdown-list-body`,
).expectOne();
const search_input = $(`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`);
const get_data = () => {
if (this.include_current_item) {
return data;
}
return data.filter((x) => x.value !== this.value.toString());
};
ListWidget.create(dropdown_list_body, get_data(data), {
name: `${CSS.escape(this.widget_name)}_list`,
modifier(item) {
return render_dropdown_list({item});
},
filter: {
element: search_input,
predicate(item, value) {
return item.name.toLowerCase().includes(value);
},
},
simplebar_container: $(`#${CSS.escape(this.container_id)} .dropdown-list-wrapper`),
});
};
// Sets the focus to the ListWidget input once the dropdown button is clicked.
DropdownListWidget.prototype.dropdown_toggle_click_handler = function () {
const dropdown_toggle = $(`#${CSS.escape(this.container_id)} .dropdown-toggle`);
const search_input = $(`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`);
dropdown_toggle.on("click", () => {
search_input.val("").trigger("input");
});
};
DropdownListWidget.prototype.dropdown_focus_events = function () {
const search_input = $(`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`);
const dropdown_menu = $(`.${CSS.escape(this.widget_name)}_setting .dropdown-menu`);
const dropdown_elements = () => {
const dropdown_list_body = $(
`#${CSS.escape(this.container_id)} .dropdown-list-body`,
).expectOne();
return dropdown_list_body.children().find("a");
};
// Rest of the key handlers are supported by our
// bootstrap library.
dropdown_menu.on("keydown", (e) => {
function trigger_element_focus(element) {
e.preventDefault();
e.stopPropagation();
element.trigger("focus");
}
switch (e.key) {
case "ArrowDown": {
switch (e.target) {
case dropdown_elements().last()[0]:
trigger_element_focus(search_input);
break;
case search_input[0]:
trigger_element_focus(dropdown_elements().first());
break;
}
break;
}
case "ArrowUp": {
switch (e.target) {
case dropdown_elements().first()[0]:
trigger_element_focus(search_input);
break;
case search_input[0]:
trigger_element_focus(dropdown_elements().last());
}
break;
}
case "Tab": {
switch (e.target) {
case search_input[0]:
trigger_element_focus(dropdown_elements().first());
break;
case dropdown_elements().last()[0]:
trigger_element_focus(search_input);
break;
}
break;
}
}
});
};
DropdownListWidget.prototype.setup = function () {
// populate the dropdown
const dropdown_list_body = $(
`#${CSS.escape(this.container_id)} .dropdown-list-body`,
).expectOne();
const search_input = $(`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`);
const dropdown_toggle = $(`#${CSS.escape(this.container_id)} .dropdown-toggle`);
this.setup_dropdown_widget(this.data);
$(`#${CSS.escape(this.container_id)} .dropdown-search`).on("click", (e) => {
e.stopPropagation();
});
this.dropdown_toggle_click_handler();
dropdown_toggle.on("focus", (e) => {
// On opening a Bootstrap Dropdown, the parent element receives focus.
// Here, we want our search input to have focus instead.
e.preventDefault();
// This function gets called twice when focusing the
// dropdown, and only in the second call is the input
// field visible in the DOM; so the following visibility
// check ensures we wait for the second call to focus.
if (dropdown_list_body.is(":visible")) {
search_input.trigger("focus");
}
});
this.dropdown_focus_events();
this.render(this.initial_value);
this.register_event_handlers();
};
// Returns the updated value
DropdownListWidget.prototype.value = function () {
let val = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.value_id)}`).data("value");
if (val === null) {
val = "";
}
return val;
};