refactor: useKeyboardEvents composable (#9959)

This PR has the following changes

1. Fix tab styles issue caused by adding an additional wrapper for
getting an element ref on `ChatTypeTabs.vue`
2. Refactor `useKeyboardEvents` composable to not require an element
ref. It will use a local abort controller to abort any listener

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Shivam Mishra
2024-08-22 16:40:55 +05:30
committed by GitHub
parent 776579ba5b
commit dadd572f9d
22 changed files with 56 additions and 160 deletions

View File

@@ -141,7 +141,7 @@ export default {
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents, conversationListRef);
useKeyboardEvents(keyboardEvents);
return {
uiSettings,

View File

@@ -19,7 +19,6 @@ const store = useStore();
const getters = useStoreGetters();
const { t } = useI18n();
const resolveActionsRef = ref(null);
const arrowDownButtonRef = ref(null);
const isLoading = ref(false);
@@ -131,17 +130,14 @@ const keyboardEvents = {
},
};
useKeyboardEvents(keyboardEvents, resolveActionsRef);
useKeyboardEvents(keyboardEvents);
useEmitter(CMD_REOPEN_CONVERSATION, onCmdOpenConversation);
useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
</script>
<template>
<div
ref="resolveActionsRef"
class="relative flex items-center justify-end resolve-actions"
>
<div class="relative flex items-center justify-end resolve-actions">
<div class="button-group">
<woot-button
v-if="isOpen"

View File

@@ -1,5 +1,4 @@
<script>
import { ref } from 'vue';
import { mapGetters } from 'vuex';
import { getSidebarItems } from './config/default-sidebar';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
@@ -29,7 +28,6 @@ export default {
},
},
setup(props, { emit }) {
const sidebarRef = ref(null);
const route = useRoute();
const router = useRouter();
@@ -67,11 +65,10 @@ export default {
action: () => navigateToRoute('agent_list'),
},
};
useKeyboardEvents(keyboardEvents, sidebarRef);
useKeyboardEvents(keyboardEvents);
return {
toggleKeyShortcutModal,
sidebarRef,
};
},
data() {
@@ -201,7 +198,7 @@ export default {
</script>
<template>
<aside ref="sidebarRef" class="flex h-full">
<aside class="flex h-full">
<PrimarySidebar
:logo-source="globalConfig.logoThumbnail"
:installation-name="globalConfig.installationName"

View File

@@ -23,7 +23,6 @@ export default {
const { isAdmin } = useAdmin();
const aiAssistanceButtonRef = ref(null);
const initialMessage = ref('');
const initializeMessage = draftMsg => {
@@ -40,13 +39,12 @@ export default {
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents, aiAssistanceButtonRef);
useKeyboardEvents(keyboardEvents);
return {
uiSettings,
updateUISettings,
isAdmin,
aiAssistanceButtonRef,
initialMessage,
initializeMessage,
recordAnalytics,
@@ -122,7 +120,7 @@ export default {
</script>
<template>
<div ref="aiAssistanceButtonRef">
<div>
<div v-if="isAIIntegrationEnabled" class="relative">
<AIAssistanceCTAButton
v-if="shouldShowAIAssistCTAButton"

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, computed } from 'vue';
import { computed } from 'vue';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import wootConstants from 'dashboard/constants/globals';
@@ -16,8 +16,6 @@ const props = defineProps({
const emit = defineEmits(['chatTabChange']);
const chatTypeTabsRef = ref(null);
const activeTabIndex = computed(() => {
return props.items.findIndex(item => item.key === props.activeTab);
});
@@ -39,24 +37,23 @@ const keyboardEvents = {
},
},
};
useKeyboardEvents(keyboardEvents, chatTypeTabsRef);
useKeyboardEvents(keyboardEvents);
</script>
<template>
<div ref="chatTypeTabsRef">
<woot-tabs
:index="activeTabIndex"
class="tab--chat-type py-0 px-4 w-full"
@change="onTabChange"
>
<woot-tabs-item
v-for="item in items"
:key="item.key"
:name="item.name"
:count="item.count"
/>
</woot-tabs>
</div>
<woot-tabs
:index="activeTabIndex"
class="w-full px-4 py-0 tab--chat-type"
@change="onTabChange"
>
<woot-tabs-item
v-for="item in items"
:key="item.key"
:name="item.name"
:count="item.count"
/>
</woot-tabs>
</template>
<style scoped lang="scss">

View File

@@ -18,8 +18,6 @@ const props = defineProps({
const emit = defineEmits(['add', 'remove']);
const labelSelectorWrapRef = ref(null);
const { isAdmin } = useAdmin();
const showSearchDropdownLabel = ref(false);
@@ -57,15 +55,11 @@ const keyboardEvents = {
},
};
useKeyboardEvents(keyboardEvents, labelSelectorWrapRef);
useKeyboardEvents(keyboardEvents);
</script>
<template>
<div
ref="labelSelectorWrapRef"
v-on-clickaway="closeDropdownLabel"
class="relative leading-6"
>
<div v-on-clickaway="closeDropdownLabel" class="relative leading-6">
<AddLabel @add="toggleLabels" />
<woot-label
v-for="label in savedLabels"

View File

@@ -1,5 +1,4 @@
<script>
import { ref, watchEffect, computed } from 'vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import FileUpload from 'vue-upload-component';
@@ -117,15 +116,13 @@ export default {
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
useUISettings();
const uploadRef = ref(null);
// TODO: This is really hacky, we need to replace the file picker component with
// a custom one, where the logic and the component markup is isolated.
// Once we have the custom component, we can remove the hacky logic below.
const uploadRefElem = computed(() => uploadRef.value?.$el);
const keyboardEvents = {
'Alt+KeyA': {
action: () => {
// TODO: This is really hacky, we need to replace the file picker component with
// a custom one, where the logic and the component markup is isolated.
// Once we have the custom component, we can remove the hacky logic below.
const uploadTriggerButton = document.querySelector(
'#conversationAttachment'
);
@@ -135,14 +132,11 @@ export default {
},
};
watchEffect(() => {
useKeyboardEvents(keyboardEvents, uploadRefElem);
});
useKeyboardEvents(keyboardEvents);
return {
setSignatureFlagForInbox,
fetchSignatureFlagFromUISettings,
uploadRef,
};
},
computed: {
@@ -267,7 +261,6 @@ export default {
@click="toggleEmojiPicker"
/>
<FileUpload
ref="uploadRef"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
input-id="conversationAttachment"
:size="4096 * 4096"

View File

@@ -1,5 +1,4 @@
<script>
import { ref } from 'vue';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
export default {
@@ -23,8 +22,6 @@ export default {
},
},
setup(props, { emit }) {
const replyTopRef = ref(null);
const setReplyMode = mode => {
emit('setReplyMode', mode);
};
@@ -44,12 +41,11 @@ export default {
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents, replyTopRef);
useKeyboardEvents(keyboardEvents);
return {
handleReplyClick,
handleNoteClick,
replyTopRef,
};
},
computed: {
@@ -76,10 +72,7 @@ export default {
</script>
<template>
<div
ref="replyTopRef"
class="flex justify-between bg-black-50 dark:bg-slate-800"
>
<div class="flex justify-between bg-black-50 dark:bg-slate-800">
<div class="button-group">
<woot-button
variant="clear"

View File

@@ -1,5 +1,4 @@
<script>
import { ref } from 'vue';
import { mapGetters } from 'vuex';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import BackButton from '../BackButton.vue';
@@ -43,18 +42,12 @@ export default {
},
},
setup(props, { emit }) {
const conversationHeaderActionsRef = ref(null);
const keyboardEvents = {
'Alt+KeyO': {
action: () => emit('contactPanelToggle'),
},
};
useKeyboardEvents(keyboardEvents, conversationHeaderActionsRef);
return {
conversationHeaderActionsRef,
};
useKeyboardEvents(keyboardEvents);
},
computed: {
...mapGetters({
@@ -182,7 +175,6 @@ export default {
</div>
<div
ref="conversationHeaderActionsRef"
class="flex items-center gap-2 overflow-hidden text-xs conversation--header--actions text-ellipsis whitespace-nowrap"
>
<InboxName v-if="hasMultipleInboxes" :inbox="inbox" />

View File

@@ -52,7 +52,6 @@ export default {
},
},
setup() {
const conversationFooterRef = ref(null);
const isPopOutReplyBox = ref(false);
const { isEnterprise } = useConfig();
@@ -70,7 +69,7 @@ export default {
},
};
useKeyboardEvents(keyboardEvents, conversationFooterRef);
useKeyboardEvents(keyboardEvents);
const {
isAIIntegrationEnabled,
@@ -81,7 +80,6 @@ export default {
return {
isEnterprise,
conversationFooterRef,
isPopOutReplyBox,
closePopOutReplyBox,
showPopOutReplyBox,
@@ -529,7 +527,6 @@ export default {
/>
</ul>
<div
ref="conversationFooterRef"
class="conversation-footer"
:class="{ 'modal-mask': isPopOutReplyBox }"
>

View File

@@ -40,7 +40,6 @@ const onSelect = () => {
};
useKeyboardNavigableList({
elementRef: tagAgentsRef,
items,
onSelect,
adjustScroll,

View File

@@ -35,7 +35,6 @@ const ALLOWED_FILE_TYPES = {
const MAX_ZOOM_LEVEL = 2;
const MIN_ZOOM_LEVEL = 1;
const galleryViewRef = ref(null);
const zoomScale = ref(1);
const activeAttachment = ref({});
const activeFileType = ref('');
@@ -202,7 +201,7 @@ const keyboardEvents = {
},
},
};
useKeyboardEvents(keyboardEvents, galleryViewRef);
useKeyboardEvents(keyboardEvents);
onMounted(() => {
setImageAndVideoSrc(props.attachment);
@@ -218,7 +217,6 @@ onMounted(() => {
:on-close="onClose"
>
<div
ref="galleryViewRef"
v-on-clickaway="onClose"
class="bg-white dark:bg-slate-900 flex flex-col h-[inherit] w-[inherit] overflow-hidden"
@click="onClose"

View File

@@ -39,7 +39,6 @@ const onSelect = () => {
};
useKeyboardNavigableList({
elementRef: mentionsListContainerRef,
items: computed(() => props.items),
onSelect,
adjustScroll,

View File

@@ -1,4 +1,3 @@
import { unref } from 'vue';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
describe('useKeyboardEvents', () => {
@@ -11,15 +10,13 @@ describe('useKeyboardEvents', () => {
});
it('should set up listeners on mount and remove them on unmount', async () => {
const el = document.createElement('div');
const elRef = unref({ value: el });
const events = {
'ALT+KeyL': () => {},
};
const mountedMock = vi.fn();
const unmountedMock = vi.fn();
useKeyboardEvents(events, elRef);
useKeyboardEvents(events);
mountedMock();
unmountedMock();

View File

@@ -9,7 +9,6 @@ vi.mock('../useKeyboardEvents', () => ({
}));
describe('useKeyboardNavigableList', () => {
let elementRef;
let items;
let onSelect;
let adjustScroll;
@@ -18,7 +17,6 @@ describe('useKeyboardNavigableList', () => {
const createMockEvent = () => ({ preventDefault: vi.fn() });
beforeEach(() => {
elementRef = ref(null);
items = ref(['item1', 'item2', 'item3']);
onSelect = vi.fn();
adjustScroll = vi.fn();
@@ -28,7 +26,6 @@ describe('useKeyboardNavigableList', () => {
it('should return moveSelectionUp and moveSelectionDown functions', () => {
const result = useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -43,7 +40,6 @@ describe('useKeyboardNavigableList', () => {
it('should move selection up correctly', () => {
const { moveSelectionUp } = useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -65,7 +61,6 @@ describe('useKeyboardNavigableList', () => {
it('should move selection down correctly', () => {
const { moveSelectionDown } = useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -87,7 +82,6 @@ describe('useKeyboardNavigableList', () => {
it('should call adjustScroll after moving selection', () => {
const { moveSelectionUp, moveSelectionDown } = useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -103,7 +97,6 @@ describe('useKeyboardNavigableList', () => {
it('should include Enter key handler when onSelect is provided', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -118,7 +111,6 @@ describe('useKeyboardNavigableList', () => {
it('should not include Enter key handler when onSelect is not provided', () => {
useKeyboardNavigableList({
elementRef,
items,
adjustScroll,
selectedIndex,
@@ -131,7 +123,6 @@ describe('useKeyboardNavigableList', () => {
it('should not trigger onSelect when items are empty', () => {
const { moveSelectionUp, moveSelectionDown } = useKeyboardNavigableList({
elementRef,
items: ref([]),
onSelect,
adjustScroll,
@@ -145,23 +136,18 @@ describe('useKeyboardNavigableList', () => {
it('should call useKeyboardEvents with correct parameters', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
expect(useKeyboardEvents).toHaveBeenCalledWith(
expect.any(Object),
elementRef
);
expect(useKeyboardEvents).toHaveBeenCalledWith(expect.any(Object));
});
// Keyboard event handlers
it('should handle ArrowUp key', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -178,7 +164,6 @@ describe('useKeyboardNavigableList', () => {
it('should handle Control+KeyP', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -195,7 +180,6 @@ describe('useKeyboardNavigableList', () => {
it('should handle ArrowDown key', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -212,7 +196,6 @@ describe('useKeyboardNavigableList', () => {
it('should handle Control+KeyN', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -229,7 +212,6 @@ describe('useKeyboardNavigableList', () => {
it('should handle Enter key when onSelect is provided', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -245,7 +227,6 @@ describe('useKeyboardNavigableList', () => {
it('should not have Enter key handler when onSelect is not provided', () => {
useKeyboardNavigableList({
elementRef,
items,
adjustScroll,
selectedIndex,
@@ -257,7 +238,6 @@ describe('useKeyboardNavigableList', () => {
it('should set allowOnFocusedInput to true for all key handlers', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,

View File

@@ -1,4 +1,3 @@
import { onMounted, onBeforeUnmount, unref } from 'vue';
import {
isActiveElementTypeable,
isEscape,
@@ -7,8 +6,7 @@ import {
} from 'shared/helpers/KeyboardHelpers';
import { useDetectKeyboardLayout } from 'dashboard/composables/useDetectKeyboardLayout';
import { createKeybindingsHandler } from 'tinykeys';
const keyboardListenerMap = new WeakMap();
import { onUnmounted, onMounted } from 'vue';
/**
* Determines if the keyboard event should be ignored based on the element type and handler settings.
@@ -69,49 +67,24 @@ async function wrapEventsInKeybindingsHandler(events) {
return wrappedEvents;
}
/**
* Sets up keyboard event listeners on the specified element.
* @param {Element} root - The DOM element to attach listeners to.
* @param {Object} events - The events to listen for.
*/
const setupListeners = (root, events) => {
if (root instanceof Element && events) {
const keydownHandler = createKeybindingsHandler(events);
document.addEventListener('keydown', keydownHandler);
keyboardListenerMap.set(root, keydownHandler);
}
};
/**
* Removes keyboard event listeners from the specified element.
* @param {Element} root - The DOM element to remove listeners from.
*/
const removeListeners = root => {
// In the future, let's use the abort controller to remove the listeners
// https://caniuse.com/abortcontroller
if (root instanceof Element) {
const handlerToRemove = keyboardListenerMap.get(root);
document.removeEventListener('keydown', handlerToRemove);
keyboardListenerMap.delete(root);
}
};
/**
* Vue composable to handle keyboard events with support for different keyboard layouts.
* @param {Object} keyboardEvents - The keyboard events to handle.
* @param {ref} elRef - A Vue ref to the element to attach the keyboard events to.
*/
export function useKeyboardEvents(keyboardEvents, elRef = null) {
export async function useKeyboardEvents(keyboardEvents) {
let abortController = new AbortController();
onMounted(async () => {
const el = unref(elRef);
const getKeyboardEvents = () => keyboardEvents || null;
const events = getKeyboardEvents();
const wrappedEvents = await wrapEventsInKeybindingsHandler(events);
setupListeners(el, wrappedEvents);
if (!keyboardEvents) return;
const wrappedEvents = await wrapEventsInKeybindingsHandler(keyboardEvents);
const keydownHandler = createKeybindingsHandler(wrappedEvents);
document.addEventListener('keydown', keydownHandler, {
signal: abortController.signal,
});
});
onBeforeUnmount(() => {
const el = unref(elRef);
removeListeners(el);
onUnmounted(() => {
abortController.abort();
});
}

View File

@@ -84,7 +84,6 @@ const updateSelectionIndex = (currentIndex, itemsLength, direction) => {
* }} An object containing functions to move the selection up and down.
*/
export function useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
@@ -109,7 +108,7 @@ export function useKeyboardNavigableList({
items
);
useKeyboardEvents(keyboardEvents, elementRef);
useKeyboardEvents(keyboardEvents);
return {
moveSelectionUp,

View File

@@ -5,7 +5,6 @@ import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vu
const emit = defineEmits(['add']);
const addNoteRef = ref(null);
const noteContent = ref('');
const buttonDisabled = computed(() => noteContent.value === '');
@@ -23,12 +22,11 @@ const keyboardEvents = {
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents, addNoteRef);
useKeyboardEvents(keyboardEvents);
</script>
<template>
<div
ref="addNoteRef"
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100"
>
<WootMessageEditor

View File

@@ -25,7 +25,6 @@ export default {
removeLabelFromConversation,
} = useConversationLabels();
const conversationLabelBoxRef = ref(null);
const showSearchDropdownLabel = ref(false);
const toggleLabels = () => {
@@ -52,7 +51,7 @@ export default {
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents, conversationLabelBoxRef);
useKeyboardEvents(keyboardEvents);
return {
isAdmin,
savedLabels,
@@ -60,7 +59,6 @@ export default {
accountLabels,
addLabelToConversation,
removeLabelFromConversation,
conversationLabelBoxRef,
showSearchDropdownLabel,
closeDropdownLabel,
toggleLabels,
@@ -81,7 +79,7 @@ export default {
</script>
<template>
<div ref="conversationLabelBoxRef" class="sidebar-labels-wrap">
<div class="sidebar-labels-wrap">
<div
v-if="!conversationUiFlags.isFetching"
class="contact-conversation--list"

View File

@@ -11,7 +11,6 @@ defineProps({
const emit = defineEmits(['search', 'close']);
const articleSearchHeaderRef = ref(null);
const searchInputRef = ref(null);
const searchQuery = ref('');
@@ -41,11 +40,11 @@ const keyboardEvents = {
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents, articleSearchHeaderRef);
useKeyboardEvents(keyboardEvents);
</script>
<template>
<div ref="articleSearchHeaderRef" class="flex flex-col py-1">
<div class="flex flex-col py-1">
<div class="flex items-center justify-between py-2 mb-1">
<h3 class="text-base text-slate-900 dark:text-slate-25">
{{ title }}

View File

@@ -44,7 +44,6 @@ export default {
};
useKeyboardNavigableList({
elementRef: portalSearchSuggestionsRef,
items: computed(() => props.items),
adjustScroll,
selectedIndex,

View File

@@ -52,7 +52,7 @@ const keyboardEvents = {
},
};
useKeyboardEvents(keyboardEvents, dropdownMenuRef);
useKeyboardEvents(keyboardEvents);
</script>
<template>