mirror of
https://github.com/zulip/zulip.git
synced 2025-11-18 12:54:58 +00:00
inbox: Simplify keyboard navigation.
This makes use of `data-col-index` attribute on the elements to know which column to focus based on user input.
This commit is contained in:
@@ -195,15 +195,14 @@ let filters_dropdown_widget;
|
|||||||
let channel_view_topic_widget: InboxTopicListWidget | undefined;
|
let channel_view_topic_widget: InboxTopicListWidget | undefined;
|
||||||
|
|
||||||
const COLUMNS = {
|
const COLUMNS = {
|
||||||
COLLAPSE_BUTTON: 0,
|
FULL_ROW: 0,
|
||||||
RECIPIENT: 1,
|
UNREAD_COUNT: 1,
|
||||||
UNREAD_COUNT: 2,
|
TOPIC_VISIBILITY: 2,
|
||||||
TOPIC_VISIBILITY: 3,
|
ACTION_MENU: 3,
|
||||||
ACTION_MENU: 4,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_ROW_FOCUS = 0;
|
const DEFAULT_ROW_FOCUS = 0;
|
||||||
const DEFAULT_COL_FOCUS = COLUMNS.COLLAPSE_BUTTON;
|
const DEFAULT_COL_FOCUS = COLUMNS.FULL_ROW;
|
||||||
|
|
||||||
const channel_view_navigation_state = {
|
const channel_view_navigation_state = {
|
||||||
channel_id: -1,
|
channel_id: -1,
|
||||||
@@ -298,13 +297,6 @@ export function show(filter?: Filter): void {
|
|||||||
assert(hide_other_views_callback !== undefined);
|
assert(hide_other_views_callback !== undefined);
|
||||||
hide_other_views_callback();
|
hide_other_views_callback();
|
||||||
const was_inbox_already_visible = inbox_util.is_visible();
|
const was_inbox_already_visible = inbox_util.is_visible();
|
||||||
// Avoid setting col_focus to recipient when moving to inbox from other narrows.
|
|
||||||
// We prefer to focus entire row instead of stream name for inbox-header.
|
|
||||||
// Since inbox-row doesn't has a collapse button, focus on COLUMNS.COLLAPSE_BUTTON
|
|
||||||
// is same as focus on COLUMNS.RECIPIENT. See `set_list_focus` for details.
|
|
||||||
if (!inbox_util.is_visible() && col_focus === COLUMNS.RECIPIENT) {
|
|
||||||
col_focus = COLUMNS.COLLAPSE_BUTTON;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we are already narrowed to the same channel view.
|
// Check if we are already narrowed to the same channel view.
|
||||||
const was_inbox_channel_view = inbox_util.is_channel_view();
|
const was_inbox_channel_view = inbox_util.is_channel_view();
|
||||||
@@ -1489,10 +1481,6 @@ function set_list_focus(input_key?: string): void {
|
|||||||
// This function is used for both revive_current_focus and
|
// This function is used for both revive_current_focus and
|
||||||
// setting focus after we modify col_focus and row_focus as per
|
// setting focus after we modify col_focus and row_focus as per
|
||||||
// hotkey pressed by user.
|
// hotkey pressed by user.
|
||||||
//
|
|
||||||
// When to focus on entire row?
|
|
||||||
// For `inbox-header`, when focus on COLUMNS.COLLAPSE_BUTTON
|
|
||||||
// For `inbox-row`, when focus on COLUMNS.COLLAPSE_BUTTON (fake) or COLUMNS.RECIPIENT
|
|
||||||
|
|
||||||
const $all_rows = get_all_rows();
|
const $all_rows = get_all_rows();
|
||||||
const max_row_focus = $all_rows.length - 1;
|
const max_row_focus = $all_rows.length - 1;
|
||||||
@@ -1510,82 +1498,48 @@ function set_list_focus(input_key?: string): void {
|
|||||||
const row_to_focus = $all_rows.get(row_focus);
|
const row_to_focus = $all_rows.get(row_focus);
|
||||||
assert(row_to_focus !== undefined);
|
assert(row_to_focus !== undefined);
|
||||||
const $row_to_focus = $(row_to_focus);
|
const $row_to_focus = $(row_to_focus);
|
||||||
// This includes a fake collapse button for `inbox-row` and a fake topic visibility
|
|
||||||
// button for `inbox-header`. The fake buttons help simplify code here and
|
|
||||||
// `$($cols_to_focus[col_focus]).trigger("focus");` at the end of this function.
|
|
||||||
const cols_to_focus = [row_to_focus, ...$row_to_focus.find("[tabindex=0]")];
|
|
||||||
const total_cols = cols_to_focus.length;
|
|
||||||
current_focus_id = $row_to_focus.attr("id");
|
current_focus_id = $row_to_focus.attr("id");
|
||||||
const is_header_row = is_row_a_header($row_to_focus);
|
const is_header_row = is_row_a_header($row_to_focus);
|
||||||
update_closed_compose_text($row_to_focus, is_header_row);
|
update_closed_compose_text($row_to_focus, is_header_row);
|
||||||
|
if (col_focus > COLUMNS.ACTION_MENU) {
|
||||||
// Loop through columns.
|
col_focus = COLUMNS.FULL_ROW;
|
||||||
if (col_focus > total_cols - 1) {
|
$row_to_focus.trigger("focus");
|
||||||
col_focus = 0;
|
return;
|
||||||
} else if (col_focus < 0) {
|
|
||||||
col_focus = total_cols - 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since header rows always have a collapse button, other rows have one less element to focus.
|
const cols_to_focus = [row_to_focus, ...$row_to_focus.find("[tabindex=0]")];
|
||||||
if (col_focus === COLUMNS.COLLAPSE_BUTTON) {
|
// We assumes that the last column has the highest index is the rightmost column.
|
||||||
if (!is_header_row && input_key !== undefined && LEFT_NAVIGATION_KEYS.includes(input_key)) {
|
const last_col_index = Number($(cols_to_focus.at(-1)!).attr("data-col-index")!);
|
||||||
// In `inbox-row` user pressed left on COLUMNS.RECIPIENT, so
|
|
||||||
// go to the last column.
|
if (col_focus < 0) {
|
||||||
col_focus = total_cols - 1;
|
col_focus = last_col_index;
|
||||||
}
|
$(cols_to_focus.at(-1)!).trigger("focus");
|
||||||
} else if (!is_header_row && col_focus === COLUMNS.RECIPIENT) {
|
return;
|
||||||
if (input_key !== undefined && RIGHT_NAVIGATION_KEYS.includes(input_key)) {
|
}
|
||||||
// In `inbox-row` user pressed right on COLUMNS.COLLAPSE_BUTTON.
|
|
||||||
// Since `inbox-row` has no collapse button, user wants to go
|
// This assumes that the last column has the highest index.
|
||||||
// to the unread count button.
|
if (col_focus > last_col_index) {
|
||||||
col_focus = COLUMNS.UNREAD_COUNT;
|
col_focus = 0;
|
||||||
} else if (input_key !== undefined && LEFT_NAVIGATION_KEYS.includes(input_key)) {
|
$(cols_to_focus[0]!).trigger("focus");
|
||||||
// In `inbox-row` user pressed left on COLUMNS.UNREAD_COUNT,
|
return;
|
||||||
// we move focus to COLUMNS.COLLAPSE_BUTTON so that moving
|
}
|
||||||
// up or down to `inbox-header` keeps the entire row focused for the
|
|
||||||
// `inbox-header` too.
|
// Find the closest column to focus based on the input key.
|
||||||
col_focus = COLUMNS.COLLAPSE_BUTTON;
|
let equal = (a: number, b: number): boolean => b >= a;
|
||||||
} else {
|
if (input_key && LEFT_NAVIGATION_KEYS.includes(input_key)) {
|
||||||
// up / down arrow
|
equal = (a: number, b: number): boolean => a >= b;
|
||||||
// For `inbox-row`, we focus entire row for COLUMNS.RECIPIENT.
|
cols_to_focus.reverse();
|
||||||
$row_to_focus.trigger("focus");
|
}
|
||||||
|
|
||||||
|
for (const col of cols_to_focus) {
|
||||||
|
const col_index = Number($(col).attr("data-col-index"));
|
||||||
|
if (equal(col_focus, col_index)) {
|
||||||
|
col_focus = col_index;
|
||||||
|
$(col).trigger("focus");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (is_header_row && col_focus === COLUMNS.TOPIC_VISIBILITY) {
|
|
||||||
// `inbox-header` doesn't have a topic visibility indicator, so focus on
|
|
||||||
// button around it instead.
|
|
||||||
if (input_key !== undefined && LEFT_NAVIGATION_KEYS.includes(input_key)) {
|
|
||||||
col_focus = COLUMNS.UNREAD_COUNT;
|
|
||||||
} else {
|
|
||||||
col_focus = COLUMNS.ACTION_MENU;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus on appropriate button if the is row has no unreads but
|
|
||||||
// `col_focus` is set to `UNREAD_COUNT` column.
|
|
||||||
if (!is_header_row && col_focus === COLUMNS.UNREAD_COUNT) {
|
|
||||||
const row_has_unreads = cols_to_focus.some((col) =>
|
|
||||||
col.classList.contains("unread-count-focus-outline"),
|
|
||||||
);
|
|
||||||
if (!row_has_unreads) {
|
|
||||||
if (input_key !== undefined && RIGHT_NAVIGATION_KEYS.includes(input_key)) {
|
|
||||||
col_focus = COLUMNS.TOPIC_VISIBILITY;
|
|
||||||
} else if (input_key !== undefined && LEFT_NAVIGATION_KEYS.includes(input_key)) {
|
|
||||||
// Focus on entire row.
|
|
||||||
col_focus = COLUMNS.COLLAPSE_BUTTON;
|
|
||||||
} else {
|
|
||||||
// up / down arrow
|
|
||||||
// Focus on the entire row without changing `col_focus` so that
|
|
||||||
// we can focus on unread count button if the next row has one.
|
|
||||||
$row_to_focus.trigger("focus");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const col_to_focus = cols_to_focus[col_focus];
|
|
||||||
assert(col_to_focus !== undefined);
|
|
||||||
$(col_to_focus).trigger("focus");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus_filters_dropdown(): void {
|
function focus_filters_dropdown(): void {
|
||||||
@@ -2019,10 +1973,6 @@ function get_focus_class_for_header(): string {
|
|||||||
let focus_class = ".collapsible-button";
|
let focus_class = ".collapsible-button";
|
||||||
|
|
||||||
switch (col_focus) {
|
switch (col_focus) {
|
||||||
case COLUMNS.RECIPIENT: {
|
|
||||||
focus_class = ".inbox-header-name a";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case COLUMNS.UNREAD_COUNT: {
|
case COLUMNS.UNREAD_COUNT: {
|
||||||
focus_class = ".unread_count";
|
focus_class = ".unread_count";
|
||||||
break;
|
break;
|
||||||
@@ -2181,7 +2131,7 @@ export function initialize({hide_other_views}: {hide_other_views: () => void}):
|
|||||||
const $elt = $(this);
|
const $elt = $(this);
|
||||||
const container_id = $elt.parents(".inbox-header").attr("id");
|
const container_id = $elt.parents(".inbox-header").attr("id");
|
||||||
assert(container_id !== undefined);
|
assert(container_id !== undefined);
|
||||||
col_focus = COLUMNS.COLLAPSE_BUTTON;
|
col_focus = COLUMNS.FULL_ROW;
|
||||||
focus_clicked_list_element($elt);
|
focus_clicked_list_element($elt);
|
||||||
collapse_or_expand(container_id);
|
collapse_or_expand(container_id);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -2209,12 +2159,11 @@ export function initialize({hide_other_views}: {hide_other_views: () => void}):
|
|||||||
|
|
||||||
let $elt = $(this);
|
let $elt = $(this);
|
||||||
const href = $elt.find("a").attr("href");
|
const href = $elt.find("a").attr("href");
|
||||||
|
col_focus = COLUMNS.FULL_ROW;
|
||||||
if (href !== undefined) {
|
if (href !== undefined) {
|
||||||
col_focus = COLUMNS.RECIPIENT;
|
|
||||||
window.location.href = href;
|
window.location.href = href;
|
||||||
} else {
|
} else {
|
||||||
$elt = $elt.closest(".inbox-header");
|
$elt = $elt.closest(".inbox-header");
|
||||||
col_focus = COLUMNS.COLLAPSE_BUTTON;
|
|
||||||
collapse_or_expand($elt.attr("id")!);
|
collapse_or_expand($elt.attr("id")!);
|
||||||
}
|
}
|
||||||
focus_clicked_list_element($elt);
|
focus_clicked_list_element($elt);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<div id="{{ header_id }}" tabindex="0" class="inbox-header inbox-folder {{#unless is_header_visible}}hidden_by_filters{{/unless}} {{#if is_collapsed}}inbox-collapsed-state{{/if}}">
|
{{!-- col-index `0` is COLUMNS.FULL_ROW --}}
|
||||||
|
<div id="{{ header_id }}" tabindex="0" class="inbox-header inbox-folder {{#unless is_header_visible}}hidden_by_filters{{/unless}} {{#if is_collapsed}}inbox-collapsed-state{{/if}}" data-col-index="0">
|
||||||
<div class="inbox-focus-border">
|
<div class="inbox-focus-border">
|
||||||
<div class="inbox-left-part-wrapper">
|
<div class="inbox-left-part-wrapper">
|
||||||
<div class="inbox-left-part">
|
<div class="inbox-left-part">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{#if is_stream}}
|
{{#if is_stream}}
|
||||||
{{> inbox_stream_header_row .}}
|
{{> inbox_stream_header_row .}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<div id="inbox-row-conversation-{{conversation_key}}" class="inbox-row {{#if is_hidden}}hidden_by_filters{{/if}}" tabindex="0" data-col-index="{{ column_indexes.RECIPIENT }}">
|
<div id="inbox-row-conversation-{{conversation_key}}" class="inbox-row {{#if is_hidden}}hidden_by_filters{{/if}}" tabindex="0" data-col-index="{{ column_indexes.FULL_ROW }}">
|
||||||
<div class="inbox-focus-border">
|
<div class="inbox-focus-border">
|
||||||
<div class="inbox-left-part-wrapper">
|
<div class="inbox-left-part-wrapper">
|
||||||
<div class="inbox-left-part">
|
<div class="inbox-left-part">
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
<span class="recipients_name">{{{rendered_dm_with}}}</span>
|
<span class="recipients_name">{{{rendered_dm_with}}}</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="hide fake-collapse-button" tabindex="0" data-col-index="{{ column_indexes.COLLAPSE_BUTTON }}"></div>
|
|
||||||
<span class="unread_mention_info tippy-zulip-tooltip
|
<span class="unread_mention_info tippy-zulip-tooltip
|
||||||
{{#unless has_unread_mention}}hidden{{/unless}}"
|
{{#unless has_unread_mention}}hidden{{/unless}}"
|
||||||
data-tippy-content="{{t 'You have unread mentions' }}">@</span>
|
data-tippy-content="{{t 'You have unread mentions' }}">@</span>
|
||||||
@@ -31,7 +30,6 @@
|
|||||||
<div class="inbox-topic-name">
|
<div class="inbox-topic-name">
|
||||||
<a tabindex="-1" href="{{topic_url}}" {{#if is_empty_string_topic}}class="empty-topic-display"{{/if}}>{{topic_display_name}}</a>
|
<a tabindex="-1" href="{{topic_url}}" {{#if is_empty_string_topic}}class="empty-topic-display"{{/if}}>{{topic_display_name}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hide fake-collapse-button" tabindex="0" data-col-index="{{ column_indexes.COLLAPSE_BUTTON }}"></div>
|
|
||||||
<span class="unread_mention_info tippy-zulip-tooltip
|
<span class="unread_mention_info tippy-zulip-tooltip
|
||||||
{{#unless mention_in_unread}}hidden{{/unless}}"
|
{{#unless mention_in_unread}}hidden{{/unless}}"
|
||||||
data-tippy-content="{{t 'You have unread mentions'}}">@</span>
|
data-tippy-content="{{t 'You have unread mentions'}}">@</span>
|
||||||
@@ -43,8 +41,6 @@
|
|||||||
{{unread_count}}
|
{{unread_count}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
|
||||||
<div class="hide fake-unread-count" tabindex="0" data-col-index="{{ column_indexes.UNREAD_COUNT }}"></div>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div id="inbox-stream-header-{{stream_id}}" class="inbox-header {{#if is_hidden}}hidden_by_filters{{/if}} {{#if is_collapsed}}inbox-collapsed-state{{/if}}" data-col-index="{{ column_indexes.COLLAPSE_BUTTON }}" tabindex="0" data-stream-id="{{stream_id}}" style="background: {{stream_header_color}};">
|
<div id="inbox-stream-header-{{stream_id}}" class="inbox-header {{#if is_hidden}}hidden_by_filters{{/if}} {{#if is_collapsed}}inbox-collapsed-state{{/if}}" data-col-index="{{ column_indexes.FULL_ROW }}" tabindex="0" data-stream-id="{{stream_id}}" style="background: {{stream_header_color}};">
|
||||||
<div class="inbox-focus-border">
|
<div class="inbox-focus-border">
|
||||||
<div class="inbox-left-part-wrapper">
|
<div class="inbox-left-part-wrapper">
|
||||||
<div class="inbox-left-part">
|
<div class="inbox-left-part">
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
data-stream-id="{{stream_id}}" data-tippy-content="{{t 'Mark as read' }}"
|
data-stream-id="{{stream_id}}" data-tippy-content="{{t 'Mark as read' }}"
|
||||||
aria-label="{{t 'Mark as read' }}">{{unread_count}}</span>
|
aria-label="{{t 'Mark as read' }}">{{unread_count}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="topic-visibility-indicator invisible" tabindex="0"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inbox-right-part-wrapper">
|
<div class="inbox-right-part-wrapper">
|
||||||
|
|||||||
Reference in New Issue
Block a user