feat: Replace the use of mentionSelectionKeyboard mixin to a composable (#9904)

This commit is contained in:
Sivin Varghese
2024-08-07 14:14:41 +05:30
committed by GitHub
parent c344f2b9cf
commit 56e93d152d
7 changed files with 606 additions and 317 deletions

View File

@@ -1,124 +1,119 @@
<script>
import { mapGetters } from 'vuex';
import mentionSelectionKeyboardMixin from '../mentions/mentionSelectionKeyboardMixin';
export default {
mixins: [mentionSelectionKeyboardMixin],
props: {
searchKey: {
type: String,
default: '',
},
},
data() {
return { selectedIndex: 0 };
},
computed: {
...mapGetters({ agents: 'agents/getVerifiedAgents' }),
items() {
if (!this.searchKey) {
return this.agents;
}
<script setup>
import { ref, computed, watch, nextTick } from 'vue';
import { useStoreGetters } from 'dashboard/composables/store';
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
return this.agents.filter(agent =>
agent.name
.toLocaleLowerCase()
.includes(this.searchKey.toLocaleLowerCase())
);
},
},
watch: {
items(newListOfAgents) {
if (newListOfAgents.length < this.selectedIndex + 1) {
this.selectedIndex = 0;
}
},
const props = defineProps({
searchKey: {
type: String,
default: '',
},
});
methods: {
adjustScroll() {
this.$nextTick(() => {
this.$el.scrollTop = 50 * this.selectedIndex;
});
},
onHover(index) {
this.selectedIndex = index;
},
onAgentSelect(index) {
this.selectedIndex = index;
this.onSelect();
},
onSelect() {
this.$emit('click', this.items[this.selectedIndex]);
},
},
const emit = defineEmits(['click']);
const getters = useStoreGetters();
const agents = computed(() => getters['agents/getVerifiedAgents'].value);
const tagAgentsRef = ref(null);
const selectedIndex = ref(0);
const items = computed(() => {
if (!props.searchKey) {
return agents.value;
}
return agents.value.filter(agent =>
agent.name.toLowerCase().includes(props.searchKey.toLowerCase())
);
});
const adjustScroll = () => {
nextTick(() => {
if (tagAgentsRef.value) {
tagAgentsRef.value.scrollTop = 50 * selectedIndex.value;
}
});
};
const onSelect = () => {
emit('click', items.value[selectedIndex.value]);
};
useKeyboardNavigableList({
elementRef: tagAgentsRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
watch(items, newListOfAgents => {
if (newListOfAgents.length < selectedIndex.value + 1) {
selectedIndex.value = 0;
}
});
const onHover = index => {
selectedIndex.value = index;
};
const onAgentSelect = index => {
selectedIndex.value = index;
onSelect();
};
</script>
<template>
<ul
v-if="items.length"
class="vertical dropdown menu mention--box"
:class="{ 'with-bottom-border': items.length <= 4 }"
>
<li
v-for="(agent, index) in items"
:id="`mention-item-${index}`"
:key="agent.id"
:class="{ active: index === selectedIndex }"
class="last:mb-2 items-center rounded-md flex p-2"
@click="onAgentSelect(index)"
@mouseover="onHover(index)"
<div>
<ul
v-if="items.length"
ref="tagAgentsRef"
class="vertical dropdown menu mention--box bg-white text-sm dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-20 pt-2 px-2 pb-0 shadow-md left-0 leading-[1.2] bottom-full max-h-[12.5rem] border-t border-solid border-slate-75 dark:border-slate-800"
:class="{
'border-b-[0.5rem] border-solid border-white dark:!border-slate-700':
items.length <= 4,
}"
>
<div class="mr-2">
<woot-thumbnail
:src="agent.thumbnail"
:username="agent.name"
size="32px"
/>
</div>
<div
class="flex-1 max-w-full overflow-hidden whitespace-nowrap text-ellipsis"
<li
v-for="(agent, index) in items"
:id="`mention-item-${index}`"
:key="agent.id"
:class="{
'bg-slate-50 dark:bg-slate-800': index === selectedIndex,
'last:mb-0': items.length <= 4,
}"
class="flex items-center p-2 rounded-md last:mb-2"
@click="onAgentSelect(index)"
@mouseover="onHover(index)"
>
<h5
class="mention--user-name mb-0 text-sm text-slate-900 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis"
>
{{ agent.name }}
</h5>
<div
class="mention--email overflow-hidden whitespace-nowrap text-ellipsis text-slate-700 dark:text-slate-300 text-xs"
>
{{ agent.email }}
<div class="mr-2">
<woot-thumbnail
:src="agent.thumbnail"
:username="agent.name"
size="32px"
/>
</div>
</div>
</li>
</ul>
<div
class="flex-1 max-w-full overflow-hidden whitespace-nowrap text-ellipsis"
>
<h5
class="mb-0 overflow-hidden text-sm text-slate-800 dark:text-slate-100 whitespace-nowrap text-ellipsis"
:class="{
'text-slate-900 dark:text-slate-100': index === selectedIndex,
}"
>
{{ agent.name }}
</h5>
<div
class="overflow-hidden text-xs whitespace-nowrap text-ellipsis text-slate-700 dark:text-slate-300"
:class="{
'text-slate-800 dark:text-slate-200': index === selectedIndex,
}"
>
{{ agent.email }}
</div>
</div>
</li>
</ul>
</div>
</template>
<style scoped lang="scss">
.mention--box {
@apply bg-white text-sm dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-20 pt-2 px-2 pb-0 shadow-md left-0 leading-[1.2] bottom-full max-h-[12.5rem] border-t border-solid border-slate-75 dark:border-slate-800;
&.with-bottom-border {
@apply border-b-[0.5rem] border-solid border-white dark:border-slate-600;
li {
&:last-child {
@apply mb-0;
}
}
}
li {
&.active {
@apply bg-slate-50 dark:bg-slate-800;
.mention--user-name {
@apply text-slate-900 dark:text-slate-100;
}
.mention--email {
@apply text-slate-800 dark:text-slate-200;
}
}
}
}
</style>

View File

@@ -1,71 +1,82 @@
<script>
import mentionSelectionKeyboardMixin from './mentionSelectionKeyboardMixin';
export default {
mixins: [mentionSelectionKeyboardMixin],
props: {
items: {
type: Array,
default: () => {},
},
type: {
type: String,
default: 'canned',
},
<script setup>
import { ref, watch, computed } from 'vue';
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
const props = defineProps({
items: {
type: Array,
default: () => [],
},
data() {
return {
selectedIndex: 0,
};
},
watch: {
items(newItems) {
if (newItems.length < this.selectedIndex + 1) {
this.selectedIndex = 0;
}
},
selectedIndex() {
const container = this.$refs.mentionsListContainer;
const item = container.querySelector(
`#mention-item-${this.selectedIndex}`
);
if (item) {
const itemTop = item.offsetTop;
const itemBottom = itemTop + item.offsetHeight;
const containerTop = container.scrollTop;
const containerBottom = containerTop + container.offsetHeight;
if (itemTop < containerTop) {
container.scrollTop = itemTop;
} else if (itemBottom + 34 > containerBottom) {
container.scrollTop = itemBottom - container.offsetHeight + 34;
}
}
},
},
methods: {
adjustScroll() {},
onHover(index) {
this.selectedIndex = index;
},
onListItemSelection(index) {
this.selectedIndex = index;
this.onSelect();
},
onSelect() {
this.$emit('mentionSelect', this.items[this.selectedIndex]);
},
variableKey(item = {}) {
return this.type === 'variable' ? `{{${item.label}}}` : `/${item.label}`;
},
type: {
type: String,
default: 'canned',
},
});
const emit = defineEmits(['mentionSelect']);
const mentionsListContainerRef = ref(null);
const selectedIndex = ref(0);
const adjustScroll = () => {
const container = mentionsListContainerRef.value;
const item = container.querySelector(`#mention-item-${selectedIndex.value}`);
if (item) {
const itemTop = item.offsetTop;
const itemBottom = itemTop + item.offsetHeight;
const containerTop = container.scrollTop;
const containerBottom = containerTop + container.offsetHeight;
if (itemTop < containerTop) {
container.scrollTop = itemTop;
} else if (itemBottom + 34 > containerBottom) {
container.scrollTop = itemBottom - container.offsetHeight + 34;
}
}
};
const onSelect = () => {
emit('mentionSelect', props.items[selectedIndex.value]);
};
useKeyboardNavigableList({
elementRef: mentionsListContainerRef,
items: computed(() => props.items),
onSelect,
adjustScroll,
selectedIndex,
});
watch(
() => props.items,
newItems => {
if (newItems.length < selectedIndex.value + 1) {
selectedIndex.value = 0;
}
}
);
watch(selectedIndex, adjustScroll);
const onHover = index => {
selectedIndex.value = index;
};
const onListItemSelection = index => {
selectedIndex.value = index;
onSelect();
};
const variableKey = (item = {}) => {
return props.type === 'variable' ? `{{${item.label}}}` : `/${item.label}`;
};
</script>
<template>
<div
ref="mentionsListContainer"
ref="mentionsListContainerRef"
class="bg-white dark:bg-slate-800 rounded-md overflow-auto absolute w-full z-20 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border border-solid border-slate-100 dark:border-slate-700 mention--box"
>
<ul class="vertical dropdown menu">
<ul class="mb-0 vertical dropdown menu">
<woot-dropdown-item
v-for="(item, index) in items"
:id="`mention-item-${index}`"

View File

@@ -1,62 +0,0 @@
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
export default {
mixins: [keyboardEventListenerMixins],
methods: {
moveSelectionUp() {
if (!this.selectedIndex) {
this.selectedIndex = this.items.length - 1;
} else {
this.selectedIndex -= 1;
}
this.adjustScroll();
},
moveSelectionDown() {
if (this.selectedIndex === this.items.length - 1) {
this.selectedIndex = 0;
} else {
this.selectedIndex += 1;
}
this.adjustScroll();
},
getKeyboardEvents() {
return {
ArrowUp: {
action: e => {
this.moveSelectionUp();
e.preventDefault();
},
allowOnFocusedInput: true,
},
'Control+KeyP': {
action: e => {
this.moveSelectionUp();
e.preventDefault();
},
allowOnFocusedInput: true,
},
ArrowDown: {
action: e => {
this.moveSelectionDown();
e.preventDefault();
},
allowOnFocusedInput: true,
},
'Control+KeyN': {
action: e => {
this.moveSelectionDown();
e.preventDefault();
},
allowOnFocusedInput: true,
},
Enter: {
action: e => {
this.onSelect();
e.preventDefault();
},
allowOnFocusedInput: true,
},
};
},
},
};

View File

@@ -1,69 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
const localVue = createLocalVue();
const buildComponent = ({ data = {}, methods = {} }) => ({
render() {},
data() {
return { ...data, selectedIndex: 0, items: [1, 2, 3] };
},
methods: { ...methods, onSelect: vi.fn(), adjustScroll: vi.fn() },
mixins: [keyboardEventListenerMixins],
});
describe('mentionSelectionKeyboardMixin', () => {
let wrapper;
beforeEach(() => {
const Component = buildComponent({});
wrapper = shallowMount(Component, { localVue });
});
it('ArrowUp and Control+KeyP update selectedIndex correctly', () => {
const preventDefault = vi.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.ArrowUp) {
keyboardEvents.ArrowUp.action({ preventDefault });
expect(wrapper.vm.selectedIndex).toBe(2);
expect(preventDefault).toHaveBeenCalled();
}
wrapper.setData({ selectedIndex: 1 });
if (keyboardEvents && keyboardEvents['Control+KeyP']) {
keyboardEvents['Control+KeyP'].action({ preventDefault });
expect(wrapper.vm.selectedIndex).toBe(0);
expect(preventDefault).toHaveBeenCalledTimes(2);
}
});
it('ArrowDown and Control+KeyN update selectedIndex correctly', () => {
const preventDefault = vi.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.ArrowDown) {
keyboardEvents.ArrowDown.action({ preventDefault });
expect(wrapper.vm.selectedIndex).toBe(1);
expect(preventDefault).toHaveBeenCalled();
}
wrapper.setData({ selectedIndex: 1 });
if (keyboardEvents && keyboardEvents['Control+KeyN']) {
keyboardEvents['Control+KeyN'].action({ preventDefault });
expect(wrapper.vm.selectedIndex).toBe(2);
expect(preventDefault).toHaveBeenCalledTimes(2);
}
});
it('Enter key triggers onSelect method', () => {
const preventDefault = vi.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.Enter) {
keyboardEvents.Enter.action({ preventDefault });
expect(wrapper.vm.onSelect).toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalled();
}
});
});

View File

@@ -0,0 +1,280 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ref } from 'vue';
import { useKeyboardNavigableList } from '../useKeyboardNavigableList';
import { useKeyboardEvents } from '../useKeyboardEvents';
// Mock the useKeyboardEvents function
vi.mock('../useKeyboardEvents', () => ({
useKeyboardEvents: vi.fn(),
}));
describe('useKeyboardNavigableList', () => {
let elementRef;
let items;
let onSelect;
let adjustScroll;
let selectedIndex;
const createMockEvent = () => ({ preventDefault: vi.fn() });
beforeEach(() => {
elementRef = ref(null);
items = ref(['item1', 'item2', 'item3']);
onSelect = vi.fn();
adjustScroll = vi.fn();
selectedIndex = ref(0);
vi.clearAllMocks();
});
it('should return moveSelectionUp and moveSelectionDown functions', () => {
const result = useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
expect(result).toHaveProperty('moveSelectionUp');
expect(result).toHaveProperty('moveSelectionDown');
expect(typeof result.moveSelectionUp).toBe('function');
expect(typeof result.moveSelectionDown).toBe('function');
});
it('should move selection up correctly', () => {
const { moveSelectionUp } = useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
moveSelectionUp();
expect(selectedIndex.value).toBe(2);
moveSelectionUp();
expect(selectedIndex.value).toBe(1);
moveSelectionUp();
expect(selectedIndex.value).toBe(0);
moveSelectionUp();
expect(selectedIndex.value).toBe(2);
});
it('should move selection down correctly', () => {
const { moveSelectionDown } = useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
moveSelectionDown();
expect(selectedIndex.value).toBe(1);
moveSelectionDown();
expect(selectedIndex.value).toBe(2);
moveSelectionDown();
expect(selectedIndex.value).toBe(0);
moveSelectionDown();
expect(selectedIndex.value).toBe(1);
});
it('should call adjustScroll after moving selection', () => {
const { moveSelectionUp, moveSelectionDown } = useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
moveSelectionUp();
expect(adjustScroll).toHaveBeenCalledTimes(1);
moveSelectionDown();
expect(adjustScroll).toHaveBeenCalledTimes(2);
});
it('should include Enter key handler when onSelect is provided', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
const keyboardEvents = useKeyboardEvents.mock.calls[0][0];
expect(keyboardEvents).toHaveProperty('Enter');
expect(keyboardEvents.Enter.allowOnFocusedInput).toBe(true);
});
it('should not include Enter key handler when onSelect is not provided', () => {
useKeyboardNavigableList({
elementRef,
items,
adjustScroll,
selectedIndex,
});
const keyboardEvents = useKeyboardEvents.mock.calls[0][0];
expect(keyboardEvents).not.toHaveProperty('Enter');
});
it('should not trigger onSelect when items are empty', () => {
const { moveSelectionUp, moveSelectionDown } = useKeyboardNavigableList({
elementRef,
items: ref([]),
onSelect,
adjustScroll,
selectedIndex,
});
moveSelectionUp();
moveSelectionDown();
expect(onSelect).not.toHaveBeenCalled();
});
it('should call useKeyboardEvents with correct parameters', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
expect(useKeyboardEvents).toHaveBeenCalledWith(
expect.any(Object),
elementRef
);
});
// Keyboard event handlers
it('should handle ArrowUp key', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
const keyboardEvents = useKeyboardEvents.mock.calls[0][0];
const mockEvent = createMockEvent();
keyboardEvents.ArrowUp.action(mockEvent);
expect(selectedIndex.value).toBe(2);
expect(adjustScroll).toHaveBeenCalled();
expect(mockEvent.preventDefault).toHaveBeenCalled();
});
it('should handle Control+KeyP', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
const keyboardEvents = useKeyboardEvents.mock.calls[0][0];
const mockEvent = createMockEvent();
keyboardEvents['Control+KeyP'].action(mockEvent);
expect(selectedIndex.value).toBe(2);
expect(adjustScroll).toHaveBeenCalled();
expect(mockEvent.preventDefault).toHaveBeenCalled();
});
it('should handle ArrowDown key', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
const keyboardEvents = useKeyboardEvents.mock.calls[0][0];
const mockEvent = createMockEvent();
keyboardEvents.ArrowDown.action(mockEvent);
expect(selectedIndex.value).toBe(1);
expect(adjustScroll).toHaveBeenCalled();
expect(mockEvent.preventDefault).toHaveBeenCalled();
});
it('should handle Control+KeyN', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
const keyboardEvents = useKeyboardEvents.mock.calls[0][0];
const mockEvent = createMockEvent();
keyboardEvents['Control+KeyN'].action(mockEvent);
expect(selectedIndex.value).toBe(1);
expect(adjustScroll).toHaveBeenCalled();
expect(mockEvent.preventDefault).toHaveBeenCalled();
});
it('should handle Enter key when onSelect is provided', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
const keyboardEvents = useKeyboardEvents.mock.calls[0][0];
const mockEvent = createMockEvent();
keyboardEvents.Enter.action(mockEvent);
expect(onSelect).toHaveBeenCalled();
expect(mockEvent.preventDefault).toHaveBeenCalled();
});
it('should not have Enter key handler when onSelect is not provided', () => {
useKeyboardNavigableList({
elementRef,
items,
adjustScroll,
selectedIndex,
});
const keyboardEventsWithoutSelect = useKeyboardEvents.mock.calls[0][0];
expect(keyboardEventsWithoutSelect).not.toHaveProperty('Enter');
});
it('should set allowOnFocusedInput to true for all key handlers', () => {
useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
});
const keyboardEvents = useKeyboardEvents.mock.calls[0][0];
const keyHandlers = [
'ArrowUp',
'Control+KeyP',
'ArrowDown',
'Control+KeyN',
'Enter',
];
keyHandlers.forEach(key => {
if (keyboardEvents[key]) {
expect(keyboardEvents[key].allowOnFocusedInput).toBe(true);
}
});
});
});

View File

@@ -0,0 +1,118 @@
/**
* This composable provides keyboard navigation functionality for list-like UI components
* such as dropdowns, autocomplete suggestions, or any list of selectable items.
*
* TODO - Things that can be improved in the future
* - The scrolling should be handled by the component instead of the consumer of this composable
* it can be done if we know the item height.
* - The focus should be trapped within the list.
* - onSelect should be callback instead of a function that is passed
*/
import { useKeyboardEvents } from './useKeyboardEvents';
/**
* Wrap the action in a function that calls the action and prevents the default event behavior.
* @param {Function} action - The action to be called.
* @returns {{action: Function, allowOnFocusedInput: boolean}} An object containing the action and a flag to allow the event on focused input.
*/
const createAction = action => ({
action: e => {
action();
e.preventDefault();
},
allowOnFocusedInput: true,
});
/**
* Creates keyboard event handlers for navigation.
* @param {Function} moveSelectionUp - Function to move selection up.
* @param {Function} moveSelectionDown - Function to move selection down.
* @param {Function} [onSelect] - Optional function to handle selection.
* @param {import('vue').Ref<Array>} items - A ref to the array of selectable items.
* @returns {Object.<string, {action: Function, allowOnFocusedInput: boolean}>}
*/
const createKeyboardEvents = (
moveSelectionUp,
moveSelectionDown,
onSelect,
items
) => {
const events = {
ArrowUp: createAction(moveSelectionUp),
'Control+KeyP': createAction(moveSelectionUp),
ArrowDown: createAction(moveSelectionDown),
'Control+KeyN': createAction(moveSelectionDown),
};
// Adds an event handler for the Enter key if the onSelect function is provided.
if (typeof onSelect === 'function') {
events.Enter = createAction(() => items.value?.length > 0 && onSelect());
}
return events;
};
/**
* Updates the selection index based on the current index, total number of items, and direction of movement.
*
* @param {number} currentIndex - The current index of the selected item.
* @param {number} itemsLength - The total number of items in the list.
* @param {string} direction - The direction of movement, either 'up' or 'down'.
* @returns {number} The new index after moving in the specified direction.
*/
const updateSelectionIndex = (currentIndex, itemsLength, direction) => {
// If the selected index is the first item, move to the last item
// If the selected index is the last item, move to the first item
if (direction === 'up') {
return currentIndex === 0 ? itemsLength - 1 : currentIndex - 1;
}
return currentIndex === itemsLength - 1 ? 0 : currentIndex + 1;
};
/**
* A composable for handling keyboard navigation in mention selection scenarios.
*
* @param {Object} options - The options for the composable.
* @param {import('vue').Ref<HTMLElement>} options.elementRef - A ref to the DOM element that will receive keyboard events.
* @param {import('vue').Ref<Array>} options.items - A ref to the array of selectable items.
* @param {Function} [options.onSelect] - An optional function to be called when an item is selected.
* @param {Function} options.adjustScroll - A function to adjust the scroll position after selection changes.
* @param {import('vue').Ref<number>} options.selectedIndex - A ref to the currently selected index.
* @returns {{
* moveSelectionUp: Function,
* moveSelectionDown: Function
* }} An object containing functions to move the selection up and down.
*/
export function useKeyboardNavigableList({
elementRef,
items,
onSelect,
adjustScroll,
selectedIndex,
}) {
const moveSelection = direction => {
selectedIndex.value = updateSelectionIndex(
selectedIndex.value,
items.value.length,
direction
);
adjustScroll();
};
const moveSelectionUp = () => moveSelection('up');
const moveSelectionDown = () => moveSelection('down');
const keyboardEvents = createKeyboardEvents(
moveSelectionUp,
moveSelectionDown,
onSelect,
items
);
useKeyboardEvents(keyboardEvents, elementRef);
return {
moveSelectionUp,
moveSelectionDown,
};
}

View File

@@ -1,9 +1,10 @@
<script>
import mentionSelectionKeyboardMixin from 'dashboard/components/widgets/mentions/mentionSelectionKeyboardMixin.js';
import { ref, computed, nextTick } from 'vue';
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
export default {
mixins: [mentionSelectionKeyboardMixin, messageFormatterMixin],
mixins: [messageFormatterMixin],
props: {
items: {
type: Array,
@@ -26,9 +27,33 @@ export default {
default: '',
},
},
data() {
setup(props) {
const selectedIndex = ref(-1);
const portalSearchSuggestionsRef = ref(null);
const adjustScroll = () => {
nextTick(() => {
portalSearchSuggestionsRef.value.scrollTop = 102 * selectedIndex.value;
});
};
const isSearchItemActive = index => {
return index === selectedIndex.value
? 'bg-slate-25 dark:bg-slate-800'
: 'bg-white dark:bg-slate-900';
};
useKeyboardNavigableList({
elementRef: portalSearchSuggestionsRef,
items: computed(() => props.items),
adjustScroll,
selectedIndex,
});
return {
selectedIndex: -1,
selectedIndex,
portalSearchSuggestionsRef,
isSearchItemActive,
};
},
@@ -42,19 +67,9 @@ export default {
},
methods: {
isSearchItemActive(index) {
return index === this.selectedIndex
? 'bg-slate-25 dark:bg-slate-800'
: 'bg-white dark:bg-slate-900';
},
generateArticleUrl(article) {
return `/hc/${article.portal.slug}/articles/${article.slug}`;
},
adjustScroll() {
this.$nextTick(() => {
this.$el.scrollTop = 102 * this.selectedIndex;
});
},
prepareContent(content) {
return this.highlightContent(
content,
@@ -68,6 +83,7 @@ export default {
<template>
<div
ref="portalSearchSuggestionsRef"
class="p-5 mt-2 overflow-y-auto text-sm bg-white border border-solid rounded-lg shadow-xl hover:shadow-lg dark:bg-slate-900 max-h-96 scroll-py-2 text-slate-700 dark:text-slate-100 border-slate-50 dark:border-slate-800"
>
<div