@defra/flood-webchat 0.0.1-beta.2 → 0.0.1-beta.21

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 (39) hide show
  1. package/README.md +83 -1
  2. package/dist/client.js +1607 -212
  3. package/dist/client.js.map +1 -1
  4. package/dist/server.js +181 -240
  5. package/dist/server.js.map +1 -1
  6. package/main.scss +4 -3
  7. package/package.json +6 -2
  8. package/src/client/components/availability/availability.jsx +44 -46
  9. package/src/client/components/availability/availability.scss +105 -5
  10. package/src/client/components/chat/chat.jsx +169 -0
  11. package/src/client/components/chat/chat.scss +143 -0
  12. package/src/client/components/errorSummary/error-summary.jsx +34 -0
  13. package/src/client/components/message/message.jsx +20 -0
  14. package/src/client/components/panel/panel-footer.jsx +9 -0
  15. package/src/client/components/panel/panel-header.jsx +41 -0
  16. package/src/client/components/panel/panel.jsx +119 -0
  17. package/src/client/components/panel/panel.scss +72 -0
  18. package/src/client/components/screens/end-chat.jsx +17 -0
  19. package/src/client/components/screens/pre-chat.jsx +65 -0
  20. package/src/client/components/screens/request-chat.jsx +156 -0
  21. package/src/client/components/screens/settings.jsx +67 -0
  22. package/src/client/components/screens/unavailable.jsx +23 -0
  23. package/src/client/hooks/useFocusedElements.js +71 -0
  24. package/src/client/hooks/useTextareaAutosize.js +13 -0
  25. package/src/client/index.jsx +15 -1
  26. package/src/client/lib/check-availability.js +1 -0
  27. package/src/client/lib/message-notification.js +36 -0
  28. package/src/client/lib/transform-messages.js +35 -0
  29. package/src/client/store/AppProvider.jsx +139 -0
  30. package/src/client/store/actions-map.js +119 -0
  31. package/src/client/store/constants.js +3 -0
  32. package/src/client/store/reducer.js +28 -0
  33. package/src/client/store/useApp.js +5 -0
  34. package/src/client/store/useChatSdk.js +37 -0
  35. package/src/server/index.js +15 -6
  36. package/src/server/lib/client.js +31 -35
  37. package/src/server/lib/utils.js +18 -9
  38. package/src/client/lib/external-stores.js +0 -4
  39. package/src/client/lib/external-sync-store.js +0 -42
@@ -0,0 +1,143 @@
1
+ @keyframes blink {
2
+ 50% {
3
+ fill: transparent
4
+ }
5
+ }
6
+
7
+ .wc-status {
8
+ display: flex;
9
+ border-bottom: 1px solid $govuk-border-colour;
10
+ padding: govuk-spacing(2);
11
+
12
+ &__availability {
13
+ margin-bottom: 0;
14
+ }
15
+
16
+ &__link {
17
+ margin-left: auto;
18
+ }
19
+ }
20
+
21
+ .defra-context-footer {
22
+ border-top:2px solid govuk-colour('blue');
23
+ margin-top: 30px;
24
+ padding-top: 10px;
25
+ @include mq ($from: tablet) {
26
+ margin-top: 40px;
27
+ }
28
+ }
29
+
30
+ .wc-chat {
31
+ &__messages {
32
+ overflow: auto;
33
+ list-style: none;
34
+ padding: 0;
35
+ margin: govuk-spacing(2) 0;
36
+ }
37
+
38
+ &__message {
39
+ margin-bottom: govuk-spacing(1);
40
+
41
+ &.inbound {
42
+ text-align: right;
43
+ }
44
+ }
45
+
46
+ &__from {
47
+ color: govuk-colour('dark-grey');
48
+ margin-bottom: 0;
49
+ }
50
+
51
+ &__text {
52
+ padding: govuk-spacing(2);
53
+ margin-bottom: 0;
54
+ display: inline-block;
55
+
56
+ &.inbound {
57
+ color: govuk-colour('white');
58
+ background-color: govuk-colour('blue');
59
+ border-radius: 8px 0 8px 8px;
60
+ }
61
+
62
+ &.outbound {
63
+ background-color: govuk-colour('light-grey');
64
+ border-radius: 0 8px 8px;
65
+ }
66
+
67
+ svg {
68
+ display: block;
69
+ color: govuk-colour('dark-grey');
70
+ }
71
+
72
+ svg circle {
73
+ animation: 1s blink infinite;
74
+ fill: currentcolor;
75
+
76
+ &:nth-child(2) {
77
+ animation-delay: 250ms
78
+ }
79
+
80
+ &:nth-child(3) {
81
+ animation-delay: 500ms
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ .wc-form {
88
+ position: relative;
89
+ display: flex;
90
+ flex-direction: row;
91
+ align-items: center;
92
+ width: 100%;
93
+
94
+ &__label {
95
+ position: absolute;
96
+ top: 9px;
97
+ left: 5px;
98
+ z-index: 3;
99
+ margin-bottom: 0;
100
+ color: govuk-colour('dark-grey');
101
+ }
102
+
103
+ &__textarea {
104
+ position: relative;
105
+ width: 100%;
106
+ margin-bottom: 0;
107
+ min-height: auto;
108
+ max-height: 120px;
109
+ resize: none;
110
+ border: none;
111
+ overflow-x: hidden;
112
+ overscroll-behavior: contain;
113
+ z-index: 4;
114
+ background-color: transparent;
115
+ }
116
+
117
+ &__button {
118
+ margin: auto 0 0 govuk-spacing(3);
119
+ width: auto;
120
+ }
121
+ }
122
+
123
+ .wc-footer {
124
+ &__settings {
125
+ @include govuk-responsive-padding(2);
126
+
127
+ display: flex;
128
+ flex-direction: row;
129
+ border-top: 1px solid govuk-colour('black');
130
+
131
+ &-link {
132
+ &:first-child {
133
+ margin-right: govuk-spacing(3);
134
+ }
135
+ }
136
+ }
137
+
138
+ &__input {
139
+ @include govuk-responsive-padding(2);
140
+
141
+ border-top: 1px solid govuk-colour('black');
142
+ }
143
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react'
2
+
3
+ export const ErrorSummary = ({ errors }) => {
4
+ const errs = Object.keys(errors)
5
+
6
+ const goToInput = e => {
7
+ e.preventDefault()
8
+ const key = e.target.getAttribute('data-key')
9
+ document.querySelector(`#wc-${key}`).focus()
10
+ }
11
+
12
+ if (errs.length === 0) {
13
+ return null
14
+ }
15
+
16
+ return (
17
+ <div className='govuk-error-summary govuk-!-static-margin-bottom-7' data-module='govuk-error-summary' tabIndex='-1'>
18
+ <div role='alert'>
19
+ <h2 id='wc-error' className='govuk-error-summary__title'>
20
+ There is a problem
21
+ </h2>
22
+ <div className='govuk-error-summary__body'>
23
+ <ul className='govuk-list govuk-error-summary__list'>
24
+ {errs.map(key => (
25
+ <li key={key}>
26
+ <a href='#' data-key={key} onClick={goToInput}>{errors[key]}</a>
27
+ </li>
28
+ ))}
29
+ </ul>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ )
34
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react'
2
+ import { classnames } from '../../lib/classnames'
3
+
4
+ export function Message ({ message, previousMessage }) {
5
+ const outbound = message.direction === 'outbound'
6
+
7
+ const messageOwnerSameAsPrevious = message?.direction === previousMessage?.direction
8
+
9
+ return (
10
+ <li className={classnames('wc-chat__message govuk-body', outbound ? 'outbound' : 'inbound')}>
11
+ <div className='wc-chat__from govuk-!-font-size-14'>
12
+ {messageOwnerSameAsPrevious ? null : <span>{outbound ? message.assignee : 'You'}</span>}
13
+ <span className='govuk-visually-hidden'>:</span>
14
+ </div>
15
+ <p className={classnames('wc-chat__text', 'govuk-body-s', outbound ? 'outbound' : 'inbound')}>
16
+ {message.text}
17
+ </p>
18
+ </li>
19
+ )
20
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react'
2
+
3
+ export function PanelFooter ({ children }) {
4
+ return (
5
+ <div className='wc-footer'>
6
+ {children}
7
+ </div>
8
+ )
9
+ }
@@ -0,0 +1,41 @@
1
+ import React from 'react'
2
+ import { useApp } from '../../store/useApp.js'
3
+
4
+ export function PanelHeader () {
5
+ const { thread, setChatVisibility, setUnseenCount } = useApp()
6
+
7
+ const onClose = e => {
8
+ e.preventDefault()
9
+ setChatVisibility(false)
10
+ thread.lastMessageSeen()
11
+ setUnseenCount(0)
12
+ }
13
+
14
+ let ButtonComponent = (
15
+ <button className='wc-header__close' aria-label='Close the webchat' onClick={onClose}>
16
+ <svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
17
+ <path d='M10,8.6L15.6,3L17,4.4L11.4,10L17,15.6L15.6,17L10,11.4L4.4,17L3,15.6L8.6,10L3,4.4L4.4,3L10,8.6Z' fill='currentColor' />
18
+ </svg>
19
+ </button>
20
+ )
21
+
22
+ if (thread) {
23
+ ButtonComponent = (
24
+ <button className='wc-header__hide' aria-label='Minimise the webchat' onClick={onClose}>
25
+ <svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
26
+ <path d='M10 14.4l-7-7L4.4 6l5.6 5.6L15.6 6 17 7.4l-7 7z' fill='currentColor' />
27
+ </svg>
28
+ </button>
29
+ )
30
+ }
31
+
32
+ return (
33
+ <div className='wc-header'>
34
+ <h2 id='wc-header-title' className='wc-header__title govuk-heading-s'>
35
+ Floodline Webchat
36
+ </h2>
37
+
38
+ {ButtonComponent}
39
+ </div>
40
+ )
41
+ }
@@ -0,0 +1,119 @@
1
+ import React, { useEffect, useState, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+
4
+ import { PreChat } from '../screens/pre-chat.jsx'
5
+ import { RequestChat } from '../screens/request-chat.jsx'
6
+ import { Chat } from '../chat/chat.jsx'
7
+ import { Unavailable } from '../screens/unavailable.jsx'
8
+ import { EndChat } from '../screens/end-chat.jsx'
9
+ import { Settings } from '../screens/settings.jsx'
10
+
11
+ import { useApp } from '../../store/useApp.js'
12
+ import { useChatSdk } from '../../store/useChatSdk.js'
13
+ import { useFocusedElements } from '../../hooks/useFocusedElements.js'
14
+
15
+ export function Panel () {
16
+ const { sdk, availability, thread, threadId, setThread, setThreadId, setChatVisibility, setMessages, setUnseenCount } = useApp()
17
+ const { fetchThread, fetchMessages } = useChatSdk(sdk)
18
+
19
+ const [screen, setScreen] = useState(threadId ? 2 : 0)
20
+
21
+ useFocusedElements(screen)
22
+
23
+ /**
24
+ * Initializes the eventListener for pressing the escape key
25
+ */
26
+ useEffect(() => {
27
+ const escapeKeyEvent = document.addEventListener('keydown', onEscapeKey)
28
+
29
+ return () => {
30
+ document.removeEventListener('keydown', escapeKeyEvent)
31
+ }
32
+ }, [])
33
+
34
+ /**
35
+ * Recovers the thread if there is a threadId but no thread loaded in to state
36
+ */
37
+ useEffect(() => {
38
+ const recover = async () => {
39
+ try {
40
+ const fetchedThread = await fetchThread(threadId)
41
+ setThread(fetchedThread)
42
+
43
+ const fetchedMessages = await fetchMessages(fetchedThread, threadId)
44
+ setMessages(fetchedMessages)
45
+ } catch (err) {
46
+ console.log('[Chat Error] fetchThread', err)
47
+
48
+ setThreadId()
49
+ setThread()
50
+ setScreen(0)
51
+ }
52
+ }
53
+
54
+ if (threadId) {
55
+ if (thread) {
56
+ thread.lastMessageSeen()
57
+ setUnseenCount(0)
58
+ setScreen(2)
59
+ } else {
60
+ recover()
61
+ }
62
+ }
63
+ }, [thread, threadId])
64
+
65
+ const onEscapeKey = useCallback(e => {
66
+ if (e.key === 'Escape' || e.key === 'Esc') {
67
+ setChatVisibility(false)
68
+ }
69
+ }, [])
70
+
71
+ const handleScreenChange = newScreen => e => {
72
+ e.preventDefault()
73
+ setScreen(newScreen)
74
+ }
75
+
76
+ const goToPreChatScreen = handleScreenChange(0)
77
+ const goToRequestChatScreen = handleScreenChange(1)
78
+ const goToChatScreen = handleScreenChange(2)
79
+ const goToEndChatScreen = handleScreenChange(3)
80
+ const goToSettingsScreen = handleScreenChange(5)
81
+
82
+ let ScreenComponent
83
+
84
+ switch (screen) {
85
+ case 0:
86
+ ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
87
+ break
88
+ case 1:
89
+ ScreenComponent = <RequestChat onPreChatScreen={goToPreChatScreen} />
90
+ break
91
+ case 2:
92
+ ScreenComponent = <Chat onSettingsScreen={goToSettingsScreen} onEndChatScreen={goToEndChatScreen} />
93
+ break
94
+ case 3:
95
+ ScreenComponent = <EndChat onChatScreen={goToChatScreen} />
96
+ break
97
+ case 5:
98
+ ScreenComponent = <Settings onCancel={goToChatScreen} />
99
+ break
100
+ default:
101
+ ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
102
+ }
103
+
104
+ if (availability === 'UNAVAILABLE') {
105
+ ScreenComponent = <Unavailable />
106
+ }
107
+
108
+ const Component = (
109
+ <div id='wc-panel' role='dialog' className='wc-panel' tabIndex='-1' aria-modal='true' aria-labelledby='wc-header-title'>
110
+ {ScreenComponent}
111
+ </div>
112
+ )
113
+
114
+ return (
115
+ <>
116
+ {createPortal(Component, document.body)}
117
+ </>
118
+ )
119
+ }
@@ -0,0 +1,72 @@
1
+ .wc-panel {
2
+ display: flex;
3
+ flex-direction: column;
4
+ position: fixed;
5
+ bottom: 0;
6
+ right: 0;
7
+ top: 0;
8
+ left: 0;
9
+ width: 100%;
10
+ height: 100%;
11
+ background-color: govuk-colour('white');
12
+ border: 1px solid govuk-colour('black');
13
+ z-index: 999;
14
+ outline: none;
15
+
16
+ @include mq ($from: 'tablet') {
17
+ bottom: 0;
18
+ right: 0;
19
+ top: auto;
20
+ left: auto;
21
+ width: 400px;
22
+ height: 540px;
23
+ margin-bottom: 10px;
24
+ margin-right: 10px;
25
+ }
26
+ }
27
+
28
+ .wc-header {
29
+ display: flex;
30
+ flex-direction: row;
31
+ align-items: center;
32
+ background-color: govuk-colour('black');
33
+
34
+ &__link {
35
+ margin: 0 govuk-spacing(2);
36
+ }
37
+
38
+ &__title {
39
+ padding: 0 0 0 govuk-spacing(2);
40
+ color: govuk-colour('white');
41
+ margin-bottom: 0;
42
+ }
43
+
44
+ &__close,
45
+ &__hide {
46
+ display: flex;
47
+ justify-content: center;
48
+ color: govuk-colour('white');
49
+ background: none;
50
+ border: 0;
51
+ padding: govuk-spacing(2);
52
+ margin: 0 0 0 auto;
53
+ cursor: pointer;
54
+
55
+ &:focus {
56
+ @include govuk-focused-text;
57
+ }
58
+ }
59
+ }
60
+
61
+ .wc-body {
62
+ @include govuk-responsive-padding(3);
63
+
64
+ flex: 1;
65
+ position: relative;
66
+ overflow-y: auto;
67
+ overscroll-behavior: contain;
68
+ }
69
+
70
+ .wc-back-link {
71
+ margin: govuk-spacing(1) 0 govuk-spacing(6) 0;
72
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+
4
+ export function EndChat ({ onChatScreen }) {
5
+ return (
6
+ <>
7
+ <PanelHeader />
8
+ <div className='wc-body'>
9
+ <h3 className='govuk-heading-s' aria-live='polite'>Are you sure you want to end the chat?</h3>
10
+ <div className='govuk-button-group'>
11
+ <a href='#' className='govuk-button govuk-!-font-size-16' data-module='govuk-button'>Yes, end chat</a>
12
+ <a href='#' className='govuk-link govuk-!-font-size-16' onClick={onChatScreen}>No, resume chat</a>
13
+ </div>
14
+ </div>
15
+ </>
16
+ )
17
+ }
@@ -0,0 +1,65 @@
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
+ <h3 className='govuk-heading-s' aria-live='polite'>What you can use webchat for</h3>
11
+ <p className='govuk-body-s'>Webchat lets you talk directly to a Floodline adviser.</p>
12
+
13
+ <p className='govuk-body-s'>You can use webchat to get:</p>
14
+ <ul className='govuk-list govuk-list--bullet govuk-body-s'>
15
+ <li>current flood warnings and alerts in your area</li>
16
+ <li>information on the flood warning service</li>
17
+ <li>advice on what to do before, during and after a flood</li>
18
+ </ul>
19
+
20
+ <p className='govuk-body-s'>
21
+ {
22
+ 'There are other ways to '
23
+ }
24
+ <a className='govuk-link' href='https://www.gov.uk/sign-up-for-flood-warnings' target='_blank' rel='noreferrer'>
25
+ sign up for flood warnings
26
+ </a>
27
+ {
28
+ ' and '
29
+ }
30
+ <a className='govuk-link' href='https://www.fws.environment-agency.gov.uk/app/olr/login' target='_blank' rel='noreferrer'>
31
+ manage your flood warnings account
32
+ </a>.
33
+ </p>
34
+ <p className='govuk-body-s'>
35
+ {'Do not use webchat to '}
36
+ <a
37
+ className='govuk-link'
38
+ href='https://www.gov.uk/report-flood-cause'
39
+ target='_blank'
40
+ rel='noreferrer'
41
+ >
42
+ report a flood
43
+ </a>.
44
+ </p>
45
+
46
+ <button className='govuk-button govuk-!-font-size-16' data-module='govuk-button' onClick={onContinue}>Continue</button>
47
+
48
+ <h3 className='govuk-heading-s'>Other flood information</h3>
49
+
50
+ <p className='govuk-body-s'>Call Floodline for all other flood and flood warning information.</p>
51
+ <p className='govuk-body-s'>
52
+ <strong>Floodline helpline</strong>
53
+ <br />
54
+ Telephone: 0345 988 1188
55
+ <br />
56
+ Textphone: 0345 602 6340
57
+ <br />
58
+ Open 24 hours a day, 7 days a week
59
+ <br />
60
+ <a className='govuk-link' href='https://gov.uk/call-charges' target='_blank' rel='noreferrer'>Find out more about call charges</a>
61
+ </p>
62
+ </div>
63
+ </>
64
+ )
65
+ }
@@ -0,0 +1,156 @@
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 } = useApp()
14
+ const { fetchCustomerId, fetchThread } = useChatSdk(sdk)
15
+
16
+ const [errors, setErrors] = useState({})
17
+ const [questionLength, setQuestionLength] = useState(0)
18
+
19
+ const nameRef = useRef()
20
+ const questionRef = useRef()
21
+
22
+ useEffect(() => {
23
+ if (Object.keys(errors).length) {
24
+ document.querySelector('.govuk-error-summary')?.focus()
25
+ }
26
+ }, [errors])
27
+
28
+ const onQuestionChange = e => {
29
+ setQuestionLength(e.target.value.length)
30
+ }
31
+
32
+ const onRequestChat = async e => {
33
+ e.preventDefault()
34
+
35
+ const errs = {}
36
+
37
+ if (nameRef.current.value.length === 0) {
38
+ errs.name = 'Enter your name'
39
+ }
40
+
41
+ if (questionRef.current.value.length === 0) {
42
+ errs.question = 'Enter your question'
43
+ }
44
+
45
+ if (questionRef.current.value.length > QUESTION_MAX_LENGTH) {
46
+ errs.question = 'Your question must be 500 characters or less'
47
+ }
48
+
49
+ if (Object.keys(errs).length === 0) {
50
+ try {
51
+ const threadId = uuid.v4()
52
+ const customerId = await fetchCustomerId()
53
+ const thread = await fetchThread(threadId)
54
+
55
+ sdk.getCustomer().setName(nameRef.current.value)
56
+
57
+ await thread.startChat(questionRef.current.value || 'Begin conversation')
58
+
59
+ setCustomerId(customerId)
60
+ setThreadId(threadId)
61
+ setThread(thread)
62
+ } catch (err) {
63
+ console.log('[Request Chat Error]', err)
64
+ }
65
+ }
66
+
67
+ setErrors(errs)
68
+ }
69
+
70
+ const isQuestionLengthValid = QUESTION_MAX_LENGTH >= questionLength
71
+ const questionLengthRemaining = QUESTION_MAX_LENGTH - questionLength
72
+ const questionLengthExceeded = questionLength - QUESTION_MAX_LENGTH
73
+
74
+ let questionHint = `You have ${questionLengthRemaining} characters remaining`
75
+
76
+ if (!isQuestionLengthValid) {
77
+ questionHint = `You have ${questionLengthExceeded} characters too many`
78
+ }
79
+
80
+ return (
81
+ <>
82
+ <PanelHeader />
83
+
84
+ <div className='wc-body'>
85
+ <a href='#' className='wc-back-link govuk-back-link' onClick={onPreChatScreen}>
86
+ What you can use webchat for
87
+ </a>
88
+
89
+ <ErrorSummary errors={errors} />
90
+
91
+ <h3 className='govuk-heading-s' aria-live='polite'>
92
+ Your name and question
93
+ </h3>
94
+
95
+ <form>
96
+ <div className={classnames('govuk-form-group', errors.name && 'govuk-form-group--error')}>
97
+ <label className='govuk-label' htmlFor='wc-name'>
98
+ Your name
99
+ </label>
100
+ {errors.name && (
101
+ <p className='govuk-error-message'>
102
+ <span className='govuk-visually-hidden'>Error:</span> {errors.name}
103
+ </p>
104
+ )}
105
+ <input
106
+ ref={nameRef}
107
+ className={classnames('govuk-input', errors.name && 'govuk-input--error')}
108
+ id='wc-name'
109
+ name='name'
110
+ type='text'
111
+ data-testid='request-chat-user-name'
112
+ />
113
+ </div>
114
+
115
+ <div className='govuk-character-count' data-module='govuk-character-count' data-maxlength='500'>
116
+ <div className={classnames('govuk-form-group', errors.question && 'govuk-form-group--error')}>
117
+ <label className='govuk-label' htmlFor='wc-question'>
118
+ Your question
119
+ </label>
120
+ {errors.question && (
121
+ <p className='govuk-error-message'>
122
+ <span className='govuk-visually-hidden'>Error:</span>
123
+ {errors.question}
124
+ </p>
125
+ )}
126
+ <textarea
127
+ ref={questionRef}
128
+ id='wc-question'
129
+ name='question'
130
+ rows='5'
131
+ aria-describedby='wc-question-info'
132
+ onChange={onQuestionChange}
133
+ className={classnames('govuk-textarea', 'govuk-js-character-count', !isQuestionLengthValid || errors.question ? 'govuk-textarea--error' : '')}
134
+ data-testid='request-chat-user-question'
135
+ />
136
+ <div id='wc-question-info' className='govuk-hint govuk-character-count__message' style={{ color: `${!isQuestionLengthValid ? '#d4351c' : ''}` }} aria-hidden='true'>
137
+ {questionHint}
138
+ </div>
139
+ <div className='govuk-character-count__sr-status govuk-visually-hidden' aria-live='polite'>
140
+ {questionHint}
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <button
146
+ className='govuk-button govuk-!-margin-top-1 govuk-!-font-size-16'
147
+ data-module='govuk-button'
148
+ onClick={onRequestChat}
149
+ >
150
+ Request chat
151
+ </button>
152
+ </form>
153
+ </div>
154
+ </>
155
+ )
156
+ }