feat: Fixes #1940 WCAG support for website widget (#2071)

Co-authored-by: Kaj Oudshoorn <kaj@milvum.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
This commit is contained in:
koudshoorn 2021-09-02 08:43:53 +02:00 committed by GitHub
parent 2ddd508aee
commit af1d8c0ee5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 109 additions and 23 deletions

View file

@ -43,7 +43,7 @@ $woot-logo-padding: $space-large $space-two;
// Colors // Colors
$color-woot: #1f93ff; $color-woot: #1f93ff;
$color-gray: #6e6f73; $color-gray: #6e6f73;
$color-light-gray: #999a9b; $color-light-gray: #747677;
$color-border: #e0e6ed; $color-border: #e0e6ed;
$color-border-light: #f0f4f5; $color-border-light: #f0f4f5;
$color-background: #f4f6fb; $color-background: #f4f6fb;

View file

@ -142,6 +142,7 @@ export const IFrameHelper = {
}, },
onBubbleToggle: isOpen => { onBubbleToggle: isOpen => {
IFrameHelper.sendMessage('toggle-open', { isOpen });
if (!isOpen) { if (!isOpen) {
IFrameHelper.events.resetUnreadMode(); IFrameHelper.events.resetUnreadMode();
} else { } else {
@ -194,6 +195,10 @@ export const IFrameHelper = {
const holderEl = document.querySelector('.woot-widget-holder'); const holderEl = document.querySelector('.woot-widget-holder');
removeClass(holderEl, 'has-unread-view'); removeClass(holderEl, 'has-unread-view');
}, },
closeChat: () => {
onBubbleClick({ toggleValue: false });
},
}, },
pushEvent: eventName => { pushEvent: eventName => {
IFrameHelper.sendMessage('push-event', { eventName }); IFrameHelper.sendMessage('push-event', { eventName });

View file

@ -9,8 +9,8 @@ export const body = document.getElementsByTagName('body')[0];
export const widgetHolder = document.createElement('div'); export const widgetHolder = document.createElement('div');
export const bubbleHolder = document.createElement('div'); export const bubbleHolder = document.createElement('div');
export const chatBubble = document.createElement('div'); export const chatBubble = document.createElement('button');
export const closeBubble = document.createElement('div'); export const closeBubble = document.createElement('button');
export const notificationBubble = document.createElement('span'); export const notificationBubble = document.createElement('span');
export const getBubbleView = type => export const getBubbleView = type =>
@ -64,6 +64,10 @@ export const onBubbleClick = (props = {}) => {
toggleClass(closeBubble, 'woot--hide'); toggleClass(closeBubble, 'woot--hide');
toggleClass(widgetHolder, 'woot--hide'); toggleClass(widgetHolder, 'woot--hide');
IFrameHelper.events.onBubbleToggle(newIsOpen); IFrameHelper.events.onBubbleToggle(newIsOpen);
if (!newIsOpen) {
chatBubble.focus();
}
} }
}; };

View file

@ -25,7 +25,9 @@ export const SDK_CSS = `.woot-widget-holder {
.woot-widget-bubble { .woot-widget-bubble {
background: #1f93ff; background: #1f93ff;
border-radius: 100px !important; border-radius: 100px !important;
border-width: 0px;
bottom: 20px; bottom: 20px;
padding: 0px;
box-shadow: 0 8px 24px rgba(0, 0, 0, .16) !important; box-shadow: 0 8px 24px rgba(0, 0, 0, .16) !important;
cursor: pointer; cursor: pointer;
height: 64px !important; height: 64px !important;
@ -40,6 +42,7 @@ export const SDK_CSS = `.woot-widget-holder {
display: flex; display: flex;
height: 48px !important; height: 48px !important;
width: auto !important; width: auto !important;
align-items: center;
} }
.woot-widget-bubble.woot-widget--expanded div { .woot-widget-bubble.woot-widget--expanded div {

View file

@ -12,7 +12,7 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions, mapMutations } from 'vuex';
import { setHeader } from 'widget/helpers/axios'; import { setHeader } from 'widget/helpers/axios';
import { IFrameHelper, RNHelper } from 'widget/helpers/utils'; import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import Router from './views/Router'; import Router from './views/Router';
@ -97,6 +97,7 @@ export default {
...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']), ...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']),
...mapActions('campaign', ['initCampaigns', 'executeCampaign']), ...mapActions('campaign', ['initCampaigns', 'executeCampaign']),
...mapActions('agent', ['fetchAvailableAgents']), ...mapActions('agent', ['fetchAvailableAgents']),
...mapMutations('events', ['toggleOpen']),
scrollConversationToBottom() { scrollConversationToBottom() {
const container = this.$el.querySelector('.conversation-wrap'); const container = this.$el.querySelector('.conversation-wrap');
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
@ -249,6 +250,8 @@ export default {
} else if (message.event === 'unset-unread-view') { } else if (message.event === 'unset-unread-view') {
this.showUnreadView = false; this.showUnreadView = false;
this.showCampaignView = false; this.showCampaignView = false;
} else if (message.event === 'toggle-open') {
this.toggleOpen();
} }
}); });
}, },

View file

@ -74,3 +74,8 @@ $color-shadow-outline: rgba(66, 153, 225, 0.5);
@mixin shadow-none { @mixin shadow-none {
box-shadow: none; box-shadow: none;
} }
@mixin button-size {
min-height: $space-large;
min-width: $space-large;
}

View file

@ -68,13 +68,19 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~widget/assets/scss/variables.scss'; @import '~widget/assets/scss/variables.scss';
@import '~widget/assets/scss/mixins.scss';
.attachment-button { .attachment-button {
@include button-size;
background: transparent; background: transparent;
border: 0; border: 0;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
width: 20px; width: 20px;
display: flex;
align-items: center;
justify-content: center;
i { i {
font-size: $font-size-large; font-size: $font-size-large;

View file

@ -124,7 +124,6 @@ export default {
.footer { .footer {
background: $color-white; background: $color-white;
box-sizing: border-box; box-sizing: border-box;
padding: $space-small $space-slab;
width: 100%; width: 100%;
border-radius: 7px; border-radius: 7px;
@include shadow-big; @include shadow-big;

View file

@ -1,27 +1,42 @@
<template> <template>
<div class="chat-message--input"> <div
class="chat-message--input"
:class="{ 'is-focused': isFocused }"
@keydown.esc="hideEmojiPicker"
>
<resizable-text-area <resizable-text-area
id="chat-input"
ref="chatInput"
v-model="userInput" v-model="userInput"
:aria-label="$t('CHAT_PLACEHOLDER')"
:placeholder="$t('CHAT_PLACEHOLDER')" :placeholder="$t('CHAT_PLACEHOLDER')"
class="form-input user-message-input" class="form-input user-message-input"
@typing-off="onTypingOff" @typing-off="onTypingOff"
@typing-on="onTypingOn" @typing-on="onTypingOn"
@focus="onFocus"
@blur="onBlur"
/> />
<div class="button-wrap"> <div class="button-wrap">
<chat-attachment-button <chat-attachment-button
v-if="showAttachment" v-if="showAttachment"
:on-attach="onSendAttachment" :on-attach="onSendAttachment"
/> />
<button
v-if="hasEmojiPickerEnabled"
class="emoji-toggle"
aria-label="Emoji picker"
@click="toggleEmojiPicker()"
>
<i
class="icon ion-happy-outline"
:class="{ active: showEmojiPicker }"
/>
</button>
<emoji-input <emoji-input
v-if="showEmojiPicker" v-if="showEmojiPicker"
v-on-clickaway="hideEmojiPicker" v-on-clickaway="hideEmojiPicker"
:on-click="emojiOnClick" :on-click="emojiOnClick"
/> @keydown.esc="hideEmojiPicker"
<i
v-if="hasEmojiPickerEnabled"
class="emoji-toggle icon ion-happy-outline"
:class="{ active: showEmojiPicker }"
@click="toggleEmojiPicker()"
/> />
<chat-send-button <chat-send-button
v-if="showSendButton" v-if="showSendButton"
@ -65,6 +80,7 @@ export default {
return { return {
userInput: '', userInput: '',
showEmojiPicker: false, showEmojiPicker: false,
isFocused: false,
}; };
}, },
@ -78,21 +94,40 @@ export default {
showSendButton() { showSendButton() {
return this.userInput.length > 0; return this.userInput.length > 0;
}, },
isOpen() {
return this.$store.state.events.isOpen;
},
},
watch: {
isOpen(isOpen) {
if (isOpen) {
this.focusInput();
}
},
}, },
destroyed() { destroyed() {
document.removeEventListener('keypress', this.handleEnterKeyPress); document.removeEventListener('keypress', this.handleEnterKeyPress);
}, },
mounted() { mounted() {
document.addEventListener('keypress', this.handleEnterKeyPress); document.addEventListener('keypress', this.handleEnterKeyPress);
if (this.isOpen) {
this.focusInput();
}
}, },
methods: { methods: {
onBlur() {
this.isFocused = false;
},
onFocus() {
this.isFocused = true;
},
handleButtonClick() { handleButtonClick() {
if (this.userInput && this.userInput.trim()) { if (this.userInput && this.userInput.trim()) {
this.onSendMessage(this.userInput); this.onSendMessage(this.userInput);
} }
this.userInput = ''; this.userInput = '';
this.focusInput();
}, },
handleEnterKeyPress(e) { handleEnterKeyPress(e) {
if (e.keyCode === 13 && !e.shiftKey) { if (e.keyCode === 13 && !e.shiftKey) {
@ -103,8 +138,9 @@ export default {
toggleEmojiPicker() { toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker; this.showEmojiPicker = !this.showEmojiPicker;
}, },
hideEmojiPicker() { hideEmojiPicker(e) {
if (this.showEmojiPicker) { if (this.showEmojiPicker) {
e.stopPropagation();
this.toggleEmojiPicker(); this.toggleEmojiPicker();
} }
}, },
@ -120,22 +156,33 @@ export default {
toggleTyping(typingStatus) { toggleTyping(typingStatus) {
this.$store.dispatch('conversation/toggleUserTyping', { typingStatus }); this.$store.dispatch('conversation/toggleUserTyping', { typingStatus });
}, },
focusInput() {
this.$refs.chatInput.focus();
},
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~widget/assets/scss/variables.scss'; @import '~widget/assets/scss/variables.scss';
@import '~widget/assets/scss/mixins.scss';
.chat-message--input { .chat-message--input {
align-items: center; align-items: center;
display: flex; display: flex;
padding: 0 $space-small 0 $space-slab;
border-radius: 7px;
&.is-focused {
box-shadow: 0 0 0 1px $color-woot, 0 0 2px 3px $color-primary-light;
}
} }
.emoji-toggle { .emoji-toggle {
@include button-size;
font-size: $font-size-large; font-size: $font-size-large;
color: $color-gray; color: $color-gray;
padding-right: $space-smaller;
cursor: pointer; cursor: pointer;
} }
@ -143,13 +190,10 @@ export default {
right: $space-one; right: $space-one;
} }
.file-uploads {
margin-right: $space-small;
}
.button-wrap { .button-wrap {
display: flex; display: flex;
align-items: center; align-items: center;
padding-left: $space-small;
} }
.user-message-input { .user-message-input {
@ -158,6 +202,9 @@ export default {
min-height: $space-large; min-height: $space-large;
max-height: 2.4 * $space-mega; max-height: 2.4 * $space-mega;
resize: none; resize: none;
padding: 0;
padding-top: $space-small; padding-top: $space-small;
margin-top: $space-small;
margin-bottom: $space-small;
} }
</style> </style>

View file

@ -1,5 +1,9 @@
import events from 'widget/api/events'; import events from 'widget/api/events';
const state = {
isOpen: false,
}
const actions = { const actions = {
create: async (_, { name }) => { create: async (_, { name }) => {
try { try {
@ -10,10 +14,16 @@ const actions = {
}, },
}; };
const mutations = {
toggleOpen($state) {
$state.isOpen = !$state.isOpen;
}
};
export default { export default {
namespaced: true, namespaced: true,
state: {}, state,
getters: {}, getters: {},
actions, actions,
mutations: {}, mutations,
}; };

View file

@ -5,7 +5,7 @@
> >
<spinner size="" /> <spinner size="" />
</div> </div>
<div v-else class="home"> <div v-else class="home" @keydown.esc="closeChat">
<div <div
class="header-wrap bg-white" class="header-wrap bg-white"
:class="{ expanded: !isHeaderCollapsed, collapsed: isHeaderCollapsed }" :class="{ expanded: !isHeaderCollapsed, collapsed: isHeaderCollapsed }"
@ -74,6 +74,7 @@ import ChatFooter from 'widget/components/ChatFooter.vue';
import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue'; import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue';
import ChatHeader from 'widget/components/ChatHeader.vue'; import ChatHeader from 'widget/components/ChatHeader.vue';
import ConversationWrap from 'widget/components/ConversationWrap.vue'; import ConversationWrap from 'widget/components/ConversationWrap.vue';
import { IFrameHelper } from 'widget/helpers/utils';
import configMixin from '../mixins/configMixin'; import configMixin from '../mixins/configMixin';
import TeamAvailability from 'widget/components/TeamAvailability'; import TeamAvailability from 'widget/components/TeamAvailability';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
@ -166,6 +167,9 @@ export default {
startConversation() { startConversation() {
this.isOnCollapsedView = !this.isOnCollapsedView; this.isOnCollapsedView = !this.isOnCollapsedView;
}, },
closeChat() {
IFrameHelper.sendMessage({ event: 'closeChat' });
},
}, },
}; };
</script> </script>
@ -227,7 +231,7 @@ export default {
} }
.input-wrap { .input-wrap {
padding: 0 $space-normal; padding: 0 $space-two;
} }
} }
</style> </style>