Files
zulip/static/js/settings_list_widget.js
Rohitt Vashishtha 9d1f9e8a75 settings: Improve user interaction in DropdownListWidget.
This is a finicky change; we need to adapt around bootstrap internals
to first steal focus from the list, and then if the user uses arrow
keys, send that key to the list letting bootstrap focus on the list
elements.

The reverse: stealing abck focus to the input from the list, is not
possible without changing third/bootstrap.js because it's concept of
currently selected item depends on the item being focused. We retain
the pre-commit behavior for this, where the user can SHIFT+TAB to get
back to the input and type.

Ideally, a user will now interact with this widget like this:

1. Click the button to open the widget. The input is in focus.
2. Type a query to filter the results.
3. Seemlessly start using arrow keys to select an option.
4. Press "enter" to select the option.
2020-04-22 17:57:16 -07:00

121 lines
4.0 KiB
JavaScript

const DropdownListWidget = function (opts) {
opts = Object.assign({
null_value: null,
render_text: (item_name) => item_name,
}, opts);
opts.container_id = `${opts.setting_name}_widget`;
opts.value_id = `id_${opts.setting_name}`;
const render_dropdown_list = require("../templates/settings/dropdown_list.hbs");
const render = (value) => {
$(`#${opts.container_id} #${opts.value_id}`).data("value", value);
const elem = $(`#${opts.container_id} #${opts.setting_name}_name`);
if (!value || value === opts.null_value) {
elem.text(opts.default_text);
elem.addClass("text-warning");
elem.closest('.input-group').find('.dropdown_list_reset_button').hide();
return;
}
// Happy path
const item = opts.data.find(x => x.value === value.toString());
const text = opts.render_text(item.name);
elem.text(text);
elem.removeClass('text-warning');
elem.closest('.input-group').find('.dropdown_list_reset_button').show();
};
const update = (value) => {
render(value);
settings_org.save_discard_widget_status_handler($(`#org-${opts.subsection}`));
};
const register_event_handlers = () => {
$(`#${opts.container_id} .dropdown-list-body`).on("click keypress", ".list_item", function (e) {
const setting_elem = $(this).closest(`.${opts.setting_name}_setting`);
if (e.type === "keypress") {
if (e.which === 13) {
setting_elem.find(".dropdown-menu").dropdown("toggle");
} else {
return;
}
}
const value = $(this).attr('data-value');
update(value);
});
$(`#${opts.container_id} .dropdown_list_reset_button`).click(function () {
update(opts.null_value);
});
};
const setup = () => {
// populate the dropdown
const dropdown_list_body = $(`#${opts.container_id} .dropdown-list-body`).expectOne();
const search_input = $(`#${opts.container_id} .dropdown-search > input[type=text]`);
const dropdown_toggle = $(`#${opts.container_id} .dropdown-toggle`);
list_render.create(dropdown_list_body, opts.data, {
name: `${opts.setting_name}_list`,
modifier: function (item) {
return render_dropdown_list({ item: item });
},
filter: {
element: search_input,
predicate: function (item, value) {
return item.name.toLowerCase().includes(value);
},
},
});
$(`#${opts.container_id} .dropdown-search`).click(function (e) {
e.stopPropagation();
});
dropdown_toggle.click(function () {
search_input.val("").trigger("input");
});
dropdown_toggle.focus(function (e) {
// On opening a Bootstrap Dropdown, the parent element recieves focus.
// Here, we want our search input to have focus instead.
e.preventDefault();
search_input.focus();
});
search_input.keydown(function (e) {
if (!/(38|40|27)/.test(e.keyCode)) {
return;
}
e.preventDefault();
const custom_event = jQuery.Event("keydown.dropdown.data-api", {
keyCode: e.keyCode,
which: e.keyCode,
});
dropdown_toggle.trigger(custom_event);
});
render(page_params[opts.setting_name]);
register_event_handlers();
};
const value = () => {
let val = $(`#${opts.container_id} #${opts.value_id}`).data('value');
if (val === null) {
val = '';
}
return val;
};
// Run setup() automatically on initialization.
setup();
return {
render,
value,
};
};
window.settings_list_widget = DropdownListWidget;