Files
element-call/src/useCallViewKeyboardShortcuts.ts

116 lines
3.3 KiB
TypeScript
Raw Normal View History

/*
Copyright 2022-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type RefObject, useCallback, useMemo, useRef } from "react";
import { useEventTarget } from "./useEvents";
import {
type ReactionOption,
ReactionSet,
ReactionsRowSize,
} from "./reactions";
/**
* Determines whether focus is in the same part of the tree as the given
* element (specifically, if the element or an ancestor of it is focused).
*/
const mayReceiveKeyEvents = (e: HTMLElement): boolean => {
2023-04-19 15:54:39 -04:00
const focusedElement = document.activeElement;
return focusedElement !== null && focusedElement.contains(e);
2023-04-19 15:54:39 -04:00
};
const KeyToReactionMap: Record<string, ReactionOption> = Object.fromEntries(
ReactionSet.slice(0, ReactionsRowSize).map((r, i) => [(i + 1).toString(), r]),
);
2023-01-13 11:52:40 +00:00
export function useCallViewKeyboardShortcuts(
focusElement: RefObject<HTMLElement | null>,
2025-08-29 18:46:24 +02:00
toggleAudio: (() => void) | null,
toggleVideo: (() => void) | null,
setAudioEnabled: ((enabled: boolean) => void) | null,
sendReaction: (reaction: ReactionOption) => void,
toggleHandRaised: () => void,
): void {
const spacebarHeld = useRef(false);
// These event handlers are set on the window because we want users to be able
// to trigger them without going to the trouble of focusing something
useEventTarget(
window,
"keydown",
useCallback(
(event: KeyboardEvent) => {
2023-04-19 15:54:39 -04:00
if (focusElement.current === null) return;
if (!mayReceiveKeyEvents(focusElement.current)) return;
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
return;
if (event.key === "m") {
event.preventDefault();
2025-08-29 18:46:24 +02:00
toggleAudio?.();
} else if (event.key === "v") {
event.preventDefault();
2025-08-29 18:46:24 +02:00
toggleVideo?.();
} else if (event.key === " ") {
event.preventDefault();
if (!spacebarHeld.current) {
spacebarHeld.current = true;
2025-08-29 18:46:24 +02:00
setAudioEnabled?.(true);
}
} else if (event.key === "h") {
event.preventDefault();
toggleHandRaised();
} else if (KeyToReactionMap[event.key]) {
event.preventDefault();
sendReaction(KeyToReactionMap[event.key]);
}
},
2023-04-19 15:55:55 -04:00
[
focusElement,
2025-08-29 18:46:24 +02:00
toggleVideo,
toggleAudio,
setAudioEnabled,
sendReaction,
toggleHandRaised,
2023-10-11 10:42:04 -04:00
],
),
// Because this is set on the window, to prevent shortcuts from activating
// another event callback at the same time, we need to preventDefault
// *before* child elements receive the event by using capture mode
useMemo(() => ({ capture: true }), []),
);
useEventTarget(
window,
"keyup",
useCallback(
(event: KeyboardEvent) => {
2023-04-19 15:54:39 -04:00
if (focusElement.current === null) return;
if (!mayReceiveKeyEvents(focusElement.current)) return;
if (event.key === " ") {
spacebarHeld.current = false;
2025-08-29 18:46:24 +02:00
setAudioEnabled?.(false);
}
},
2025-08-29 18:46:24 +02:00
[focusElement, setAudioEnabled],
2023-10-11 10:42:04 -04:00
),
);
useEventTarget(
window,
"blur",
useCallback(() => {
if (spacebarHeld.current) {
spacebarHeld.current = false;
2025-08-29 18:46:24 +02:00
setAudioEnabled?.(true);
}
2025-08-29 18:46:24 +02:00
}, [setAudioEnabled, spacebarHeld]),
);
}