mirror of https://github.com/zulip/zulip.git
recent_topics: Support arrow key navigation.
Add arrow key navigation support for recent topics. Simple jquery is used to allow navigation for filter buttons, a grid system is used for navigation inside table.
This commit is contained in:
parent
05f7cb7750
commit
a052d24231
|
@ -318,20 +318,12 @@ run_test("test_recent_topics_launch", () => {
|
|||
return '<recent_topics table stub>';
|
||||
});
|
||||
|
||||
$("#recent_topics_search").selected = false;
|
||||
$("#recent_topics_search").select = () => {
|
||||
$("#recent_topics_search").selected = true;
|
||||
};
|
||||
|
||||
const rt = zrequire('recent_topics');
|
||||
rt.process_messages(messages);
|
||||
|
||||
assert.equal($("#recent_topics_search").selected, false);
|
||||
|
||||
rt.launch();
|
||||
|
||||
// Test if search text is selected
|
||||
assert.equal($("#recent_topics_search").selected, true);
|
||||
overlays.close_callback();
|
||||
|
||||
// incorrect topic_key
|
||||
|
|
|
@ -422,6 +422,19 @@ exports.process_shift_tab_key = function () {
|
|||
exports.process_hotkey = function (e, hotkey) {
|
||||
const event_name = hotkey.name;
|
||||
|
||||
// This block needs to be before the `tab` handler.
|
||||
switch (event_name) {
|
||||
case 'up_arrow':
|
||||
case 'down_arrow':
|
||||
case 'left_arrow':
|
||||
case 'right_arrow':
|
||||
case 'tab':
|
||||
case 'shift_tab':
|
||||
if (overlays.recent_topics_open()) {
|
||||
return recent_topics.change_focused_element(e, event_name);
|
||||
}
|
||||
}
|
||||
|
||||
// We handle the most complex keys in their own functions.
|
||||
switch (event_name) {
|
||||
case 'escape':
|
||||
|
|
|
@ -8,6 +8,78 @@ let topics_widget;
|
|||
const MAX_AVATAR = 4;
|
||||
let filters = new Set();
|
||||
|
||||
// Use this to set the focused element.
|
||||
//
|
||||
// We set it's value to `table` in case the
|
||||
// focus in one of the table rows, since the
|
||||
// table rows are constantly updated and tracking
|
||||
// the selected element in them would be tedious via
|
||||
// jquery.
|
||||
//
|
||||
// So, we use table as a grid system and
|
||||
// track the coordinates of the focus element via
|
||||
// `row_focus` and `col_focus`.
|
||||
let current_focus_elem;
|
||||
let row_focus = 0;
|
||||
// Start focus on the topic column, so Down+Enter works to visit a topic.
|
||||
let col_focus = 1;
|
||||
|
||||
// The number of selectable actions in a recent_topics. Used to
|
||||
// implement wraparound of elements with the right/left keys. Must be
|
||||
// increased when we add new actions, or rethought if we add optional
|
||||
// actions that only appear in some rows.
|
||||
const MAX_SELECTABLE_COLS = 4;
|
||||
|
||||
function set_default_focus() {
|
||||
// If at any point we are confused about the currently
|
||||
// focused element, we switch focus to search.
|
||||
current_focus_elem = $("#recent_topics_search");
|
||||
current_focus_elem.focus();
|
||||
}
|
||||
|
||||
function set_table_focus(row, col) {
|
||||
const topic_rows = $("#recent_topics_table table tbody tr");
|
||||
if (topic_rows.length === 0 || row < 0 || row >= topic_rows.length) {
|
||||
row_focus = 0;
|
||||
// return focus back to filters if we cannot focus on the table.
|
||||
set_default_focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
// The first two "columns" are the stream/topic links.
|
||||
if (col === 0 || col === 1) {
|
||||
topic_rows.eq(row).children().eq(col).find('a').focus();
|
||||
} else {
|
||||
topic_rows.eq(row).children().eq(2).children().eq(col - 2).focus();
|
||||
}
|
||||
current_focus_elem = 'table';
|
||||
return true;
|
||||
}
|
||||
|
||||
function revive_current_focus() {
|
||||
// After re-render, the current_focus_elem is no longer linked
|
||||
// to the focused element, this function attempts to revive the
|
||||
// link and focus to the element prior to the rerender.
|
||||
if (!current_focus_elem) {
|
||||
set_default_focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current_focus_elem === 'table') {
|
||||
set_table_focus(row_focus, col_focus);
|
||||
return true;
|
||||
}
|
||||
|
||||
const filter_button = current_focus_elem.data('filter');
|
||||
if (!filter_button) {
|
||||
set_default_focus();
|
||||
} else {
|
||||
current_focus_elem = $("#recent_topics_filter_buttons").find("[data-filter='" + filter_button + "']");
|
||||
current_focus_elem.focus();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function get_topic_key(stream_id, topic) {
|
||||
return stream_id + ":" + topic.toLowerCase();
|
||||
}
|
||||
|
@ -208,6 +280,7 @@ exports.inplace_rerender = function (topic_key) {
|
|||
} else {
|
||||
topic_row.show();
|
||||
}
|
||||
revive_current_focus();
|
||||
return true;
|
||||
};
|
||||
|
||||
|
@ -276,9 +349,9 @@ exports.update_filters_view = function () {
|
|||
show_selected_filters();
|
||||
|
||||
topics_widget.hard_redraw();
|
||||
revive_current_focus();
|
||||
};
|
||||
|
||||
|
||||
function stream_sort(a, b) {
|
||||
const a_stream = message_store.get(a.last_msg_id).stream;
|
||||
const b_stream = message_store.get(b.last_msg_id).stream;
|
||||
|
@ -341,6 +414,7 @@ exports.complete_rerender = function () {
|
|||
},
|
||||
html_selector: get_topic_row,
|
||||
});
|
||||
revive_current_focus();
|
||||
};
|
||||
|
||||
exports.launch = function () {
|
||||
|
@ -352,7 +426,109 @@ exports.launch = function () {
|
|||
},
|
||||
});
|
||||
exports.complete_rerender();
|
||||
$("#recent_topics_search").select();
|
||||
};
|
||||
|
||||
exports.change_focused_element = function (e, input_key) {
|
||||
// Called from hotkeys.js; like all logic in that module,
|
||||
// returning true will cause the caller to do
|
||||
// preventDefault/stopPropagation; false will let the browser
|
||||
// handle the key.
|
||||
if (input_key === 'tab') {
|
||||
input_key = 'right_arrow';
|
||||
} else if (input_key === 'shift_tab') {
|
||||
input_key = 'left_arrow';
|
||||
}
|
||||
|
||||
const $elem = $(e.target);
|
||||
|
||||
if ($("#recent_topics_table").find(':focus').length === 0) {
|
||||
// This is a failsafe to return focus back to recent topics overlay,
|
||||
// in case it loses focus due to some unknown reason.
|
||||
set_default_focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.target.id === 'recent_topics_search') {
|
||||
// Since the search box a text area, we want the browser to handle
|
||||
// Left/Right and selection within the widget; but if the user
|
||||
// arrows off the edges, we should move focus to the adjacent widgets..
|
||||
const textInput = $("#recent_topics_search").get(0);
|
||||
const start = textInput.selectionStart;
|
||||
const end = textInput.selectionEnd;
|
||||
const text_length = textInput.value.length;
|
||||
let is_selected = false;
|
||||
if (end - start > 0) {
|
||||
is_selected = true;
|
||||
}
|
||||
|
||||
switch (input_key) {
|
||||
case 'left_arrow':
|
||||
if (start !== 0 || is_selected) {
|
||||
return false;
|
||||
}
|
||||
current_focus_elem = $elem.prev().children().last();
|
||||
break;
|
||||
case 'right_arrow':
|
||||
if (end !== text_length || is_selected) {
|
||||
return false;
|
||||
}
|
||||
current_focus_elem = $elem.prev().children().first();
|
||||
break;
|
||||
case 'down_arrow':
|
||||
set_table_focus(row_focus, col_focus);
|
||||
return true;
|
||||
}
|
||||
} else if ($elem.hasClass('btn-recent-filters')) {
|
||||
switch (input_key) {
|
||||
case 'left_arrow':
|
||||
if ($elem.parent().children().first()[0] === $elem[0]) {
|
||||
current_focus_elem = $("#recent_topics_search");
|
||||
} else {
|
||||
current_focus_elem = $elem.prev();
|
||||
}
|
||||
break;
|
||||
case 'right_arrow':
|
||||
if ($elem.parent().children().last()[0] === $elem[0]) {
|
||||
current_focus_elem = $("#recent_topics_search");
|
||||
} else {
|
||||
current_focus_elem = $elem.next();
|
||||
}
|
||||
break;
|
||||
case 'down_arrow':
|
||||
set_table_focus(row_focus, col_focus);
|
||||
return true;
|
||||
}
|
||||
} else if (current_focus_elem === 'table') {
|
||||
// For arrowing around the table of topics, we implement left/right
|
||||
// wraparound. Going off the top or the bottom takes one
|
||||
// to the navigation at the top (see set_table_focus).
|
||||
switch (input_key) {
|
||||
case 'left_arrow':
|
||||
col_focus -= 1;
|
||||
if (col_focus < 0) {
|
||||
col_focus = MAX_SELECTABLE_COLS - 1;
|
||||
}
|
||||
break;
|
||||
case 'right_arrow':
|
||||
col_focus += 1;
|
||||
if (col_focus >= MAX_SELECTABLE_COLS) {
|
||||
col_focus = 0;
|
||||
}
|
||||
break;
|
||||
case 'down_arrow':
|
||||
row_focus += 1;
|
||||
break;
|
||||
case 'up_arrow':
|
||||
row_focus -= 1;
|
||||
}
|
||||
set_table_focus(row_focus, col_focus);
|
||||
return true;
|
||||
}
|
||||
if (current_focus_elem) {
|
||||
current_focus_elem.focus();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
window.recent_topics = exports;
|
||||
|
|
|
@ -97,6 +97,7 @@ EXEMPT_FILES = {
|
|||
'static/js/ready.js',
|
||||
'static/js/realm_icon.js',
|
||||
'static/js/realm_logo.js',
|
||||
'static/js/recent_topics.js',
|
||||
'static/js/reload.js',
|
||||
'static/js/reload_state.js',
|
||||
'static/js/reminder.js',
|
||||
|
|
Loading…
Reference in New Issue