feat: Adds keyboard shortcuts for conversation actions (#2672)
* feat: Adds keyboard shortcuts for conversation actions * Minor fixes * Minor fixes * Minor fixes and add new shortcut * MInor fixes * Review fixes * Minor fixes * Code cleanup * Minor fixes * Uses Alt or Option key instead of shift-key * Review fixes * Review fixes Co-authored-by: Pranav Raj S <pranav@chatwoot.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
parent
c7482696d4
commit
c523a953f7
15 changed files with 311 additions and 51 deletions
|
@ -19,7 +19,7 @@
|
||||||
{{ $t('CHAT_LIST.LIST.404') }}
|
{{ $t('CHAT_LIST.LIST.404') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="conversations-list">
|
<div ref="activeConversation" class="conversations-list">
|
||||||
<conversation-card
|
<conversation-card
|
||||||
v-for="chat in conversationList"
|
v-for="chat in conversationList"
|
||||||
:key="chat.id"
|
:key="chat.id"
|
||||||
|
@ -62,8 +62,13 @@ import ChatFilter from './widgets/conversation/ChatFilter';
|
||||||
import ChatTypeTabs from './widgets/ChatTypeTabs';
|
import ChatTypeTabs from './widgets/ChatTypeTabs';
|
||||||
import ConversationCard from './widgets/conversation/ConversationCard';
|
import ConversationCard from './widgets/conversation/ConversationCard';
|
||||||
import timeMixin from '../mixins/time';
|
import timeMixin from '../mixins/time';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import conversationMixin from '../mixins/conversations';
|
import conversationMixin from '../mixins/conversations';
|
||||||
import wootConstants from '../constants';
|
import wootConstants from '../constants';
|
||||||
|
import {
|
||||||
|
hasPressedAltAndJKey,
|
||||||
|
hasPressedAltAndKKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -71,7 +76,7 @@ export default {
|
||||||
ConversationCard,
|
ConversationCard,
|
||||||
ChatFilter,
|
ChatFilter,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, conversationMixin],
|
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
conversationInbox: {
|
conversationInbox: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
|
@ -94,6 +99,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
currentChat: 'getSelectedChat',
|
||||||
chatLists: 'getAllConversations',
|
chatLists: 'getAllConversations',
|
||||||
mineChatsList: 'getMineChats',
|
mineChatsList: 'getMineChats',
|
||||||
allChatList: 'getAllStatusChats',
|
allChatList: 'getAllStatusChats',
|
||||||
|
@ -188,6 +194,33 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
||||||
|
'div.conversations-list div.conversation'
|
||||||
|
);
|
||||||
|
const activeConversation = this.$refs.activeConversation.querySelector(
|
||||||
|
'div.conversations-list div.conversation.active'
|
||||||
|
);
|
||||||
|
const activeConversationIndex = [...allConversations].indexOf(
|
||||||
|
activeConversation
|
||||||
|
);
|
||||||
|
const lastConversationIndex = allConversations.length - 1;
|
||||||
|
if (hasPressedAltAndJKey(e)) {
|
||||||
|
if (activeConversationIndex === -1) {
|
||||||
|
allConversations[0].click();
|
||||||
|
}
|
||||||
|
if (activeConversationIndex >= 1) {
|
||||||
|
allConversations[activeConversationIndex - 1].click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndKKey(e)) {
|
||||||
|
if (activeConversationIndex === -1) {
|
||||||
|
allConversations[lastConversationIndex].click();
|
||||||
|
} else if (activeConversationIndex < lastConversationIndex) {
|
||||||
|
allConversations[activeConversationIndex + 1].click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
resetAndFetchData() {
|
resetAndFetchData() {
|
||||||
this.$store.dispatch('conversationPage/reset');
|
this.$store.dispatch('conversationPage/reset');
|
||||||
this.$store.dispatch('emptyAllConversations');
|
this.$store.dispatch('emptyAllConversations');
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="showAdditionalActions"
|
v-if="showAdditionalActions"
|
||||||
|
ref="arrowDownButton"
|
||||||
:color-scheme="buttonClass"
|
:color-scheme="buttonClass"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
icon="ion-arrow-down-b"
|
icon="ion-arrow-down-b"
|
||||||
|
@ -99,6 +100,12 @@
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
import {
|
||||||
|
hasPressedAltAndEKey,
|
||||||
|
hasPressedCommandPlusAltAndEKey,
|
||||||
|
hasPressedAltAndMKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
|
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
|
import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
|
||||||
|
@ -118,7 +125,7 @@ export default {
|
||||||
WootDropdownMenu,
|
WootDropdownMenu,
|
||||||
WootDropdownSubMenu,
|
WootDropdownSubMenu,
|
||||||
},
|
},
|
||||||
mixins: [clickaway, alertMixin],
|
mixins: [clickaway, alertMixin, eventListenerMixins],
|
||||||
props: { conversationId: { type: [String, Number], required: true } },
|
props: { conversationId: { type: [String, Number], required: true } },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -164,6 +171,33 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async handleKeyEvents(e) {
|
||||||
|
const allConversations = document.querySelectorAll(
|
||||||
|
'.conversations-list .conversation'
|
||||||
|
);
|
||||||
|
if (hasPressedAltAndEKey(e)) {
|
||||||
|
const activeConversation = document.querySelector(
|
||||||
|
'div.conversations-list div.conversation.active'
|
||||||
|
);
|
||||||
|
const activeConversationIndex = [...allConversations].indexOf(
|
||||||
|
activeConversation
|
||||||
|
);
|
||||||
|
const lastConversationIndex = allConversations.length - 1;
|
||||||
|
try {
|
||||||
|
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
||||||
|
} catch (error) {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
if (hasPressedCommandPlusAltAndEKey(e)) {
|
||||||
|
if (activeConversationIndex < lastConversationIndex) {
|
||||||
|
allConversations[activeConversationIndex + 1].click();
|
||||||
|
} else if (allConversations.length > 1) {
|
||||||
|
allConversations[0].click();
|
||||||
|
document.querySelector('.conversations-list').scrollTop = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
showOpenButton() {
|
showOpenButton() {
|
||||||
return this.isResolved || this.isSnoozed;
|
return this.isResolved || this.isSnoozed;
|
||||||
},
|
},
|
||||||
|
|
|
@ -59,10 +59,17 @@
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import router from '../../routes';
|
import router from '../../routes';
|
||||||
|
import {
|
||||||
|
hasPressedAltAndCKey,
|
||||||
|
hasPressedAltAndVKey,
|
||||||
|
hasPressedAltAndRKey,
|
||||||
|
hasPressedAltAndSKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
import adminMixin from '../../mixins/isAdmin';
|
import adminMixin from '../../mixins/isAdmin';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||||
export default {
|
export default {
|
||||||
mixins: [adminMixin],
|
mixins: [adminMixin, eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
menuItem: {
|
menuItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -117,6 +124,20 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndCKey(e)) {
|
||||||
|
router.push({ name: 'home' });
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndVKey(e)) {
|
||||||
|
router.push({ name: 'contacts_dashboard' });
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndRKey(e)) {
|
||||||
|
router.push({ name: 'settings_account_reports' });
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndSKey(e)) {
|
||||||
|
router.push({ name: 'settings_home' });
|
||||||
|
}
|
||||||
|
},
|
||||||
showItem(item) {
|
showItem(item) {
|
||||||
return this.isAdmin && item.newLink !== undefined;
|
return this.isAdmin && item.newLink !== undefined;
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,8 +10,11 @@
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import wootConstants from '../../constants';
|
import wootConstants from '../../constants';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
import { hasPressedAltAndNKey } from 'shared/helpers/KeyboardHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -28,6 +31,15 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndNKey(e)) {
|
||||||
|
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||||
|
this.onTabChange(0);
|
||||||
|
} else {
|
||||||
|
this.onTabChange(this.activeTabIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onTabChange(selectedTabIndex) {
|
onTabChange(selectedTabIndex) {
|
||||||
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
||||||
this.$emit('chatTabChange', this.items[selectedTabIndex].key);
|
this.$emit('chatTabChange', this.items[selectedTabIndex].key);
|
||||||
|
|
|
@ -78,11 +78,17 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FileUpload from 'vue-upload-component';
|
import FileUpload from 'vue-upload-component';
|
||||||
|
import {
|
||||||
|
hasPressedAltAndWKey,
|
||||||
|
hasPressedAltAndAKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
|
||||||
import { REPLY_EDITOR_MODES } from './constants';
|
import { REPLY_EDITOR_MODES } from './constants';
|
||||||
export default {
|
export default {
|
||||||
name: 'ReplyTopPanel',
|
name: 'ReplyTopPanel',
|
||||||
components: { FileUpload },
|
components: { FileUpload },
|
||||||
|
mixins: [eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -156,6 +162,14 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndWKey(e)) {
|
||||||
|
this.toggleFormatMode();
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndAKey(e)) {
|
||||||
|
this.$refs.upload.$children[1].$el.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
toggleFormatMode() {
|
toggleFormatMode() {
|
||||||
this.setFormatMode(!this.isFormatMode);
|
this.setFormatMode(!this.isFormatMode);
|
||||||
},
|
},
|
||||||
|
|
|
@ -32,11 +32,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
||||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
||||||
|
import {
|
||||||
|
hasPressedAltAndPKey,
|
||||||
|
hasPressedAltAndLKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
export default {
|
export default {
|
||||||
name: 'ReplyTopPanel',
|
name: 'ReplyTopPanel',
|
||||||
components: {
|
components: {
|
||||||
EmojiOrIcon,
|
EmojiOrIcon,
|
||||||
},
|
},
|
||||||
|
mixins: [eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -76,6 +82,14 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndPKey(e)) {
|
||||||
|
this.handleNoteClick();
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndLKey(e)) {
|
||||||
|
this.handleReplyClick();
|
||||||
|
}
|
||||||
|
},
|
||||||
handleReplyClick() {
|
handleReplyClick() {
|
||||||
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,12 +12,29 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import wootConstants from '../../../constants';
|
import wootConstants from '../../../constants';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
import { hasPressedAltAndBKey } from 'shared/helpers/KeyboardHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [eventListenerMixins],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
||||||
}),
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndBKey(e)) {
|
||||||
|
if (this.activeStatus === wootConstants.STATUS_TYPE.OPEN) {
|
||||||
|
this.activeStatus = wootConstants.STATUS_TYPE.RESOLVED;
|
||||||
|
} else if (this.activeStatus === wootConstants.STATUS_TYPE.RESOLVED) {
|
||||||
|
this.activeStatus = wootConstants.STATUS_TYPE.PENDING;
|
||||||
|
} else if (this.activeStatus === wootConstants.STATUS_TYPE.PENDING) {
|
||||||
|
this.activeStatus = wootConstants.STATUS_TYPE.SNOOZED;
|
||||||
|
} else if (this.activeStatus === wootConstants.STATUS_TYPE.SNOOZED) {
|
||||||
|
this.activeStatus = wootConstants.STATUS_TYPE.OPEN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.onTabChange();
|
||||||
|
},
|
||||||
onTabChange() {
|
onTabChange() {
|
||||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||||
this.$emit('statusFilterChange', this.activeStatus);
|
this.$emit('statusFilterChange', this.activeStatus);
|
||||||
|
|
|
@ -68,7 +68,9 @@ import { mapGetters } from 'vuex';
|
||||||
import MoreActions from './MoreActions';
|
import MoreActions from './MoreActions';
|
||||||
import Thumbnail from '../Thumbnail';
|
import Thumbnail from '../Thumbnail';
|
||||||
import agentMixin from '../../../mixins/agentMixin.js';
|
import agentMixin from '../../../mixins/agentMixin.js';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import AvailabilityStatusBadge from '../conversation/AvailabilityStatusBadge';
|
import AvailabilityStatusBadge from '../conversation/AvailabilityStatusBadge';
|
||||||
|
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -76,7 +78,7 @@ export default {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
AvailabilityStatusBadge,
|
AvailabilityStatusBadge,
|
||||||
},
|
},
|
||||||
mixins: [agentMixin],
|
mixins: [agentMixin, eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
chat: {
|
chat: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -117,6 +119,11 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndOKey(e)) {
|
||||||
|
this.$emit('contact-panel-toggle');
|
||||||
|
}
|
||||||
|
},
|
||||||
assignAgent(agent) {
|
assignAgent(agent) {
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('assignAgent', {
|
.dispatch('assignAgent', {
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
|
||||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
||||||
import CannedResponse from './CannedResponse';
|
import CannedResponse from './CannedResponse';
|
||||||
|
@ -105,7 +106,13 @@ export default {
|
||||||
ReplyBottomPanel,
|
ReplyBottomPanel,
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
},
|
},
|
||||||
mixins: [clickaway, inboxMixin, uiSettingsMixin, alertMixin],
|
mixins: [
|
||||||
|
clickaway,
|
||||||
|
inboxMixin,
|
||||||
|
uiSettingsMixin,
|
||||||
|
alertMixin,
|
||||||
|
eventListenerMixins,
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
selectedTweet: {
|
selectedTweet: {
|
||||||
type: [Object, String],
|
type: [Object, String],
|
||||||
|
@ -289,12 +296,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
document.addEventListener('keydown', this.handleKeyEvents);
|
|
||||||
},
|
|
||||||
destroyed() {
|
|
||||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserMention(currentMentionState) {
|
toggleUserMention(currentMentionState) {
|
||||||
this.hasUserMention = currentMentionState;
|
this.hasUserMention = currentMentionState;
|
||||||
|
@ -353,6 +354,9 @@ export default {
|
||||||
if (this.showRichContentEditor) {
|
if (this.showRichContentEditor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.$refs.messageInput === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$nextTick(() => this.$refs.messageInput.focus());
|
this.$nextTick(() => this.$refs.messageInput.focus());
|
||||||
},
|
},
|
||||||
emojiOnClick(emoji) {
|
emojiOnClick(emoji) {
|
||||||
|
|
|
@ -9,7 +9,11 @@
|
||||||
icon="ion-pricetags"
|
icon="ion-pricetags"
|
||||||
emoji="🏷️"
|
emoji="🏷️"
|
||||||
/>
|
/>
|
||||||
<div v-on-clickaway="closeDropdownLabel" class="label-wrap">
|
<div
|
||||||
|
v-on-clickaway="closeDropdownLabel"
|
||||||
|
class="label-wrap"
|
||||||
|
@keyup.esc="closeDropdownLabel"
|
||||||
|
>
|
||||||
<add-label @add="toggleLabels" />
|
<add-label @add="toggleLabels" />
|
||||||
<woot-label
|
<woot-label
|
||||||
v-for="label in activeLabels"
|
v-for="label in activeLabels"
|
||||||
|
|
|
@ -11,12 +11,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-scroll-container">
|
<div class="list-scroll-container">
|
||||||
<div
|
<div class="multiselect-dropdown--list">
|
||||||
ref="multiselectDropdown"
|
|
||||||
class="multiselect-dropdown--list"
|
|
||||||
@keyup.up="onArrowUp"
|
|
||||||
@keyup.down="onArrowDown"
|
|
||||||
>
|
|
||||||
<woot-dropdown-menu>
|
<woot-dropdown-menu>
|
||||||
<woot-dropdown-item
|
<woot-dropdown-item
|
||||||
v-for="option in filteredOptions"
|
v-for="option in filteredOptions"
|
||||||
|
@ -119,38 +114,6 @@ export default {
|
||||||
focusInput() {
|
focusInput() {
|
||||||
this.$refs.searchbar.focus();
|
this.$refs.searchbar.focus();
|
||||||
},
|
},
|
||||||
onArrowUp() {
|
|
||||||
const allDropdownItems = this.$refs.multiselectDropdown.querySelectorAll(
|
|
||||||
'.dropdown .multiselect-dropdown--item'
|
|
||||||
);
|
|
||||||
const focusedElement = this.$refs.multiselectDropdown.querySelector(
|
|
||||||
'.dropdown .multiselect-dropdown--item:focus'
|
|
||||||
);
|
|
||||||
const activeElementIndex = [...allDropdownItems].indexOf(focusedElement);
|
|
||||||
const lastElementIndex = allDropdownItems.length - 1;
|
|
||||||
|
|
||||||
if (activeElementIndex >= 1) {
|
|
||||||
allDropdownItems[activeElementIndex - 1].focus();
|
|
||||||
} else {
|
|
||||||
allDropdownItems[lastElementIndex].focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onArrowDown() {
|
|
||||||
const allDropdownItems = this.$refs.multiselectDropdown.querySelectorAll(
|
|
||||||
'.dropdown .multiselect-dropdown--item'
|
|
||||||
);
|
|
||||||
const focusedElement = this.$refs.multiselectDropdown.querySelector(
|
|
||||||
'.dropdown .multiselect-dropdown--item:focus'
|
|
||||||
);
|
|
||||||
const activeElementIndex = [...allDropdownItems].indexOf(focusedElement);
|
|
||||||
const lastElementIndex = allDropdownItems.length - 1;
|
|
||||||
|
|
||||||
if (activeElementIndex === lastElementIndex) {
|
|
||||||
allDropdownItems[0].focus();
|
|
||||||
} else {
|
|
||||||
allDropdownItems[activeElementIndex + 1].focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -42,6 +42,10 @@ export default {
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<ul
|
<ul
|
||||||
|
ref="dropdownMenu"
|
||||||
class="dropdown menu vertical"
|
class="dropdown menu vertical"
|
||||||
:class="[placement && `dropdown--${placement}`]"
|
:class="[placement && `dropdown--${placement}`]"
|
||||||
>
|
>
|
||||||
|
@ -7,15 +8,67 @@
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
import {
|
||||||
|
hasPressedArrowUpKey,
|
||||||
|
hasPressedArrowDownKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
export default {
|
export default {
|
||||||
name: 'WootDropdownMenu',
|
name: 'WootDropdownMenu',
|
||||||
componentName: 'WootDropdownMenu',
|
componentName: 'WootDropdownMenu',
|
||||||
|
|
||||||
|
mixins: [eventListenerMixins],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
placement: {
|
placement: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'top',
|
default: 'top',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.focusItem();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focusItem() {
|
||||||
|
this.$refs.dropdownMenu
|
||||||
|
.querySelector('ul.dropdown li.dropdown-menu__item .button')
|
||||||
|
.focus();
|
||||||
|
},
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedArrowUpKey(e)) {
|
||||||
|
const items = this.$refs.dropdownMenu.querySelectorAll(
|
||||||
|
'ul.dropdown li.dropdown-menu__item .button'
|
||||||
|
);
|
||||||
|
const focusItems = this.$refs.dropdownMenu.querySelector(
|
||||||
|
'ul.dropdown li.dropdown-menu__item .button:focus'
|
||||||
|
);
|
||||||
|
const activeElementIndex = [...items].indexOf(focusItems);
|
||||||
|
const lastElementIndex = items.length - 1;
|
||||||
|
|
||||||
|
if (activeElementIndex >= 1) {
|
||||||
|
items[activeElementIndex - 1].focus();
|
||||||
|
} else {
|
||||||
|
items[lastElementIndex].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasPressedArrowDownKey(e)) {
|
||||||
|
const items = this.$refs.dropdownMenu.querySelectorAll(
|
||||||
|
'li.dropdown-menu__item .button'
|
||||||
|
);
|
||||||
|
const focusItems = this.$refs.dropdownMenu.querySelector(
|
||||||
|
'li.dropdown-menu__item .button:focus'
|
||||||
|
);
|
||||||
|
const activeElementIndex = [...items].indexOf(focusItems);
|
||||||
|
const lastElementIndex = items.length - 1;
|
||||||
|
|
||||||
|
if (activeElementIndex === lastElementIndex) {
|
||||||
|
items[0].focus();
|
||||||
|
} else {
|
||||||
|
items[activeElementIndex + 1].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -9,3 +9,75 @@ export const isEscape = e => {
|
||||||
export const hasPressedShift = e => {
|
export const hasPressedShift = e => {
|
||||||
return e.shiftKey;
|
return e.shiftKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndCKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 67;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndVKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 86;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndRKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 82;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndSKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 83;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndBKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 66;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndNKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 78;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndWKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 87;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndAKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 65;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndPKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 80;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndLKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 76;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndEKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 69;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedCommandPlusAltAndEKey = e => {
|
||||||
|
return e.metaKey && e.altKey && e.keyCode === 69;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndOKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 79;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndJKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 74;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndKKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 75;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedAltAndMKey = e => {
|
||||||
|
return e.altKey && e.keyCode === 77;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedArrowUpKey = e => {
|
||||||
|
return e.keyCode === 38;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPressedArrowDownKey = e => {
|
||||||
|
return e.keyCode === 40;
|
||||||
|
};
|
||||||
|
|
8
app/javascript/shared/mixins/eventListenerMixins.js
Normal file
8
app/javascript/shared/mixins/eventListenerMixins.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default {
|
||||||
|
mounted() {
|
||||||
|
document.addEventListener('keydown', this.handleKeyEvents);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||||
|
},
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue