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
$color-woot: #1f93ff;
$color-gray: #6e6f73;
$color-light-gray: #999a9b;
$color-light-gray: #747677;
$color-border: #e0e6ed;
$color-border-light: #f0f4f5;
$color-background: #f4f6fb;

View file

@ -142,6 +142,7 @@ export const IFrameHelper = {
},
onBubbleToggle: isOpen => {
IFrameHelper.sendMessage('toggle-open', { isOpen });
if (!isOpen) {
IFrameHelper.events.resetUnreadMode();
} else {
@ -194,6 +195,10 @@ export const IFrameHelper = {
const holderEl = document.querySelector('.woot-widget-holder');
removeClass(holderEl, 'has-unread-view');
},
closeChat: () => {
onBubbleClick({ toggleValue: false });
},
},
pushEvent: 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 bubbleHolder = document.createElement('div');
export const chatBubble = document.createElement('div');
export const closeBubble = document.createElement('div');
export const chatBubble = document.createElement('button');
export const closeBubble = document.createElement('button');
export const notificationBubble = document.createElement('span');
export const getBubbleView = type =>
@ -64,6 +64,10 @@ export const onBubbleClick = (props = {}) => {
toggleClass(closeBubble, 'woot--hide');
toggleClass(widgetHolder, 'woot--hide');
IFrameHelper.events.onBubbleToggle(newIsOpen);
if (!newIsOpen) {
chatBubble.focus();
}
}
};

View file

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

View file

@ -12,7 +12,7 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { mapGetters, mapActions, mapMutations } from 'vuex';
import { setHeader } from 'widget/helpers/axios';
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import Router from './views/Router';
@ -97,6 +97,7 @@ export default {
...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']),
...mapActions('campaign', ['initCampaigns', 'executeCampaign']),
...mapActions('agent', ['fetchAvailableAgents']),
...mapMutations('events', ['toggleOpen']),
scrollConversationToBottom() {
const container = this.$el.querySelector('.conversation-wrap');
container.scrollTop = container.scrollHeight;
@ -249,6 +250,8 @@ export default {
} else if (message.event === 'unset-unread-view') {
this.showUnreadView = 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 {
box-shadow: none;
}
@mixin button-size {
min-height: $space-large;
min-width: $space-large;
}

View file

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

View file

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

View file

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

View file

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

View file

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