@defra/flood-webchat 0.0.1-beta.40 → 0.0.1-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +178 -27
- package/dist/client.js.map +1 -1
- package/package.json +2 -1
- package/src/client/components/availability/availability.jsx +17 -9
- package/src/client/components/chat/chat.jsx +36 -2
- package/src/client/components/live-region.jsx +55 -0
- package/src/client/components/panel/panel.jsx +18 -2
- package/src/client/components/screens/feedback.jsx +1 -0
- package/src/client/components/screens/request-chat.jsx +7 -1
- package/src/client/scss/_settings.scss +21 -0
- package/src/client/store/AppProvider.jsx +10 -2
- package/src/client/store/actions-map.js +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/flood-webchat",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.41",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"browser": "dist/client.js",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"@nice-devone/nice-cxone-chat-web-sdk": "^1.3.0",
|
|
63
63
|
"axios": "^1.5.0",
|
|
64
64
|
"jwt-decode": "^3.1.2",
|
|
65
|
+
"lodash.debounce": "^4.0.8",
|
|
65
66
|
"luxon": "^3.4.3",
|
|
66
67
|
"querystring": "^0.2.1",
|
|
67
68
|
"react": "^18.2.0",
|
|
@@ -6,12 +6,15 @@ import { SkipLink } from '../skip-link.jsx'
|
|
|
6
6
|
|
|
7
7
|
import { useApp } from '../../store/useApp'
|
|
8
8
|
import { historyPushState, historyReplaceState } from '../../lib/history.js'
|
|
9
|
+
import { LiveRegion } from '../live-region.jsx'
|
|
9
10
|
|
|
10
11
|
export function Availability () {
|
|
11
|
-
const { availability, isChatOpen, setChatVisibility, unseenCount, threadId, setUnseenCount, setInstigatorId } = useApp()
|
|
12
|
+
const { availability, isChatOpen, setChatVisibility, unseenCount, threadId, setUnseenCount, setInstigatorId, setLiveRegionText } = useApp()
|
|
12
13
|
|
|
13
14
|
const buttonRef = useRef()
|
|
14
15
|
|
|
16
|
+
const showUnseenCount = unseenCount > 0 && !isChatOpen
|
|
17
|
+
|
|
15
18
|
const onClick = e => {
|
|
16
19
|
e.preventDefault()
|
|
17
20
|
setUnseenCount(0)
|
|
@@ -38,6 +41,16 @@ export function Availability () {
|
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (showUnseenCount) {
|
|
46
|
+
setLiveRegionText(`Floodline Webchat - ${unseenCount} new messages`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
setLiveRegionText()
|
|
51
|
+
}
|
|
52
|
+
}, [unseenCount, isChatOpen])
|
|
53
|
+
|
|
41
54
|
useEffect(() => {
|
|
42
55
|
const onScroll = () => {
|
|
43
56
|
if (!threadId) {
|
|
@@ -66,12 +79,6 @@ export function Availability () {
|
|
|
66
79
|
}
|
|
67
80
|
}, [threadId, isChatOpen, availability])
|
|
68
81
|
|
|
69
|
-
let TextComponent = (<>Show chat</>)
|
|
70
|
-
|
|
71
|
-
if (!threadId) {
|
|
72
|
-
TextComponent = (<>Start chat</>)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
82
|
switch (availability) {
|
|
76
83
|
case 'AVAILABLE':
|
|
77
84
|
return (
|
|
@@ -92,12 +99,13 @@ export function Availability () {
|
|
|
92
99
|
role='button'
|
|
93
100
|
data-module='govuk-button'
|
|
94
101
|
>
|
|
95
|
-
{
|
|
96
|
-
{
|
|
102
|
+
{!threadId ? <>Start chat</> : <>Show chat</>}
|
|
103
|
+
{showUnseenCount ? <span className='wc-availability__unseen'>{unseenCount}</span> : null}
|
|
97
104
|
</a>
|
|
98
105
|
</div>
|
|
99
106
|
</div>
|
|
100
107
|
{isChatOpen && <Panel />}
|
|
108
|
+
{showUnseenCount ? <LiveRegion /> : null}
|
|
101
109
|
<SkipLink />
|
|
102
110
|
</>
|
|
103
111
|
)
|
|
@@ -10,7 +10,7 @@ import { formatTranscript } from '../../lib/transform-messages.js'
|
|
|
10
10
|
import { agentStatusHeadline } from '../../lib/agent-status-headline.js'
|
|
11
11
|
|
|
12
12
|
export function Chat ({ onEndChatScreen, onSettingsScreen }) {
|
|
13
|
-
const { availability, thread, messages, agent, agentStatus, isAgentTyping, settings, isKeyboard } = useApp()
|
|
13
|
+
const { availability, thread, messages, agent, agentStatus, isAgentTyping, isChatOpen, settings, isKeyboard, setLiveRegionText } = useApp()
|
|
14
14
|
|
|
15
15
|
const [userMessage, setUserMessage] = useState('')
|
|
16
16
|
const [focusVisibleWithin, setFocusVisibleWithin] = useState(false)
|
|
@@ -44,13 +44,47 @@ export function Chat ({ onEndChatScreen, onSettingsScreen }) {
|
|
|
44
44
|
|
|
45
45
|
const connectionHeadlineText = agentStatusHeadline(availability, agentStatus, agentName)
|
|
46
46
|
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (connectionHeadlineText) {
|
|
49
|
+
setLiveRegionText(`Floodline Webchat - ${connectionHeadlineText}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
setLiveRegionText()
|
|
54
|
+
}
|
|
55
|
+
}, [connectionHeadlineText])
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (isAgentTyping && isChatOpen) {
|
|
59
|
+
setLiveRegionText(`${agentName} is typing...`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
setLiveRegionText()
|
|
64
|
+
}
|
|
65
|
+
}, [isAgentTyping, isChatOpen])
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const lastAgentMessage = messages[messages.length - 1]
|
|
69
|
+
|
|
70
|
+
if (agentStatus !== 'closed' && lastAgentMessage?.direction === 'outbound') {
|
|
71
|
+
setLiveRegionText(`${agentName} said: ${lastAgentMessage.text}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
setLiveRegionText()
|
|
76
|
+
}
|
|
77
|
+
}, [messages])
|
|
78
|
+
|
|
47
79
|
const sendMessage = () => {
|
|
48
80
|
if (messageRef.current.value.length === 0 || agentStatus === 'closed') {
|
|
49
81
|
return
|
|
50
82
|
}
|
|
51
83
|
|
|
52
84
|
try {
|
|
53
|
-
|
|
85
|
+
const message = messageRef.current.value.trim()
|
|
86
|
+
thread.sendTextMessage(message)
|
|
87
|
+
setLiveRegionText(`You said: ${message}`)
|
|
54
88
|
} catch (err) {
|
|
55
89
|
console.log('[Chat Error] sendMessage', err)
|
|
56
90
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from 'react'
|
|
2
|
+
import debounce from 'lodash.debounce'
|
|
3
|
+
import { useApp } from '../store/useApp'
|
|
4
|
+
|
|
5
|
+
const DEBOUNCE_MILLISECONDS = 2000
|
|
6
|
+
|
|
7
|
+
export const LiveRegion = () => {
|
|
8
|
+
const { agentStatus, liveRegionText, setLiveRegionText } = useApp()
|
|
9
|
+
|
|
10
|
+
const [textA, setTextA] = useState()
|
|
11
|
+
const [textB, setTextB] = useState()
|
|
12
|
+
|
|
13
|
+
const isSessionEnded = agentStatus === 'closed' || agentStatus === null
|
|
14
|
+
|
|
15
|
+
const clearText = () => {
|
|
16
|
+
setTextA()
|
|
17
|
+
setTextB()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const setText = useCallback(debounce(text => {
|
|
21
|
+
if (textA) {
|
|
22
|
+
setTextB(text)
|
|
23
|
+
} else {
|
|
24
|
+
setTextA(text)
|
|
25
|
+
}
|
|
26
|
+
}, DEBOUNCE_MILLISECONDS), [textA, textB])
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (liveRegionText) {
|
|
30
|
+
clearText()
|
|
31
|
+
setText(liveRegionText)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (isSessionEnded) {
|
|
35
|
+
clearText()
|
|
36
|
+
setLiveRegionText()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
clearText()
|
|
41
|
+
setLiveRegionText()
|
|
42
|
+
}
|
|
43
|
+
}, [liveRegionText])
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<div className='wc-live' aria-live='polite' aria-atomic='true'>
|
|
48
|
+
{textA}
|
|
49
|
+
</div>
|
|
50
|
+
<div className='wc-live' aria-live='polite' aria-atomic='true'>
|
|
51
|
+
{textB}
|
|
52
|
+
</div>
|
|
53
|
+
</>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -9,6 +9,7 @@ import { Unavailable } from '../screens/unavailable.jsx'
|
|
|
9
9
|
import { EndChat } from '../screens/end-chat.jsx'
|
|
10
10
|
import { Settings } from '../screens/settings.jsx'
|
|
11
11
|
import { Feedback } from '../screens/feedback.jsx'
|
|
12
|
+
import { LiveRegion } from '../live-region.jsx'
|
|
12
13
|
|
|
13
14
|
import { useApp } from '../../store/useApp.js'
|
|
14
15
|
import { useChatSdk } from '../../store/useChatSdk.js'
|
|
@@ -31,10 +32,18 @@ export function Panel () {
|
|
|
31
32
|
}
|
|
32
33
|
setChatVisibility(false)
|
|
33
34
|
historyReplaceState()
|
|
34
|
-
document.getElementById(instigatorId)?.focus()
|
|
35
35
|
}
|
|
36
36
|
}, [thread, threadId, setUnseenCount, setChatVisibility])
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Focus previously focused element when closing the webchat
|
|
40
|
+
*/
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
return () => {
|
|
43
|
+
document.getElementById(instigatorId)?.focus()
|
|
44
|
+
}
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
38
47
|
/**
|
|
39
48
|
* We need ammend classes on the body element to handle mobile behaviour
|
|
40
49
|
*/
|
|
@@ -118,7 +127,13 @@ export function Panel () {
|
|
|
118
127
|
ScreenComponent = <EndChat onChatScreen={goToChatScreen} onEndChatConfirm={goToFeedbackScreen} />
|
|
119
128
|
break
|
|
120
129
|
case 4:
|
|
121
|
-
ScreenComponent =
|
|
130
|
+
ScreenComponent = (
|
|
131
|
+
<Feedback onCancel={() => {
|
|
132
|
+
setChatVisibility(false)
|
|
133
|
+
historyReplaceState()
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
)
|
|
122
137
|
break
|
|
123
138
|
case 5:
|
|
124
139
|
ScreenComponent = <Settings onCancel={goToChatScreen} />
|
|
@@ -136,6 +151,7 @@ export function Panel () {
|
|
|
136
151
|
<div className='wc-panel__inner'>
|
|
137
152
|
{ScreenComponent}
|
|
138
153
|
</div>
|
|
154
|
+
<LiveRegion />
|
|
139
155
|
</div>
|
|
140
156
|
)
|
|
141
157
|
|
|
@@ -10,7 +10,7 @@ import { ErrorSummary } from '../errorSummary/error-summary.jsx'
|
|
|
10
10
|
const QUESTION_MAX_LENGTH = 500
|
|
11
11
|
|
|
12
12
|
export function RequestChat ({ onPreChatScreen }) {
|
|
13
|
-
const { sdk, setCustomerId, setThreadId, setThread } = useApp()
|
|
13
|
+
const { sdk, setCustomerId, setThreadId, setThread, setLiveRegionText } = useApp()
|
|
14
14
|
const { fetchCustomerId, fetchThread } = useChatSdk(sdk)
|
|
15
15
|
const [errors, setErrors] = useState({})
|
|
16
16
|
const [questionLength, setQuestionLength] = useState(0)
|
|
@@ -41,6 +41,12 @@ export function RequestChat ({ onPreChatScreen }) {
|
|
|
41
41
|
|
|
42
42
|
const buttonLabel = isButtonDisabled ? 'Requesting...' : 'Request Chat'
|
|
43
43
|
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (isButtonDisabled) {
|
|
46
|
+
setLiveRegionText(buttonLabel)
|
|
47
|
+
}
|
|
48
|
+
}, [isButtonDisabled])
|
|
49
|
+
|
|
44
50
|
const onRequestChat = async e => {
|
|
45
51
|
e.preventDefault()
|
|
46
52
|
|
|
@@ -78,3 +78,24 @@
|
|
|
78
78
|
line-height: 1.25;
|
|
79
79
|
margin: 0 0 25px;
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
// stylelint-disable declaration-no-important
|
|
83
|
+
.wc-live {
|
|
84
|
+
position: absolute !important;
|
|
85
|
+
width: 1px !important;
|
|
86
|
+
height: 1px !important;
|
|
87
|
+
margin: 0 !important;
|
|
88
|
+
padding: 0 !important;
|
|
89
|
+
overflow: hidden !important;
|
|
90
|
+
clip: rect(0 0 0 0) !important;
|
|
91
|
+
-webkit-clip-path: inset(50%) !important;
|
|
92
|
+
clip-path: inset(50%) !important;
|
|
93
|
+
border: 0 !important;
|
|
94
|
+
white-space: nowrap !important;
|
|
95
|
+
|
|
96
|
+
&::before,
|
|
97
|
+
&::after {
|
|
98
|
+
content: ' ';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// stylelint-enable declaration-no-important
|
|
@@ -33,6 +33,7 @@ export const AppProvider = ({ sdk, availability, options, children }) => {
|
|
|
33
33
|
|
|
34
34
|
const onAgentTypingEnded = () => {
|
|
35
35
|
dispatch({ type: 'SET_AGENT_TYPING', payload: false })
|
|
36
|
+
dispatch({ type: 'SET_LIVE_REGION_TEXT' })
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const onMessageCreated = e => {
|
|
@@ -101,13 +102,15 @@ export const AppProvider = ({ sdk, availability, options, children }) => {
|
|
|
101
102
|
setChatVisibility(true)
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
const onBrowserNavigation =
|
|
105
|
+
const onBrowserNavigation = () => {
|
|
105
106
|
if (window.location.hash === '#webchat') {
|
|
106
107
|
setChatVisibility(true)
|
|
107
108
|
} else {
|
|
108
109
|
setChatVisibility(false)
|
|
109
110
|
}
|
|
110
|
-
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
window.addEventListener('popstate', onBrowserNavigation)
|
|
111
114
|
|
|
112
115
|
return () => {
|
|
113
116
|
window.removeEventListener('popstate', onBrowserNavigation)
|
|
@@ -149,6 +152,10 @@ export const AppProvider = ({ sdk, availability, options, children }) => {
|
|
|
149
152
|
dispatch({ type: 'SET_INSTIGATOR_ID', payload: id })
|
|
150
153
|
}
|
|
151
154
|
|
|
155
|
+
const setLiveRegionText = text => {
|
|
156
|
+
dispatch({ type: 'SET_LIVE_REGION_TEXT', payload: text })
|
|
157
|
+
}
|
|
158
|
+
|
|
152
159
|
/**
|
|
153
160
|
* Application-wide state and state functions
|
|
154
161
|
*/
|
|
@@ -163,6 +170,7 @@ export const AppProvider = ({ sdk, availability, options, children }) => {
|
|
|
163
170
|
setUnseenCount,
|
|
164
171
|
setChatVisibility,
|
|
165
172
|
setInstigatorId,
|
|
173
|
+
setLiveRegionText,
|
|
166
174
|
onLiveChatRecovered,
|
|
167
175
|
onAssignedAgentChanged,
|
|
168
176
|
onAgentTypingStarted,
|
|
@@ -61,17 +61,18 @@ const setThread = (state, payload) => {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const
|
|
64
|
+
const setMessages = (state, payload) => {
|
|
65
65
|
return {
|
|
66
66
|
...state,
|
|
67
|
-
messages:
|
|
67
|
+
messages: transformMessages(payload).reverse()
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const
|
|
71
|
+
const setMessage = (state, payload) => {
|
|
72
72
|
return {
|
|
73
73
|
...state,
|
|
74
|
-
|
|
74
|
+
message: transformMessage(payload),
|
|
75
|
+
messages: [...state.messages, transformMessage(payload)]
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -110,6 +111,13 @@ const setInstigatorId = (state, payload) => {
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
const setLiveRegionText = (state, payload) => {
|
|
115
|
+
return {
|
|
116
|
+
...state,
|
|
117
|
+
liveRegionText: payload
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
113
121
|
const toggleIsMobile = (state, payload) => {
|
|
114
122
|
return {
|
|
115
123
|
...state,
|
|
@@ -138,6 +146,7 @@ export const actionsMap = {
|
|
|
138
146
|
SET_AGENT_STATUS: setAgentStatus,
|
|
139
147
|
SET_UNSEEN_COUNT: setUnseenCount,
|
|
140
148
|
SET_INSTIGATOR_ID: setInstigatorId,
|
|
149
|
+
SET_LIVE_REGION_TEXT: setLiveRegionText,
|
|
141
150
|
TOGGLE_IS_MOBILE: toggleIsMobile,
|
|
142
151
|
TOGGLE_IS_KEYBOARD: toggleIsKeyboard
|
|
143
152
|
}
|