RocketChat/Rocket.Chat

View on GitHub
apps/meteor/client/views/room/hooks/useMessageListNavigation.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import { createFocusManager } from '@react-aria/focus';
import type { RefCallback } from 'react';
import { useCallback } from 'react';
import { useFocusManager } from 'react-aria';

const isListItem = (node: EventTarget) =>
    (node as HTMLElement).getAttribute('role') === 'listitem' || (node as HTMLElement).getAttribute('role') === 'link';
const isMessageToolbarAction = (node: EventTarget) => (node as HTMLElement).parentElement?.getAttribute('role') === 'toolbar';
const isSystemMessage = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-message-system');
const isThreadMessage = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-message-thread');

/**
 * Custom hook to provide the room navigation by keyboard.
 * @param ref - A ref to the message list DOM element.
 */
export const useMessageListNavigation = (): { messageListRef: RefCallback<HTMLElement> } => {
    const roomFocusManager = useFocusManager();

    const messageListRef = useCallback(
        (node: HTMLElement | null) => {
            let lastMessageFocused: HTMLElement | null = null;
            let initialFocus = true;

            if (!node) {
                return;
            }

            const massageListFocusManager = createFocusManager({
                current: node,
            });

            node.addEventListener('keydown', (e) => {
                if (!e.target) {
                    return;
                }

                if (!isListItem(e.target)) {
                    return;
                }

                if (e.key === 'Tab') {
                    if (e.shiftKey) {
                        e.preventDefault();
                        e.stopPropagation();

                        roomFocusManager.focusFirst({
                            from: document.getElementsByClassName('rcx-room-header')[0],
                        });
                    } else if (isThreadMessage(e.target) || isSystemMessage(e.target) || isMessageToolbarAction(e.target)) {
                        e.preventDefault();
                        e.stopPropagation();

                        roomFocusManager.focusNext({
                            accept: (node) => node.tagName === 'TEXTAREA',
                        });
                    }
                }

                if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
                    if (e.key === 'ArrowUp') {
                        massageListFocusManager.focusPrevious({ accept: (node) => isListItem(node) });
                    }

                    if (e.key === 'ArrowDown') {
                        massageListFocusManager.focusNext({ accept: (node) => isListItem(node) });
                    }

                    lastMessageFocused = document.activeElement as HTMLElement;
                }
            });

            node.addEventListener(
                'blur',
                (e) => {
                    if (
                        !(e.relatedTarget as HTMLElement)?.classList.contains('focus-visible') ||
                        !(e.currentTarget instanceof HTMLElement && e.relatedTarget instanceof HTMLElement)
                    ) {
                        return;
                    }

                    if (!e.currentTarget.contains(e.relatedTarget) && !lastMessageFocused) {
                        lastMessageFocused = e.target as HTMLElement;
                    }
                },
                { capture: true },
            );

            node.addEventListener(
                'focus',
                (e) => {
                    const triggeredByKeyboard = (e.target as HTMLElement)?.classList.contains('focus-visible');
                    if (!triggeredByKeyboard || !(e.currentTarget instanceof HTMLElement && e.relatedTarget instanceof HTMLElement)) {
                        return;
                    }

                    if (initialFocus) {
                        massageListFocusManager.focusLast({ accept: (node) => isListItem(node) });
                        lastMessageFocused = document.activeElement as HTMLElement;
                        initialFocus = false;
                        return;
                    }

                    if (lastMessageFocused && !e.currentTarget.contains(e.relatedTarget) && node.contains(e.target as HTMLElement)) {
                        lastMessageFocused?.focus();
                    }
                },
                { capture: true },
            );
        },
        [roomFocusManager],
    );

    return {
        messageListRef,
    };
};