@defra/flood-webchat 0.0.1-beta.9 → 1.0.2

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.
Files changed (49) hide show
  1. package/README.md +6 -0
  2. package/dist/client.js +1 -329
  3. package/dist/server.js +1 -286
  4. package/main.scss +13 -8
  5. package/package.json +29 -22
  6. package/src/client/components/availability/availability.jsx +94 -65
  7. package/src/client/components/availability/availability.scss +20 -32
  8. package/src/client/components/chat/chat.jsx +239 -0
  9. package/src/client/components/chat/chat.scss +258 -0
  10. package/src/client/components/errorSummary/error-summary.jsx +34 -0
  11. package/src/client/components/live-region.jsx +55 -0
  12. package/src/client/components/message/message.jsx +21 -0
  13. package/src/client/components/panel/panel-footer.jsx +9 -0
  14. package/src/client/components/panel/panel-header.jsx +75 -0
  15. package/src/client/components/panel/panel.jsx +163 -0
  16. package/src/client/components/panel/panel.scss +112 -0
  17. package/src/client/components/screens/end-chat.jsx +77 -0
  18. package/src/client/components/screens/feedback.jsx +65 -0
  19. package/src/client/components/screens/pre-chat.jsx +62 -0
  20. package/src/client/components/screens/request-chat.jsx +183 -0
  21. package/src/client/components/screens/settings.jsx +97 -0
  22. package/src/client/components/screens/unavailable.jsx +23 -0
  23. package/src/client/components/skip-link.jsx +42 -0
  24. package/src/client/hooks/useFocusedElements.js +80 -0
  25. package/src/client/hooks/useTextareaAutosize.js +13 -0
  26. package/src/client/index.jsx +18 -1
  27. package/src/client/lib/agent-status-headline.js +17 -0
  28. package/src/client/lib/check-availability.js +1 -0
  29. package/src/client/lib/history.js +13 -0
  30. package/src/client/lib/message-notification.js +30 -0
  31. package/src/client/lib/transform-messages.js +47 -0
  32. package/src/client/scss/_objects.scss +33 -0
  33. package/src/client/scss/_settings.scss +101 -0
  34. package/src/client/scss/_tools.scss +33 -0
  35. package/src/client/scss/_utilities.scss +25 -0
  36. package/src/client/store/AppProvider.jsx +183 -0
  37. package/src/client/store/actions-map.js +152 -0
  38. package/src/client/store/constants.js +4 -0
  39. package/src/client/store/reducer.js +31 -0
  40. package/src/client/store/useApp.js +5 -0
  41. package/src/client/store/useChatSdk.js +37 -0
  42. package/src/server/lib/client.js +2 -2
  43. package/src/server/lib/utils.js +16 -8
  44. package/webpack.config.mjs +0 -2
  45. package/webpack.prod.mjs +7 -0
  46. package/dist/client.js.map +0 -1
  47. package/dist/server.js.map +0 -1
  48. package/src/client/lib/external-stores.js +0 -4
  49. package/src/client/lib/external-sync-store.js +0 -42
@@ -0,0 +1,163 @@
1
+ import React, { useEffect, useState, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { classnames } from '../../lib/classnames.js'
4
+
5
+ import { PreChat } from '../screens/pre-chat.jsx'
6
+ import { RequestChat } from '../screens/request-chat.jsx'
7
+ import { Chat } from '../chat/chat.jsx'
8
+ import { Unavailable } from '../screens/unavailable.jsx'
9
+ import { EndChat } from '../screens/end-chat.jsx'
10
+ import { Settings } from '../screens/settings.jsx'
11
+ import { Feedback } from '../screens/feedback.jsx'
12
+ import { LiveRegion } from '../live-region.jsx'
13
+
14
+ import { useApp } from '../../store/useApp.js'
15
+ import { useChatSdk } from '../../store/useChatSdk.js'
16
+ import { useFocusedElements } from '../../hooks/useFocusedElements.js'
17
+ import { historyReplaceState } from '../../lib/history.js'
18
+
19
+ export function Panel () {
20
+ const { sdk, availability, instigatorId, thread, threadId, setThread, setThreadId, setChatVisibility, setMessages, setUnseenCount, isMobile, isKeyboard } = useApp()
21
+ const { fetchThread, fetchMessages } = useChatSdk(sdk)
22
+
23
+ const [screen, setScreen] = useState(threadId ? 2 : 0)
24
+
25
+ useFocusedElements(screen)
26
+
27
+ const onEscapeKey = useCallback(e => {
28
+ if (e.key === 'Escape' || e.key === 'Esc') {
29
+ if (threadId) {
30
+ thread?.lastMessageSeen()
31
+ setUnseenCount(0)
32
+ }
33
+ setChatVisibility(false)
34
+ historyReplaceState()
35
+ }
36
+ }, [thread, threadId, setUnseenCount, setChatVisibility])
37
+
38
+ /**
39
+ * Focus previously focused element when closing the webchat
40
+ */
41
+ useEffect(() => {
42
+ return () => {
43
+ document.getElementById(instigatorId)?.focus()
44
+ }
45
+ }, [])
46
+
47
+ /**
48
+ * We need ammend classes on the body element to handle mobile behaviour
49
+ */
50
+ useEffect(() => {
51
+ document.body.classList.remove('wc-u-hidden')
52
+ document.getElementsByTagName('html')[0].classList.add('wc-u-html')
53
+ document.body.classList.add('wc-u-body')
54
+ return () => {
55
+ document.getElementsByTagName('html')[0].classList.remove('wc-u-html')
56
+ document.body.classList.remove('wc-u-body')
57
+ }
58
+ }, [isMobile])
59
+
60
+ /**
61
+ * Initializes the eventListener for pressing the escape key
62
+ */
63
+ useEffect(() => {
64
+ document.addEventListener('keydown', onEscapeKey)
65
+
66
+ return () => {
67
+ document.removeEventListener('keydown', onEscapeKey)
68
+ }
69
+ }, [onEscapeKey, setChatVisibility])
70
+
71
+ /**
72
+ * Recovers the thread if there is a threadId but no thread loaded in to state
73
+ */
74
+ useEffect(() => {
75
+ const recover = async () => {
76
+ try {
77
+ const fetchedThread = await fetchThread(threadId)
78
+ setThread(fetchedThread)
79
+
80
+ const fetchedMessages = await fetchMessages(fetchedThread, threadId)
81
+ setMessages(fetchedMessages)
82
+ } catch (err) {
83
+ console.log('[Chat Error] fetchThread', err)
84
+
85
+ setThreadId()
86
+ setThread()
87
+ setScreen(0)
88
+ }
89
+ }
90
+
91
+ if (threadId) {
92
+ if (thread) {
93
+ thread.lastMessageSeen()
94
+ setUnseenCount(0)
95
+ setScreen(2)
96
+ } else {
97
+ recover()
98
+ }
99
+ }
100
+ }, [thread, threadId])
101
+
102
+ const handleScreenChange = newScreen => e => {
103
+ e.preventDefault()
104
+ setScreen(newScreen)
105
+ }
106
+
107
+ const goToPreChatScreen = handleScreenChange(0)
108
+ const goToRequestChatScreen = handleScreenChange(1)
109
+ const goToChatScreen = handleScreenChange(2)
110
+ const goToEndChatScreen = handleScreenChange(3)
111
+ const goToFeedbackScreen = handleScreenChange(4)
112
+ const goToSettingsScreen = handleScreenChange(5)
113
+
114
+ let ScreenComponent
115
+
116
+ switch (screen) {
117
+ case 0:
118
+ ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
119
+ break
120
+ case 1:
121
+ ScreenComponent = <RequestChat onPreChatScreen={goToPreChatScreen} />
122
+ break
123
+ case 2:
124
+ ScreenComponent = <Chat onSettingsScreen={goToSettingsScreen} onEndChatScreen={goToEndChatScreen} />
125
+ break
126
+ case 3:
127
+ ScreenComponent = <EndChat onChatScreen={goToChatScreen} onEndChatConfirm={goToFeedbackScreen} />
128
+ break
129
+ case 4:
130
+ ScreenComponent = (
131
+ <Feedback onCancel={() => {
132
+ setChatVisibility(false)
133
+ historyReplaceState()
134
+ }}
135
+ />
136
+ )
137
+ break
138
+ case 5:
139
+ ScreenComponent = <Settings onCancel={goToChatScreen} />
140
+ break
141
+ default:
142
+ ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
143
+ }
144
+
145
+ if (availability === 'UNAVAILABLE') {
146
+ ScreenComponent = <Unavailable />
147
+ }
148
+
149
+ const Component = (
150
+ <div id='wc-panel' role='dialog' className={classnames('wc-panel', isKeyboard && 'wc-focus-visible')} tabIndex='-1' aria-modal='true' aria-labelledby='wc-header wc-subtitle'>
151
+ <div className='wc-panel__inner'>
152
+ {ScreenComponent}
153
+ </div>
154
+ <LiveRegion />
155
+ </div>
156
+ )
157
+
158
+ return (
159
+ <>
160
+ {createPortal(Component, document.body)}
161
+ </>
162
+ )
163
+ }
@@ -0,0 +1,112 @@
1
+ .wc-panel {
2
+ display: flex;
3
+ position: fixed;
4
+ flex-wrap: nowrap;
5
+ z-index: 999;
6
+ top: 0;
7
+ right: 0;
8
+ bottom: 0;
9
+ left: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ -webkit-overflow-scrolling: touch;
13
+ touch-action: pan-x pan-y;
14
+ background-color: #ffffff;
15
+ @include mq ($from: tablet) {
16
+ top: auto;
17
+ left: auto;
18
+ width: 400px;
19
+ height: 540px;
20
+ margin-right: 10px;
21
+ margin-bottom: 10px;
22
+ }
23
+
24
+ &:focus {
25
+ outline: none;
26
+ }
27
+
28
+ &:focus.wc-focus-visible::after {
29
+ @include focus($glow: 5px, $strong: 2px, $background: 0, $inset: 0);
30
+ left: 4px;
31
+ right: 4px;
32
+ top: 4px;
33
+ bottom: 4px;
34
+ }
35
+
36
+ &__inner {
37
+ position: relative;
38
+ flex: 0 0 100%;
39
+ display: flex;
40
+ flex-wrap: nowrap;
41
+ flex-direction: column;
42
+ background-color: transparent;
43
+ border: 0;
44
+ outline: 0;
45
+ @include mq ($from: tablet) {
46
+ border: 1px solid govuk-colour('black');
47
+ }
48
+ box-sizing: border-box;
49
+ }
50
+ }
51
+
52
+ .wc-header {
53
+ display: flex;
54
+ flex-direction: row;
55
+ align-items: center;
56
+ background-color: govuk-colour('black');
57
+ border-bottom: 1px solid govuk-colour('black');
58
+ padding: 3px;
59
+
60
+ &__link {
61
+ margin: 0 govuk-spacing(2);
62
+ }
63
+
64
+ &__title {
65
+ @extend %govuk-heading-s;
66
+ font-size: 19px;
67
+ padding: 0 0 0 10px;
68
+ @include mq ($from: tablet) {
69
+ padding-left: 7px;
70
+ }
71
+ color: govuk-colour('white');
72
+ margin-bottom: 0;
73
+ }
74
+
75
+ &__close,
76
+ &__hide,
77
+ &__back {
78
+ display: flex;
79
+ justify-content: center;
80
+ color: govuk-colour('white');
81
+ background: none;
82
+ border: 0;
83
+ padding: 7px;
84
+ margin: 0 0 0 auto;
85
+ cursor: pointer;
86
+
87
+ &:focus {
88
+ outline: 3px solid $govuk-focus-colour;
89
+ }
90
+ }
91
+
92
+ &__back {
93
+ margin: 0;
94
+ display: flex;
95
+
96
+ @include mq ($from: 'tablet') {
97
+ display: none
98
+ }
99
+ }
100
+ }
101
+
102
+ .wc-body {
103
+ font-size: 16px;
104
+ flex: 1;
105
+ position: relative;
106
+ overflow-y: auto;
107
+ overscroll-behavior: contain;
108
+ }
109
+
110
+ .wc-content {
111
+ padding: 20px 15px;
112
+ }
@@ -0,0 +1,77 @@
1
+ import React, { useEffect } from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+ import { useApp } from '../../store/useApp.js'
4
+
5
+ export function EndChat ({ onChatScreen, onEndChatConfirm }) {
6
+ const { setCustomerId, setThreadId, setMessages, agentStatus, thread, threadId, setUnseenCount, isKeyboard } = useApp()
7
+
8
+ const confirmEndChat = async e => {
9
+ e.preventDefault()
10
+
11
+ window.localStorage.setItem('tmpThreadId', threadId)
12
+ setThreadId()
13
+ setMessages([])
14
+ setCustomerId()
15
+ thread.lastMessageSeen()
16
+ setUnseenCount(0)
17
+
18
+ // End chat if still open
19
+ if (agentStatus !== 'closed') {
20
+ thread.endChat()
21
+ }
22
+
23
+ onEndChatConfirm(e)
24
+ }
25
+
26
+ useEffect(() => {
27
+ if (isKeyboard) {
28
+ setTimeout(() => {
29
+ document.querySelector('#confirm-endchat').focus()
30
+ }, 10)
31
+ }
32
+ }, [])
33
+
34
+ const handleKeyPress = e => {
35
+ if ((e.key === 'Enter' || e.key === ' ') && e.target.id === 'confirm-endchat') {
36
+ confirmEndChat(e)
37
+ }
38
+ if ((e.key === 'Enter' || e.key === ' ') && e.target.id === 'resume-chat') {
39
+ onChatScreen(e)
40
+ }
41
+ }
42
+
43
+ return (
44
+ <>
45
+ <PanelHeader />
46
+ <div className='wc-body'>
47
+ <div className='wc-content'>
48
+ <h3 id='wc-subtitle' className='wc-heading' aria-live='polite'>Are you sure you want to end the chat?</h3>
49
+ <div className='govuk-button-group'>
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>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </>
76
+ )
77
+ }
@@ -0,0 +1,65 @@
1
+ import React from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+
4
+ export function Feedback ({ onCancel }) {
5
+ const feedbackSend = () => {
6
+ const tmpThreadId = window.localStorage.getItem('tmpThreadId')
7
+ window.location.href = `https://defragroup.eu.qualtrics.com/jfe/form/SV_8dgFSJcxxIfqx5Y?Id=${tmpThreadId}&Source=${window.location.href}`
8
+ window.localStorage.removeItem('tmpThreadId')
9
+ }
10
+
11
+ const feedbackClose = async e => {
12
+ window.localStorage.removeItem('tmpThreadId')
13
+ onCancel(e)
14
+ }
15
+
16
+ const handleKeyPress = e => {
17
+ if (e.key === 'Enter' || e.key === ' ') {
18
+ if (e.target.id === 'feedback-close') {
19
+ feedbackClose(e)
20
+ }
21
+ if (e.target.id === 'feedback-send') {
22
+ feedbackSend()
23
+ }
24
+ }
25
+ }
26
+
27
+ return (
28
+ <>
29
+ <PanelHeader />
30
+
31
+ <div className='wc-body'>
32
+ <div className='wc-content'>
33
+ <h3 id='wc-subtitle' className='wc-heading'>Give feedback on Floodline webchat</h3>
34
+ <p>We're running webchat as a trial. you can&nbsp;
35
+ <a
36
+ id='feedback-send'
37
+ className='govuk-link'
38
+ href='#'
39
+ target='_blank'
40
+ rel='noreferrer'
41
+ onKeyDown={handleKeyPress}
42
+ onClick={feedbackSend}
43
+ >
44
+ give feedback
45
+ </a>
46
+ &nbsp;to help us improve it.
47
+ </p>
48
+ <p>
49
+ <a
50
+ id='feedback-close'
51
+ className='wc-link govuk-link'
52
+ href='#'
53
+ data-module='govuk-button'
54
+ role='button'
55
+ onKeyDown={handleKeyPress}
56
+ onClick={feedbackClose}
57
+ >
58
+ Close
59
+ </a>
60
+ </p>
61
+ </div>
62
+ </div>
63
+ </>
64
+ )
65
+ }
@@ -0,0 +1,62 @@
1
+ import React from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+
4
+ export function PreChat ({ onContinue }) {
5
+ return (
6
+ <>
7
+ <PanelHeader />
8
+
9
+ <div className='wc-body'>
10
+ <div className='wc-content'>
11
+ <h3 id='wc-subtitle' className='wc-heading' aria-live='polite'>What you can use webchat for</h3>
12
+ <p>Webchat lets you talk directly to a Floodline adviser.</p>
13
+
14
+ <p>You can use webchat to get:</p>
15
+ <ul className='wc-list'>
16
+ <li>current flood warnings and alerts in your area</li>
17
+ <li>information on the flood warning service</li>
18
+ <li>advice on what to do before, during and after a flood</li>
19
+ </ul>
20
+ <p>
21
+ There are other ways to&nbsp;
22
+ <a className='govuk-link' href='https://www.gov.uk/sign-up-for-flood-warnings' target='_blank' rel='noreferrer'>
23
+ sign up for flood warnings
24
+ </a>
25
+ &nbsp;and&nbsp;
26
+ <a className='govuk-link' href='https://www.fws.environment-agency.gov.uk/app/olr/login' target='_blank' rel='noreferrer'>
27
+ manage your flood warnings account
28
+ </a>.
29
+ </p>
30
+ <p>
31
+ Do not use webchat to&nbsp;
32
+ <a
33
+ className='govuk-link'
34
+ href='https://www.gov.uk/report-flood-cause'
35
+ target='_blank'
36
+ rel='noreferrer'
37
+ >
38
+ report a flood
39
+ </a>.
40
+ </p>
41
+
42
+ <button className='govuk-button wc-button govuk-!-margin-top-1' data-module='govuk-button' onClick={onContinue}>Continue</button>
43
+
44
+ <h3 className='wc-heading'>Other flood information</h3>
45
+
46
+ <p>Call Floodline for all other flood and flood warning information.</p>
47
+ <p>
48
+ <strong>Floodline helpline</strong>
49
+ <br />
50
+ Telephone: 0345 988 1188
51
+ <br />
52
+ Textphone: 0345 602 6340
53
+ <br />
54
+ Open 24 hours a day, 7 days a week
55
+ <br />
56
+ <a className='govuk-link' href='https://gov.uk/call-charges' target='_blank' rel='noreferrer'>Find out more about call charges</a>
57
+ </p>
58
+ </div>
59
+ </div>
60
+ </>
61
+ )
62
+ }
@@ -0,0 +1,183 @@
1
+ import React, { useRef, useState, useEffect } from 'react'
2
+ import * as uuid from 'uuid'
3
+
4
+ import { classnames } from '../../lib/classnames.js'
5
+ import { PanelHeader } from '../panel/panel-header.jsx'
6
+ import { useApp } from '../../store/useApp.js'
7
+ import { useChatSdk } from '../../store/useChatSdk.js'
8
+ import { ErrorSummary } from '../errorSummary/error-summary.jsx'
9
+
10
+ const QUESTION_MAX_LENGTH = 500
11
+
12
+ export function RequestChat ({ onPreChatScreen }) {
13
+ const { sdk, setCustomerId, setThreadId, setThread, setLiveRegionText } = useApp()
14
+ const { fetchCustomerId, fetchThread } = useChatSdk(sdk)
15
+ const [errors, setErrors] = useState({})
16
+ const [questionLength, setQuestionLength] = useState(0)
17
+ const [isButtonDisabled, setButtonDisabled] = useState(false)
18
+
19
+ const nameRef = useRef()
20
+ const questionRef = useRef()
21
+
22
+ const isQuestionLengthValid = QUESTION_MAX_LENGTH >= questionLength
23
+ const questionLengthRemaining = QUESTION_MAX_LENGTH - questionLength
24
+ const questionLengthExceeded = questionLength - QUESTION_MAX_LENGTH
25
+
26
+ let questionHint = `You have ${questionLengthRemaining} characters remaining`
27
+
28
+ if (!isQuestionLengthValid) {
29
+ questionHint = `You have ${questionLengthExceeded} characters too many`
30
+ }
31
+
32
+ useEffect(() => {
33
+ if (Object.keys(errors).length) {
34
+ document.querySelector('.govuk-error-summary')?.focus()
35
+ }
36
+ }, [errors])
37
+
38
+ const onQuestionChange = e => {
39
+ setQuestionLength(e.target.value.length)
40
+ }
41
+
42
+ const buttonLabel = isButtonDisabled ? 'Requesting...' : 'Request chat'
43
+
44
+ useEffect(() => {
45
+ if (isButtonDisabled) {
46
+ setLiveRegionText(buttonLabel)
47
+ }
48
+ }, [isButtonDisabled])
49
+
50
+ const onRequestChat = async e => {
51
+ e.preventDefault()
52
+
53
+ if (isButtonDisabled) {
54
+ return
55
+ }
56
+
57
+ const errs = {}
58
+
59
+ if (nameRef.current.value.length === 0) {
60
+ errs.name = 'Enter your name'
61
+ }
62
+
63
+ if (questionRef.current.value.length === 0) {
64
+ errs.question = 'Enter your question'
65
+ }
66
+
67
+ if (questionRef.current.value.length > QUESTION_MAX_LENGTH) {
68
+ errs.question = 'Your question must be 500 characters or less'
69
+ }
70
+
71
+ if (Object.keys(errs).length === 0) {
72
+ try {
73
+ setButtonDisabled(true)
74
+ const threadId = uuid.v4()
75
+ const customerId = await fetchCustomerId()
76
+ const thread = await fetchThread(threadId)
77
+
78
+ sdk.getCustomer().setName(nameRef.current.value)
79
+
80
+ await thread.startChat(questionRef.current.value || 'Begin conversation')
81
+
82
+ setCustomerId(customerId)
83
+ setThreadId(threadId)
84
+ setThread(thread)
85
+ thread.setCustomField('threadid', threadId)
86
+ } catch (err) {
87
+ console.log('[Request chat Error]', err)
88
+ }
89
+ }
90
+
91
+ setErrors(errs)
92
+ }
93
+
94
+ return (
95
+ <>
96
+ <PanelHeader />
97
+
98
+ <div className='wc-body'>
99
+ <div className='wc-content'>
100
+ <a href='#' className='wc-back-link govuk-back-link' onClick={onPreChatScreen}>
101
+ What you can use webchat for
102
+ </a>
103
+
104
+ <ErrorSummary errors={errors} />
105
+
106
+ <h3 id='wc-subtitle' className='wc-heading' aria-live='polite'>
107
+ Your name and question
108
+ </h3>
109
+
110
+ <form>
111
+ <div className={classnames('wc-form-group', 'govuk-form-group', errors.name && 'govuk-form-group--error')}>
112
+ <label className='wc-label govuk-label' htmlFor='wc-name'>
113
+ Your name
114
+ </label>
115
+ {errors.name && (
116
+ <p className='govuk-error-message'>
117
+ <span className='govuk-visually-hidden'>Error:</span> {errors.name}
118
+ </p>
119
+ )}
120
+ <input
121
+ ref={nameRef}
122
+ className={classnames('wc-input', 'govuk-input', errors.name && 'govuk-input--error')}
123
+ id='wc-name'
124
+ name='name'
125
+ type='text'
126
+ data-testid='request-chat-user-name'
127
+ />
128
+ </div>
129
+
130
+ <div className='wc-form-group govuk-character-count' data-module='govuk-character-count' data-maxlength='500'>
131
+ <div className={classnames('govuk-form-group', errors.question && 'govuk-form-group--error')}>
132
+ <label className='wc-label govuk-label' htmlFor='wc-question'>
133
+ Your question
134
+ </label>
135
+ {errors.question && (
136
+ <p className='govuk-error-message'>
137
+ <span className='govuk-visually-hidden'>Error:</span>
138
+ {errors.question}
139
+ </p>
140
+ )}
141
+ <textarea
142
+ ref={questionRef}
143
+ id='wc-question'
144
+ name='question'
145
+ rows='5'
146
+ aria-describedby='wc-question-info'
147
+ onChange={onQuestionChange}
148
+ className={classnames('wc-textarea', 'govuk-textarea', 'govuk-js-character-count', !isQuestionLengthValid || errors.question ? 'govuk-textarea--error' : '')}
149
+ data-testid='request-chat-user-question'
150
+ />
151
+ <div
152
+ id='wc-question-info'
153
+ className='wc-hint govuk-hint govuk-char-count__msg'
154
+ style={{ color: `${!isQuestionLengthValid ? '#d4351c' : ''}` }}
155
+ aria-hidden='true'
156
+ >
157
+ {questionHint}
158
+ </div>
159
+ <div className='govuk-character-count__sr-status govuk-visually-hidden' aria-live='polite'>
160
+ {questionHint}
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ <div className='wc-inset-text govuk-inset-text'>
166
+ By selecting 'Request chat' you agree to the terms of our&nbsp;
167
+ <a href='https://check-for-flooding.service.gov.uk/privacy-notice' target='_blank' rel='noreferrer' className='govuk-link'>privacy notice</a>.
168
+ </div>
169
+
170
+ <button
171
+ className='wc-button govuk-button govuk-!-margin-top-1'
172
+ data-module='govuk-button'
173
+ onClick={onRequestChat}
174
+ aria-disabled={isButtonDisabled}
175
+ >
176
+ {buttonLabel}
177
+ </button>
178
+ </form>
179
+ </div>
180
+ </div>
181
+ </>
182
+ )
183
+ }