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

View file

@ -1,49 +1,160 @@
<template> <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> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import MentionBox from '../mentions/MentionBox.vue'; import mentionSelectionKeyboardMixin from '../mentions/mentionSelectionKeyboardMixin';
export default { export default {
components: { MentionBox }, mixins: [mentionSelectionKeyboardMixin],
props: { props: {
searchKey: { searchKey: {
type: String, type: String,
default: '', default: '',
}, },
}, },
data() {
return { selectedIndex: 0 };
},
computed: { computed: {
...mapGetters({ ...mapGetters({ agents: 'agents/getVerifiedAgents' }),
agents: 'agents/getVerifiedAgents',
}),
items() { items() {
if (!this.searchKey) { if (!this.searchKey) {
return this.agents.map(agent => ({ return this.agents;
label: agent.name,
key: agent.id,
description: agent.email,
}));
} }
return this.agents return this.agents.filter(agent =>
.filter(agent => agent.name
agent.name .toLocaleLowerCase()
.toLocaleLowerCase() .includes(this.searchKey.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: { methods: {
handleMentionClick(item = {}) { getTopSpacing() {
this.$emit('click', item); 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> </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> </template>
<script> <script>
import mentionSelectionKeyboardMixin from './mentionSelectionKeyboardMixin';
export default { export default {
mixins: [mentionSelectionKeyboardMixin],
props: { props: {
items: { items: {
type: Array, type: Array,
@ -39,56 +41,25 @@ export default {
} }
}, },
}, },
mounted() {
document.addEventListener('keydown', this.keyListener);
},
beforeDestroy() {
document.removeEventListener('keydown', this.keyListener);
},
methods: { methods: {
getTopPadding() { getTopPadding() {
if (this.items.length <= 4) { if (this.items.length <= 4) {
return -(this.items.length * 2.8 + 1.7); return -(this.items.length * 2.9 + 1.7);
} }
return -14; return -14;
}, },
isUp(e) { handleKeyboardEvent(e) {
return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P this.processKeyDownEvent(e);
}, this.$el.scrollTop = 29 * this.selectedIndex;
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;
}, },
onHover(index) { onHover(index) {
this.selectedIndex = index; this.selectedIndex = index;
}, },
onListItemSelection(index) { onListItemSelection(index) {
this.selectedIndex = index; this.selectedIndex = index;
this.onMentionSelect(); this.onSelect();
}, },
onMentionSelect() { onSelect() {
this.$emit('mention-select', this.items[this.selectedIndex]); 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();
});
});