From ec7dd2554a97aa30e74dbbe2792f9f677644d8e6 Mon Sep 17 00:00:00 2001 From: WofWca Date: Fri, 18 Oct 2024 20:29:43 +0400 Subject: [PATCH] improvement: arrow-key navigation for chat list Implements the "roving tabindex" approach: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#technique_1_roving_tabindex. Supersedes https://github.com/deltachat/deltachat-desktop/pull/4211. This improves things, but the UX as a whole is not great yet: - The tab order is such that the chat list does not immediately follow the search field, so you have to tab through the other navbar items before you get to the chat list. Same for going back from the chat list to the search bar. - The initially "active" element is just the first chat item, and not the currently selected chat. - Since the chat list is "virtualized", the currently active element might get removed from DOM when the user scrolls, thus we lose track of the item that was last selected. Related: https://github.com/deltachat/deltachat-desktop/issues/2784 --- .../frontend/src/components/chat/ChatList.tsx | 188 ++++++++------- .../src/components/chat/ChatListItem.tsx | 63 +++++- .../frontend/src/contexts/RovingTabindex.tsx | 214 ++++++++++++++++++ 3 files changed, 373 insertions(+), 92 deletions(-) create mode 100644 packages/frontend/src/contexts/RovingTabindex.tsx diff --git a/packages/frontend/src/components/chat/ChatList.tsx b/packages/frontend/src/components/chat/ChatList.tsx index 62946acab5..3e6ed4bf74 100644 --- a/packages/frontend/src/components/chat/ChatList.tsx +++ b/packages/frontend/src/components/chat/ChatList.tsx @@ -44,6 +44,7 @@ import type { MessageChatListItemData, } from './ChatListItemRow' import { isInviteLink } from '../../../../shared/util' +import { RovingTabindexProvider } from '../../contexts/RovingTabindex' const enum LoadStatus { FETCHING = 1, @@ -163,6 +164,8 @@ export default function ChatList(props: { const createChatByContactId = useCreateChatByContactId() const { selectChat } = useChat() + const tabindexWrapperElement = useRef(null) + const addContactOnClick = async () => { if (!queryStrIsValidEmail || !queryStr) return @@ -344,27 +347,31 @@ export default function ChatList(props: {
{({ width, height }) => ( -
+
{tx('search_in', searchChatInfo.name)} {messageResultIds.length !== 0 && ': ' + translate_n('n_messages', messageResultIds.length)}
- 'key' + messageResultIds[index]} - itemData={messagelistData} - itemHeight={CHATLISTITEM_MESSAGE_HEIGHT} + - {ChatListItemRowMessage} - + 'key' + messageResultIds[index]} + itemData={messagelistData} + itemHeight={CHATLISTITEM_MESSAGE_HEIGHT} + > + {ChatListItemRowMessage} + +
)} @@ -378,90 +385,97 @@ export default function ChatList(props: {
{({ width, height }) => ( -
+
{isSearchActive && (
{translate_n('n_chats', chatListIds.length)}
)} - | null) => - ((listRefRef.current as any) = ref) - } - itemKey={index => 'key' + chatListIds[index]} - itemData={chatlistData} - itemHeight={CHATLISTITEM_CHAT_HEIGHT} + {/* TODO RovingTabindex doesn't work well with virtualized + lists, because the currently active element might get removed + from DOM if scrolled out of view. */} + - {ChatListItemRowChat} - - {isSearchActive && ( - <> -
- {translate_n('n_contacts', contactIds.length)} -
- 'key' + contactIds[index]} - itemData={contactlistData} - itemHeight={CHATLISTITEM_CONTACT_HEIGHT} - > - {ChatListItemRowContact} - - {contactIds.length === 0 && - chatListIds.length === 0 && - queryStrIsValidEmail && ( + | null) => + ((listRefRef.current as any) = ref) + } + itemKey={index => 'key' + chatListIds[index]} + itemData={chatlistData} + itemHeight={CHATLISTITEM_CHAT_HEIGHT} + > + {ChatListItemRowChat} + + {isSearchActive && ( + <> +
+ {translate_n('n_contacts', contactIds.length)} +
+ 'key' + contactIds[index]} + itemData={contactlistData} + itemHeight={CHATLISTITEM_CONTACT_HEIGHT} + > + {ChatListItemRowContact} + + {contactIds.length === 0 && + chatListIds.length === 0 && + queryStrIsValidEmail && ( +
+ +
+ )} + {showPseudoListItemAddContactFromInviteLink && (
-
)} - {showPseudoListItemAddContactFromInviteLink && ( -
- +
+ {translated_messages_label(messageResultIds.length)}
- )} -
- {translated_messages_label(messageResultIds.length)} -
- 'key' + messageResultIds[index]} - itemData={messagelistData} - itemHeight={CHATLISTITEM_MESSAGE_HEIGHT} - > - {ChatListItemRowMessage} - - - )} + 'key' + messageResultIds[index]} + itemData={messagelistData} + itemHeight={CHATLISTITEM_MESSAGE_HEIGHT} + > + {ChatListItemRowMessage} + + + )} +
(null) + + const { + tabIndex, + onKeydown: tabindexOnKeydown, + setAsActiveElement: tabindexSetAsActiveElement, + } = useRovingTabindex(ref) + return (