@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/flood-webchat",
3
- "version": "0.0.1-beta.40",
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
- {TextComponent}
96
- {unseenCount > 0 && !isChatOpen ? <span className='wc-availability__unseen'>{unseenCount}</span> : null}
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
- thread.sendTextMessage(messageRef.current.value.trim())
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 = <Feedback onCancel={() => setChatVisibility(false)} />
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
 
@@ -1,5 +1,6 @@
1
1
  import React from 'react'
2
2
  import { PanelHeader } from '../panel/panel-header.jsx'
3
+
3
4
  export function Feedback ({ onCancel }) {
4
5
  const feedbackSend = e => {
5
6
  const tmpThreadId = window.localStorage.getItem('tmpThreadId')
@@ -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 = window.addEventListener('popstate', () => {
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 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
  }