@defra/flood-webchat 0.0.1-beta.36 → 0.0.1-beta.38-draft

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.
@@ -6,9 +6,10 @@ 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
 
@@ -38,6 +39,16 @@ export function Availability () {
38
39
  }
39
40
  }
40
41
 
42
+ useEffect(() => {
43
+ if (!isChatOpen && unseenCount > 0) {
44
+ setLiveRegionText(`Floodline Webchat - ${unseenCount} new messages`)
45
+ }
46
+
47
+ return () => {
48
+ setLiveRegionText()
49
+ }
50
+ }, [unseenCount, isChatOpen])
51
+
41
52
  useEffect(() => {
42
53
  const onScroll = () => {
43
54
  if (!threadId) {
@@ -93,7 +104,14 @@ export function Availability () {
93
104
  data-module='govuk-button'
94
105
  >
95
106
  {TextComponent}
96
- {unseenCount > 0 && !isChatOpen ? <span className='wc-availability__unseen'>{unseenCount}</span> : null}
107
+ {unseenCount > 0 && !isChatOpen
108
+ ? (
109
+ <>
110
+ <span className='wc-availability__unseen'>{unseenCount}</span>
111
+ <LiveRegion />
112
+ </>
113
+ )
114
+ : null}
97
115
  </a>
98
116
  </div>
99
117
  </div>
@@ -7,9 +7,10 @@ import { Message } from '../message/message.jsx'
7
7
  import { useApp } from '../../store/useApp.js'
8
8
  import { useTextareaAutosize } from '../../hooks/useTextareaAutosize.js'
9
9
  import { formatTranscript } from '../../lib/transform-messages.js'
10
+ import { agentStatusHeadline } from '../../lib/agent-status-headline.js'
10
11
 
11
12
  export function Chat ({ onEndChatScreen, onSettingsScreen }) {
12
- const { availability, thread, messages, agent, agentStatus, isAgentTyping, settings, isKeyboard } = useApp()
13
+ const { availability, thread, messages, agent, agentStatus, isAgentTyping, isChatOpen, settings, isKeyboard, setLiveRegionText } = useApp()
13
14
 
14
15
  const [userMessage, setUserMessage] = useState('')
15
16
  const [focusVisibleWithin, setFocusVisibleWithin] = useState(false)
@@ -41,21 +42,39 @@ export function Chat ({ onEndChatScreen, onSettingsScreen }) {
41
42
 
42
43
  const agentName = agent?.nickname || agent?.firstName
43
44
 
44
- let connectionHeadlineText = 'Connecting to Floodline'
45
+ const connectionHeadlineText = agentStatusHeadline(availability, agentStatus, agentName)
45
46
 
46
- if (agentStatus === 'closed') {
47
- connectionHeadlineText = agentName ? `${agentName} ended the session` : 'Session ended by advisor'
48
- } else if (agentStatus) {
49
- if (agentName) {
50
- connectionHeadlineText = `You are speaking with ${agentName}`
51
- } else {
52
- connectionHeadlineText = 'No advisers currently available'
47
+ useEffect(() => {
48
+ if (connectionHeadlineText) {
49
+ setLiveRegionText(`Floodline Webchat - ${connectionHeadlineText}`)
53
50
  }
54
- }
55
51
 
56
- if (availability === 'UNAVAILABLE') {
57
- connectionHeadlineText = 'Webchat is not currently available'
58
- }
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 (lastAgentMessage?.direction === 'outbound') {
71
+ setLiveRegionText(`${agentName} said: ${lastAgentMessage.text}`)
72
+ }
73
+
74
+ return () => {
75
+ setLiveRegionText()
76
+ }
77
+ }, [messages])
59
78
 
60
79
  const sendMessage = () => {
61
80
  if (messageRef.current.value.length === 0) {
@@ -63,19 +82,15 @@ export function Chat ({ onEndChatScreen, onSettingsScreen }) {
63
82
  }
64
83
 
65
84
  try {
66
- thread.sendTextMessage(messageRef.current.value.trim())
85
+ const message = messageRef.current.value.trim()
86
+ thread.sendTextMessage(message)
87
+ setLiveRegionText(`You said: ${message}`)
67
88
  } catch (err) {
68
89
  console.log('[Chat Error] sendMessage', err)
69
90
  }
70
91
 
71
92
  setUserMessage('')
72
93
  }
73
- const handleKeyPress = e => {
74
- if (e.key === 'Enter' && !e.shiftKey) {
75
- e.preventDefault()
76
- sendMessage()
77
- }
78
- }
79
94
 
80
95
  const handleSubmit = e => {
81
96
  e.preventDefault()
@@ -90,13 +105,51 @@ export function Chat ({ onEndChatScreen, onSettingsScreen }) {
90
105
  saveChatLink.setAttribute('href', `data:text/plain;charset=utf-8,${transcript}`)
91
106
  }
92
107
 
108
+ const handleKeyPress = e => {
109
+ if (e.key === 'Enter' && !e.shiftKey) {
110
+ switch (e.target.id) {
111
+ case 'text-area':
112
+ e.preventDefault()
113
+ sendMessage()
114
+ break
115
+ case 'end-chat':
116
+ onEndChatScreen(e)
117
+ break
118
+ case 'wc-settings':
119
+ onSettingsScreen(e)
120
+ break
121
+ case 'transcript-download':
122
+ saveChat()
123
+ break
124
+ default:
125
+ break
126
+ }
127
+ } else if (e.key === ' ') {
128
+ if (e.target.id === 'end-chat') {
129
+ onEndChatScreen(e)
130
+ } else if (e.target.id === 'wc-settings') {
131
+ onSettingsScreen(e)
132
+ }
133
+ }
134
+ }
135
+
93
136
  return (
94
137
  <>
95
138
  <PanelHeader />
96
139
 
97
140
  <div className='wc-status'>
98
141
  <p className='wc-status__availability'>{connectionHeadlineText}</p>
99
- <a className='wc-status__link' href='#' role='button' data-module='govuk-button' onClick={onEndChatScreen}>End chat</a>
142
+ <a
143
+ id='end-chat'
144
+ className='wc-status__link'
145
+ href='#'
146
+ data-module='govuk-button'
147
+ role='button'
148
+ onClick={onEndChatScreen}
149
+ onKeyDown={handleKeyPress}
150
+ >
151
+ End chat
152
+ </a>
100
153
  </div>
101
154
 
102
155
  <div className='wc-body' tabIndex='0'>
@@ -132,7 +185,7 @@ export function Chat ({ onEndChatScreen, onSettingsScreen }) {
132
185
  rows='1'
133
186
  aria-required='true'
134
187
  className='wc-form__textarea'
135
- id='wc-form-textarea'
188
+ id='text-area'
136
189
  name='message'
137
190
  onChange={onChange}
138
191
  onKeyDown={handleKeyPress}
@@ -155,6 +208,7 @@ export function Chat ({ onEndChatScreen, onSettingsScreen }) {
155
208
  id='wc-settings'
156
209
  className='wc-footer__settings-link'
157
210
  data-module='govuk-button'
211
+ onKeyDown={handleKeyPress}
158
212
  onClick={onSettingsScreen}
159
213
  >
160
214
  Settings
@@ -165,6 +219,7 @@ export function Chat ({ onEndChatScreen, onSettingsScreen }) {
165
219
  className='wc-footer__settings-link'
166
220
  data-module='govuk-button'
167
221
  download='floodline-webchat-transcript.txt'
222
+ onKeyDown={handleKeyPress}
168
223
  onClick={saveChat}
169
224
  >
170
225
  Save chat
@@ -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
+ export const LiveRegion = () => {
6
+ const { agentStatus, liveRegionText, setLiveRegionText } = useApp()
7
+
8
+ const [textA, setTextA] = useState()
9
+ const [textB, setTextB] = useState()
10
+
11
+ const isSessionEnded = agentStatus === 'closed' || agentStatus === null
12
+
13
+ const clearText = () => {
14
+ setTextA()
15
+ setTextB()
16
+ }
17
+
18
+ const setText = useCallback(debounce(text => {
19
+ if (textA) {
20
+ setTextB(text)
21
+ } else {
22
+ setTextA(text)
23
+ }
24
+ }, 2000), [textA, textB])
25
+
26
+ useEffect(() => {
27
+ console.log('liveRegionText', liveRegionText)
28
+
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'
@@ -136,6 +137,7 @@ export function Panel () {
136
137
  <div className='wc-panel__inner'>
137
138
  {ScreenComponent}
138
139
  </div>
140
+ <LiveRegion />
139
141
  </div>
140
142
  )
141
143
 
@@ -1,6 +1,7 @@
1
1
  import React, { useEffect } from 'react'
2
2
  import { PanelHeader } from '../panel/panel-header.jsx'
3
3
  import { useApp } from '../../store/useApp.js'
4
+ import { historyReplaceState } from '../../lib/history.js'
4
5
 
5
6
  export function EndChat ({ onChatScreen, onEndChatConfirm }) {
6
7
  const { setCustomerId, setThreadId, setMessages, agentStatus, thread, setUnseenCount, isKeyboard } = useApp()
@@ -19,17 +20,26 @@ export function EndChat ({ onChatScreen, onEndChatConfirm }) {
19
20
  thread.endChat()
20
21
  }
21
22
 
23
+ historyReplaceState()
22
24
  onEndChatConfirm(e)
23
25
  }
24
26
 
25
27
  useEffect(() => {
26
28
  if (isKeyboard) {
27
29
  setTimeout(() => {
28
- document.querySelector('#confirmEndChat').focus()
30
+ document.querySelector('#confirm-endchat').focus()
29
31
  }, 10)
30
32
  }
31
33
  }, [])
32
34
 
35
+ const handleKeyPress = e => {
36
+ if ((e.key === 'Enter' || e.key === ' ') && e.target.id === 'confirm-endchat') {
37
+ confirmEndChat(e)
38
+ } else if ((e.key === 'Enter' || e.key === ' ') && e.target.id === 'resume-chat') {
39
+ onChatScreen(e)
40
+ }
41
+ }
42
+
33
43
  return (
34
44
  <>
35
45
  <PanelHeader />
@@ -37,8 +47,28 @@ export function EndChat ({ onChatScreen, onEndChatConfirm }) {
37
47
  <div className='wc-content'>
38
48
  <h3 className='wc-heading' aria-live='polite'>Are you sure you want to end the chat?</h3>
39
49
  <div className='govuk-button-group'>
40
- <a href='#endChat' role='button' className='wc-button govuk-button' data-module='govuk-button' id='confirmEndChat' onClick={confirmEndChat}>Yes, end chat</a>
41
- <a href='#resumeChat' role='button' className='wc-link govuk-link' data-module='govuk-button' onClick={onChatScreen}>No, resume chat</a>
50
+ <a
51
+ id='confirm-endchat'
52
+ href='#endChat'
53
+ role='button'
54
+ className='wc-button govuk-button'
55
+ data-module='govuk-button'
56
+ onClick={confirmEndChat}
57
+ onKeyDown={handleKeyPress}
58
+ >
59
+ Yes, end chat
60
+ </a>
61
+ <a
62
+ id='resume-chat'
63
+ href='#resumeChat'
64
+ role='button'
65
+ className='wc-link govuk-link'
66
+ data-module='govuk-button'
67
+ onClick={onChatScreen}
68
+ onKeyDown={handleKeyPress}
69
+ >
70
+ No, resume chat
71
+ </a>
42
72
  </div>
43
73
  </div>
44
74
  </div>
@@ -6,6 +6,12 @@ export function Feedback ({ onCancel }) {
6
6
  onCancel(e)
7
7
  }
8
8
 
9
+ const handleKeyPress = e => {
10
+ if ((e.key === 'Enter' || e.key === ' ') && e.target.id === 'feedback-cancel') {
11
+ onCancel(e)
12
+ }
13
+ }
14
+
9
15
  return (
10
16
  <>
11
17
  <PanelHeader />
@@ -14,8 +20,29 @@ export function Feedback ({ onCancel }) {
14
20
  <div className='wc-content'>
15
21
  <h3 id='wc-subtitle' className='wc-heading'>Give Feedback on Floodline webchat</h3>
16
22
  <p>Please note we’re unable to respond to feedback.</p>
17
- <p><a className='govuk-link' href='#' target='_blank' rel='noreferrer' role='button' data-module='govuk-button'>Feedback Link</a></p>
18
- <p><a id='feedback-cancel' className='wc-link govuk-link' href='#' data-module='govuk-button' role='button' onClick={feedbackCancel}>Close</a></p>
23
+ <p>
24
+ <a
25
+ className='govuk-link'
26
+ href='#'
27
+ target='_blank'
28
+ rel='noreferrer'
29
+ >
30
+ Feedback Link
31
+ </a>
32
+ </p>
33
+ <p>
34
+ <a
35
+ id='feedback-cancel'
36
+ className='wc-link govuk-link'
37
+ href='#'
38
+ data-module='govuk-button'
39
+ role='button'
40
+ onKeyDown={handleKeyPress}
41
+ onClick={feedbackCancel}
42
+ >
43
+ Close
44
+ </a>
45
+ </p>
19
46
  </div>
20
47
  </div>
21
48
  </>
@@ -16,6 +16,14 @@ export function Settings ({ onCancel }) {
16
16
  onCancel(e)
17
17
  }
18
18
 
19
+ const handleKeyPress = e => {
20
+ if ((e.key === 'Enter' || e.key === ' ') && e.target.id === 'settings-save') {
21
+ onSave(e)
22
+ } else if ((e.key === 'Enter' || e.key === ' ') && e.target.id === 'settings-cancel') {
23
+ onCancel(e)
24
+ }
25
+ }
26
+
19
27
  return (
20
28
  <>
21
29
  <PanelHeader />
@@ -59,8 +67,28 @@ export function Settings ({ onCancel }) {
59
67
  </fieldset>
60
68
 
61
69
  <div className='govuk-button-group'>
62
- <a id='settings-save' href='#' className='wc-button govuk-button' data-module='govuk-button' role='button' onClick={onSave}>Save</a>
63
- <a id='settings-cancel' href='#' className='wc-link govuk-link' data-module='govuk-button' role='button' onClick={onCancel}>Cancel</a>
70
+ <a
71
+ id='settings-save'
72
+ href='#'
73
+ className='wc-button govuk-button'
74
+ data-module='govuk-button'
75
+ role='button'
76
+ onClick={onSave}
77
+ onKeyDown={handleKeyPress}
78
+ >
79
+ Save
80
+ </a>
81
+ <a
82
+ id='settings-cancel'
83
+ href='#'
84
+ className='wc-link govuk-link'
85
+ data-module='govuk-button'
86
+ role='button'
87
+ onClick={onCancel}
88
+ onKeyDown={handleKeyPress}
89
+ >
90
+ Cancel
91
+ </a>
64
92
  </div>
65
93
  </div>
66
94
  </div>
@@ -0,0 +1,19 @@
1
+ export const agentStatusHeadline = (availability, agentStatus, agentName) => {
2
+ let connectionHeadlineText = 'Connecting to Floodline'
3
+
4
+ if (agentStatus === 'closed') {
5
+ connectionHeadlineText = agentName ? `${agentName} ended the session` : 'Session ended by advisor'
6
+ } else if (agentStatus) {
7
+ if (agentName) {
8
+ connectionHeadlineText = `You are speaking with ${agentName}`
9
+ } else {
10
+ connectionHeadlineText = 'No advisers currently available'
11
+ }
12
+ }
13
+
14
+ if (availability === 'UNAVAILABLE') {
15
+ connectionHeadlineText = 'Webchat is not currently available'
16
+ }
17
+
18
+ return connectionHeadlineText
19
+ }
@@ -73,3 +73,24 @@
73
73
  line-height: 1.25;
74
74
  margin: 0 0 25px;
75
75
  }
76
+
77
+ // stylelint-disable declaration-no-important
78
+ .wc-live {
79
+ position: absolute !important;
80
+ width: 1px !important;
81
+ height: 1px !important;
82
+ margin: 0 !important;
83
+ padding: 0 !important;
84
+ overflow: hidden !important;
85
+ clip: rect(0 0 0 0) !important;
86
+ -webkit-clip-path: inset(50%) !important;
87
+ clip-path: inset(50%) !important;
88
+ border: 0 !important;
89
+ white-space: nowrap !important;
90
+
91
+ &::before,
92
+ &::after {
93
+ content: ' ';
94
+ }
95
+ }
96
+ // stylelint-enable declaration-no-important
@@ -27,12 +27,13 @@ export const AppProvider = ({ sdk, availability, options, children }) => {
27
27
  dispatch({ type: 'SET_AGENT_STATUS', payload: e.detail.data.case.status })
28
28
  }
29
29
 
30
- const onAgentTypingStarted = () => {
30
+ const onAgentTypingStarted = e => {
31
31
  dispatch({ type: 'SET_AGENT_TYPING', payload: true })
32
32
  }
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 => {
@@ -149,6 +150,10 @@ export const AppProvider = ({ sdk, availability, options, children }) => {
149
150
  dispatch({ type: 'SET_INSTIGATOR_ID', payload: id })
150
151
  }
151
152
 
153
+ const setLiveRegionText = text => {
154
+ dispatch({ type: 'SET_LIVE_REGION_TEXT', payload: text })
155
+ }
156
+
152
157
  /**
153
158
  * Application-wide state and state functions
154
159
  */
@@ -163,6 +168,7 @@ export const AppProvider = ({ sdk, availability, options, children }) => {
163
168
  setUnseenCount,
164
169
  setChatVisibility,
165
170
  setInstigatorId,
171
+ setLiveRegionText,
166
172
  onLiveChatRecovered,
167
173
  onAssignedAgentChanged,
168
174
  onAgentTypingStarted,
@@ -61,17 +61,18 @@ const setThread = (state, payload) => {
61
61
  }
62
62
  }
63
63
 
64
- const setMessage = (state, payload) => {
64
+ const setMessages = (state, payload) => {
65
65
  return {
66
66
  ...state,
67
- messages: [...state.messages, transformMessage(payload)]
67
+ messages: transformMessages(payload).reverse()
68
68
  }
69
69
  }
70
70
 
71
- const setMessages = (state, payload) => {
71
+ const setMessage = (state, payload) => {
72
72
  return {
73
73
  ...state,
74
- messages: transformMessages(payload).reverse()
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
  }