feat: Update the design of mentions with thumbnail (#5551)

This commit is contained in:
Pranav Raj S 2022-10-05 14:18:16 -07:00 committed by GitHub
parent 8b0e95ece8
commit cd4c1ef27e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 251 additions and 65 deletions

View file

@ -6,7 +6,7 @@
@click="insertMentionNode"
/>
<canned-response
v-if="showCannedMenu"
v-if="showCannedMenu && !isPrivate"
:search-key="cannedSearchTerm"
@click="insertCannedResponse"
/>
@ -223,8 +223,8 @@ export default {
return null;
}
const node = this.editorView.state.schema.nodes.mention.create({
userId: mentionItem.key,
userFullName: mentionItem.label,
userId: mentionItem.id,
userFullName: mentionItem.name,
});
const tr = this.editorView.state.tr.replaceWith(
@ -256,6 +256,7 @@ export default {
this.plugins
);
this.editorView.updateState(this.state);
this.focusEditorInputField();
return false;
},

View file

@ -1,49 +1,160 @@
<template>
<mention-box :items="items" @mention-select="handleMentionClick" />
<ul
v-if="items.length"
class="vertical dropdown menu mention--box"
:style="{ top: getTopSpacing() + 'rem' }"
: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 }"
@click="onAgentSelect(index)"
@mouseover="onHover(index)"
>
<div class="mention--thumbnail">
<woot-thumbnail
:src="agent.thumbnail"
:username="agent.name"
size="32px"
/>
</div>
<div class="mention--metadata text-truncate">
<h5 class="text-block-title mention--user-name text-truncate">
{{ agent.name }}
</h5>
<div class="text-truncate mention--email text-truncate">
{{ agent.email }}
</div>
</div>
</li>
</ul>
</template>
<script>
import { mapGetters } from 'vuex';
import MentionBox from '../mentions/MentionBox.vue';
import mentionSelectionKeyboardMixin from '../mentions/mentionSelectionKeyboardMixin';
export default {
components: { MentionBox },
mixins: [mentionSelectionKeyboardMixin],
props: {
searchKey: {
type: String,
default: '',
},
},
data() {
return { selectedIndex: 0 };
},
computed: {
...mapGetters({
agents: 'agents/getVerifiedAgents',
}),
...mapGetters({ agents: 'agents/getVerifiedAgents' }),
items() {
if (!this.searchKey) {
return this.agents.map(agent => ({
label: agent.name,
key: agent.id,
description: agent.email,
}));
return this.agents;
}
return this.agents
.filter(agent =>
return this.agents.filter(agent =>
agent.name
.toLocaleLowerCase()
.includes(this.searchKey.toLocaleLowerCase())
)
.map(agent => ({
label: agent.name,
key: agent.id,
description: agent.email,
}));
);
},
},
watch: {
items(newListOfAgents) {
if (newListOfAgents.length < this.selectedIndex + 1) {
this.selectedIndex = 0;
}
},
},
methods: {
handleMentionClick(item = {}) {
this.$emit('click', item);
getTopSpacing() {
if (this.items.length <= 4) {
return -(this.items.length * 5 + 1.7);
}
return -20;
},
handleKeyboardEvent(e) {
this.processKeyDownEvent(e);
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]);
},
},
};
</script>
<style scoped lang="scss">
.mention--box {
background: var(--white);
border-top: 1px solid var(--color-border);
font-size: var(--font-size-small);
left: 0;
line-height: 1.2;
max-height: 20rem;
overflow: auto;
padding: var(--space-small) var(--space-small) 0 var(--space-small);
position: absolute;
width: 100%;
z-index: 100;
&.with-bottom-border {
border-bottom: var(--space-small) solid var(--white);
li {
&:last-child {
margin-bottom: 0;
}
}
}
li {
align-items: center;
border-radius: var(--border-radius-normal);
display: flex;
padding: var(--space-small);
&.active {
background: var(--s-50);
.mention--user-name {
color: var(--s-900);
}
.mention--email {
color: var(--s-800);
}
}
&:last-child {
margin-bottom: var(--space-small);
}
}
}
.mention--thumbnail {
margin-right: var(--space-small);
}
.mention--user-name {
margin-bottom: 0;
}
.mention--email {
color: var(--s-700);
font-size: var(--font-size-mini);
}
.mention--metadata {
flex: 1;
max-width: 100%;
}
</style>

View file

@ -20,7 +20,9 @@
</template>
<script>
import mentionSelectionKeyboardMixin from './mentionSelectionKeyboardMixin';
export default {
mixins: [mentionSelectionKeyboardMixin],
props: {
items: {
type: Array,
@ -39,56 +41,25 @@ export default {
}
},
},
mounted() {
document.addEventListener('keydown', this.keyListener);
},
beforeDestroy() {
document.removeEventListener('keydown', this.keyListener);
},
methods: {
getTopPadding() {
if (this.items.length <= 4) {
return -(this.items.length * 2.8 + 1.7);
return -(this.items.length * 2.9 + 1.7);
}
return -14;
},
isUp(e) {
return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P
},
isDown(e) {
return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78); // DOWN, Ctrl-N
},
isEnter(e) {
return e.keyCode === 13;
},
keyListener(e) {
if (this.isUp(e)) {
if (!this.selectedIndex) {
this.selectedIndex = this.items.length - 1;
} else {
this.selectedIndex -= 1;
}
}
if (this.isDown(e)) {
if (this.selectedIndex === this.items.length - 1) {
this.selectedIndex = 0;
} else {
this.selectedIndex += 1;
}
}
if (this.isEnter(e)) {
this.onMentionSelect();
}
this.$el.scrollTop = 28 * this.selectedIndex;
handleKeyboardEvent(e) {
this.processKeyDownEvent(e);
this.$el.scrollTop = 29 * this.selectedIndex;
},
onHover(index) {
this.selectedIndex = index;
},
onListItemSelection(index) {
this.selectedIndex = index;
this.onMentionSelect();
this.onSelect();
},
onMentionSelect() {
onSelect() {
this.$emit('mention-select', this.items[this.selectedIndex]);
},
},

View file

@ -0,0 +1,39 @@
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
export default {
mounted() {
document.addEventListener('keydown', this.handleKeyboardEvent);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyboardEvent);
},
methods: {
moveSelectionUp() {
if (!this.selectedIndex) {
this.selectedIndex = this.items.length - 1;
} else {
this.selectedIndex -= 1;
}
},
moveSelectionDown() {
if (this.selectedIndex === this.items.length - 1) {
this.selectedIndex = 0;
} else {
this.selectedIndex += 1;
}
},
processKeyDownEvent(e) {
const keyPattern = buildHotKeys(e);
if (['arrowup', 'ctrl+p'].includes(keyPattern)) {
this.moveSelectionUp();
e.preventDefault();
} else if (['arrowdown', 'ctrl+n'].includes(keyPattern)) {
this.moveSelectionDown();
e.preventDefault();
} else if (keyPattern === 'enter') {
this.onSelect();
e.preventDefault();
}
},
},
};

View file

@ -0,0 +1,64 @@
import mentionSelectionKeyboardMixin from '../mentionSelectionKeyboardMixin';
import { shallowMount } from '@vue/test-utils';
const buildComponent = ({ data = {}, methods = {} }) => ({
render() {},
data() {
return data;
},
methods,
mixins: [mentionSelectionKeyboardMixin],
});
describe('mentionSelectionKeyboardMixin', () => {
test('register listeners', () => {
jest.spyOn(document, 'addEventListener');
const Component = buildComponent({});
shallowMount(Component);
// undefined expected as the method is not defined in the component
expect(document.addEventListener).toHaveBeenCalledWith(
'keydown',
undefined
);
});
test('processKeyDownEvent updates index on arrow up', () => {
const Component = buildComponent({
data: { selectedIndex: 0, items: [1, 2, 3] },
});
const wrapper = shallowMount(Component);
wrapper.vm.processKeyDownEvent({
ctrlKey: true,
key: 'p',
preventDefault: jest.fn(),
});
expect(wrapper.vm.selectedIndex).toBe(2);
});
test('processKeyDownEvent updates index on arrow down', () => {
const Component = buildComponent({
data: { selectedIndex: 0, items: [1, 2, 3] },
});
const wrapper = shallowMount(Component);
wrapper.vm.processKeyDownEvent({
key: 'ArrowDown',
preventDefault: jest.fn(),
});
expect(wrapper.vm.selectedIndex).toBe(1);
});
test('processKeyDownEvent calls select methods on Enter Key', () => {
const onSelectMockFn = jest.fn();
const Component = buildComponent({
data: { selectedIndex: 0, items: [1, 2, 3] },
methods: { onSelect: () => onSelectMockFn('enterKey pressed') },
});
const wrapper = shallowMount(Component);
wrapper.vm.processKeyDownEvent({
key: 'Enter',
preventDefault: jest.fn(),
});
expect(onSelectMockFn).toHaveBeenCalledWith('enterKey pressed');
wrapper.vm.onSelect();
});
});