From f2e627ba51d5daef518cf81c31e7d8bde6b7c340 Mon Sep 17 00:00:00 2001 From: Daniil Fadeev Date: Mon, 15 May 2023 09:25:44 +0400 Subject: [PATCH] drafts: Move code handling keyboard navigation to a separate module. The keyboard navigation that used to only work in drafts can now be reused. This commit has moved the related functions to a separate module. --- tools/test-js-with-node | 1 + web/src/drafts.js | 195 ++-------------------------- web/src/messages_overlay_ui.js | 181 ++++++++++++++++++++++++++ web/templates/tooltip_templates.hbs | 2 + web/tests/drafts.test.js | 5 +- 5 files changed, 195 insertions(+), 189 deletions(-) create mode 100644 web/src/messages_overlay_ui.js diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 03028a5c26..2267bc8bc1 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -120,6 +120,7 @@ EXEMPT_FILES = make_set( "web/src/message_util.js", "web/src/message_view_header.js", "web/src/message_viewport.js", + "web/src/messages_overlay_ui.js", "web/src/muted_topics_ui.js", "web/src/muted_users_ui.js", "web/src/narrow.js", diff --git a/web/src/drafts.js b/web/src/drafts.js index db770b4e67..6cb412e80b 100644 --- a/web/src/drafts.js +++ b/web/src/drafts.js @@ -19,6 +19,7 @@ import * as confirm_dialog from "./confirm_dialog"; import {$t, $t_html} from "./i18n"; import {localstorage} from "./localstorage"; import * as markdown from "./markdown"; +import * as messages_overlay_ui from "./messages_overlay_ui"; import * as narrow from "./narrow"; import * as narrow_state from "./narrow_state"; import * as overlays from "./overlays"; @@ -415,44 +416,6 @@ export function format_draft(draft) { return formatted; } -function row_with_focus(context) { - const focused_item = $(`.${CSS.escape(context.box_item_selector)}:focus`)[0]; - return $(focused_item).parent(`.${CSS.escape(context.row_item_selector)}`); -} - -function row_before_focus(context) { - const $focused_row = row_with_focus(context); - const $prev_row = $focused_row.prev(`.${CSS.escape(context.row_item_selector)}:visible`); - // The draft modal can have two sub-sections. This handles the edge case - // when the user moves from the second "Other drafts" section to the first - // section which contains drafts from a particular narrow. - if ( - $prev_row.length === 0 && - $focused_row.parent().attr("id") === "other-drafts" && - $("#drafts-from-conversation").is(":visible") - ) { - return $($("#drafts-from-conversation").children(".draft-row:visible").last()); - } - - return $prev_row; -} - -function row_after_focus(context) { - const $focused_row = row_with_focus(context); - const $next_row = $focused_row.next(`.${CSS.escape(context.row_item_selector)}:visible`); - // The draft modal can have two sub-sections. This handles the edge case - // when the user moves from the first section (drafts from a particular - // narrow) to the second section which contains the rest of the drafts. - if ( - $next_row.length === 0 && - $focused_row.parent().attr("id") === "drafts-from-conversation" && - $("#other-drafts").is(":visible") - ) { - return $("#other-drafts").children(".draft-row:visible").first(); - } - return $next_row; -} - function remove_draft($draft_row) { // Deletes the draft and removes it from the list const draft_id = $draft_row.data("draft-id"); @@ -565,7 +528,7 @@ const keyboard_handling_context = { // This handles when pressing Enter while looking at drafts. // It restores draft that is focused. const draft_id_arrow = this.get_items_ids(); - const focused_draft_id = get_focused_element_id(this); + const focused_draft_id = messages_overlay_ui.get_focused_element_id(this); if (Object.hasOwn(document.activeElement.parentElement.dataset, "draftId")) { restore_draft(focused_draft_id); } else { @@ -575,12 +538,12 @@ const keyboard_handling_context = { }, on_delete() { // Allows user to delete drafts with Backspace - const focused_element_id = get_focused_element_id(this); + const focused_element_id = messages_overlay_ui.get_focused_element_id(this); if (focused_element_id === undefined) { return; } - const $focused_row = row_with_focus(this); - focus_on_sibling_element(this); + const $focused_row = messages_overlay_ui.row_with_focus(this); + messages_overlay_ui.focus_on_sibling_element(this); remove_draft($focused_row); }, items_container_selector: "drafts-container", @@ -591,7 +554,7 @@ const keyboard_handling_context = { }; export function handle_keyboard_events(e, event_key) { - modals_handle_events(e, event_key, keyboard_handling_context); + messages_overlay_ui.modals_handle_events(e, event_key, keyboard_handling_context); } export function launch() { @@ -690,143 +653,10 @@ export function launch() { open_overlay(); const first_element_id = [...formatted_narrow_drafts, ...formatted_other_drafts][0]?.draft_id; - set_initial_element(first_element_id, keyboard_handling_context); + messages_overlay_ui.set_initial_element(first_element_id, keyboard_handling_context); setup_event_handlers(); } -function activate_element(elem, context) { - $(`.${CSS.escape(context.box_item_selector)}`).removeClass("active"); - $(elem).expectOne().addClass("active"); - elem.focus(); -} - -function initialize_focus(event_name, context) { - // If an item is not focused in modal, then focus the last item - // if up_arrow is clicked or the first item if down_arrow is clicked. - if ( - (event_name !== "up_arrow" && event_name !== "down_arrow") || - $(`.${CSS.escape(context.box_item_selector)}:focus`)[0] - ) { - return; - } - - const modal_items_ids = context.get_items_ids(); - if (modal_items_ids.length === 0) { - // modal is empty - return; - } - - let element; - - function get_last_element() { - const last_id = modal_items_ids.at(-1); - return get_element_by_id(last_id, context); - } - - function get_first_element() { - const first_id = modal_items_ids[0]; - return get_element_by_id(first_id, context); - } - - if (event_name === "up_arrow") { - element = get_last_element(); - } else if (event_name === "down_arrow") { - element = get_first_element(); - } - const focus_element = element[0].children[0]; - activate_element(focus_element, context); -} - -function scroll_to_element($element, context) { - if ($element[0] === undefined) { - return; - } - if ($element[0].children[0] === undefined) { - return; - } - activate_element($element[0].children[0], context); - - const $items_list = $(`.${CSS.escape(context.items_list_selector)}`); - const $items_container = $(`.${CSS.escape(context.items_container_selector)}`); - const $box_item = $(`.${CSS.escape(context.box_item_selector)}`); - - // If focused element is first, scroll to the top. - if ($box_item.first()[0].parentElement === $element[0]) { - $items_list[0].scrollTop = 0; - } - - // If focused element is last, scroll to the bottom. - if ($box_item.last()[0].parentElement === $element[0]) { - $items_list[0].scrollTop = $items_list[0].scrollHeight - $items_list.height(); - } - - // If focused element is cut off from the top, scroll up halfway in modal. - if ($element.position().top < 55) { - // 55 is the minimum distance from the top that will require extra scrolling. - $items_list[0].scrollTop -= $items_list[0].clientHeight / 2; - } - - // If focused element is cut off from the bottom, scroll down halfway in modal. - const dist_from_top = $element.position().top; - const total_dist = dist_from_top + $element[0].clientHeight; - const dist_from_bottom = $items_container[0].clientHeight - total_dist; - if (dist_from_bottom < -4) { - // -4 is the min dist from the bottom that will require extra scrolling. - $items_list[0].scrollTop += $items_list[0].clientHeight / 2; - } -} - -function get_element_by_id(id, context) { - return $(`[${context.id_attribute_name}='${id}']`); -} - -function get_focused_element_id(context) { - return row_with_focus(context).attr(context.id_attribute_name); -} - -function focus_on_sibling_element(context) { - const $next_row = row_after_focus(context); - const $prev_row = row_before_focus(context); - let elem_to_be_focused_id; - - // Try to get the next item in the list and 'focus' on it. - // Use previous item as a fallback. - if ($next_row[0] !== undefined) { - elem_to_be_focused_id = $next_row.attr(context.id_attribute_name); - } else if ($prev_row[0] !== undefined) { - elem_to_be_focused_id = $prev_row.attr(context.id_attribute_name); - } - - const new_focus_element = get_element_by_id(elem_to_be_focused_id, context); - if (new_focus_element[0] !== undefined) { - activate_element(new_focus_element[0].children[0], context); - } -} - -function modals_handle_events(e, event_key, context) { - initialize_focus(event_key, context); - - // This detects up arrow key presses when the overlay - // is open and scrolls through. - if (event_key === "up_arrow" || event_key === "vim_up") { - scroll_to_element(row_before_focus(context), context); - } - - // This detects down arrow key presses when the overlay - // is open and scrolls through. - if (event_key === "down_arrow" || event_key === "vim_down") { - scroll_to_element(row_after_focus(context), context); - } - - if (event_key === "backspace" || event_key === "delete") { - context.on_delete(); - } - - if (event_key === "enter") { - context.on_enter(); - } -} - function open_overlay() { overlays.open_overlay({ name: "drafts", @@ -837,15 +667,6 @@ function open_overlay() { }); } -function set_initial_element(element_id, context) { - if (element_id) { - const current_element = get_element_by_id(element_id, context); - const focus_element = current_element[0].children[0]; - activate_element(focus_element, keyboard_handling_context); - $(`.${CSS.escape(context.items_list_selector)}`)[0].scrollTop = 0; - } -} - export function initialize() { remove_old_drafts(); @@ -860,6 +681,6 @@ export function initialize() { set_count(Object.keys(draft_model.get()).length); $("body").on("focus", ".draft-info-box", (e) => { - activate_element(e.target, keyboard_handling_context); + messages_overlay_ui.activate_element(e.target, keyboard_handling_context); }); } diff --git a/web/src/messages_overlay_ui.js b/web/src/messages_overlay_ui.js new file mode 100644 index 0000000000..147584120e --- /dev/null +++ b/web/src/messages_overlay_ui.js @@ -0,0 +1,181 @@ +import $ from "jquery"; + +export function row_with_focus(context) { + const focused_item = $(`.${CSS.escape(context.box_item_selector)}:focus`)[0]; + return $(focused_item).parent(`.${CSS.escape(context.row_item_selector)}`); +} + +export function activate_element(elem, context) { + $(`.${CSS.escape(context.box_item_selector)}`).removeClass("active"); + $(elem).expectOne().addClass("active"); + elem.focus(); +} + +export function get_focused_element_id(context) { + return row_with_focus(context).attr(context.id_attribute_name); +} + +export function focus_on_sibling_element(context) { + const $next_row = row_after_focus(context); + const $prev_row = row_before_focus(context); + let elem_to_be_focused_id; + + // Try to get the next item in the list and 'focus' on it. + // Use previous item as a fallback. + if ($next_row[0] !== undefined) { + elem_to_be_focused_id = $next_row.attr(context.id_attribute_name); + } else if ($prev_row[0] !== undefined) { + elem_to_be_focused_id = $prev_row.attr(context.id_attribute_name); + } + + const new_focus_element = get_element_by_id(elem_to_be_focused_id, context); + if (new_focus_element[0] !== undefined) { + activate_element(new_focus_element[0].children[0], context); + } +} + +export function modals_handle_events(e, event_key, context) { + initialize_focus(event_key, context); + + // This detects up arrow key presses when the overlay + // is open and scrolls through. + if (event_key === "up_arrow" || event_key === "vim_up") { + scroll_to_element(row_before_focus(context), context); + } + + // This detects down arrow key presses when the overlay + // is open and scrolls through. + if (event_key === "down_arrow" || event_key === "vim_down") { + scroll_to_element(row_after_focus(context), context); + } + + if (event_key === "backspace" || event_key === "delete") { + context.on_delete(); + } + + if (event_key === "enter") { + context.on_enter(); + } +} + +export function set_initial_element(element_id, context) { + if (element_id) { + const current_element = get_element_by_id(element_id, context); + const focus_element = current_element[0].children[0]; + activate_element(focus_element, context); + $(`.${CSS.escape(context.items_list_selector)}`)[0].scrollTop = 0; + } +} + +function row_before_focus(context) { + const $focused_row = row_with_focus(context); + const $prev_row = $focused_row.prev(`.${CSS.escape(context.row_item_selector)}:visible`); + // The draft modal can have two sub-sections. This handles the edge case + // when the user moves from the second "Other drafts" section to the first + // section which contains drafts from a particular narrow. + if ( + $prev_row.length === 0 && + $focused_row.parent().attr("id") === "other-drafts" && + $("#drafts-from-conversation").is(":visible") + ) { + return $($("#drafts-from-conversation").children(".draft-row:visible").last()); + } + + return $prev_row; +} + +function row_after_focus(context) { + const $focused_row = row_with_focus(context); + const $next_row = $focused_row.next(`.${CSS.escape(context.row_item_selector)}:visible`); + // The draft modal can have two sub-sections. This handles the edge case + // when the user moves from the first section (drafts from a particular + // narrow) to the second section which contains the rest of the drafts. + if ( + $next_row.length === 0 && + $focused_row.parent().attr("id") === "drafts-from-conversation" && + $("#other-drafts").is(":visible") + ) { + return $("#other-drafts").children(".draft-row:visible").first(); + } + return $next_row; +} + +function initialize_focus(event_name, context) { + // If an item is not focused in modal, then focus the last item + // if up_arrow is clicked or the first item if down_arrow is clicked. + if ( + (event_name !== "up_arrow" && event_name !== "down_arrow") || + $(`.${CSS.escape(context.box_item_selector)}:focus`)[0] + ) { + return; + } + + const modal_items_ids = context.get_items_ids(); + if (modal_items_ids.length === 0) { + // modal is empty + return; + } + + let element; + + function get_last_element() { + const last_id = modal_items_ids.at(-1); + return get_element_by_id(last_id, context); + } + + function get_first_element() { + const first_id = modal_items_ids[0]; + return get_element_by_id(first_id, context); + } + + if (event_name === "up_arrow") { + element = get_last_element(); + } else if (event_name === "down_arrow") { + element = get_first_element(); + } + const focus_element = element[0].children[0]; + activate_element(focus_element, context); +} + +function scroll_to_element($element, context) { + if ($element[0] === undefined) { + return; + } + if ($element[0].children[0] === undefined) { + return; + } + activate_element($element[0].children[0], context); + + const $items_list = $(`.${CSS.escape(context.items_list_selector)}`); + const $items_container = $(`.${CSS.escape(context.items_container_selector)}`); + const $box_item = $(`.${CSS.escape(context.box_item_selector)}`); + + // If focused element is first, scroll to the top. + if ($box_item.first()[0].parentElement === $element[0]) { + $items_list[0].scrollTop = 0; + } + + // If focused element is last, scroll to the bottom. + if ($box_item.last()[0].parentElement === $element[0]) { + $items_list[0].scrollTop = $items_list[0].scrollHeight - $items_list.height(); + } + + // If focused element is cut off from the top, scroll up halfway in modal. + if ($element.position().top < 55) { + // 55 is the minimum distance from the top that will require extra scrolling. + $items_list[0].scrollTop -= $items_list[0].clientHeight / 2; + } + + // If focused element is cut off from the bottom, scroll down halfway in modal. + const dist_from_top = $element.position().top; + const total_dist = dist_from_top + $element[0].clientHeight; + const dist_from_bottom = $items_container[0].clientHeight - total_dist; + if (dist_from_bottom < -4) { + // -4 is the min dist from the bottom that will require extra scrolling. + $items_list[0].scrollTop += $items_list[0].clientHeight / 2; + } +} + +function get_element_by_id(id, context) { + return $(`[${context.id_attribute_name}='${id}']`); +} diff --git a/web/templates/tooltip_templates.hbs b/web/templates/tooltip_templates.hbs index 6537619b25..c7bdfad96a 100644 --- a/web/templates/tooltip_templates.hbs +++ b/web/templates/tooltip_templates.hbs @@ -160,9 +160,11 @@