@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
package/dist/server.js CHANGED
@@ -1,286 +1 @@
1
- /******/ (() => { // webpackBootstrap
2
- /******/ var __webpack_modules__ = ({
3
-
4
- /***/ "./src/server/index.js":
5
- /*!*****************************!*\
6
- !*** ./src/server/index.js ***!
7
- \*****************************/
8
- /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
9
-
10
- const {
11
- authenticate,
12
- getApiBaseUrl,
13
- getIsOpen,
14
- getActivity
15
- } = __webpack_require__(/*! ./lib/client.js */ "./src/server/lib/client.js");
16
-
17
- /**
18
- * Returns webchat availability
19
- * @param options {object}
20
- * @param options.clientId {string}
21
- * @param options.clientSecret {string}
22
- * @param options.accessKey {string}
23
- * @param options.accessSecret {string}
24
- * @param options.skillEndpoint {string}
25
- * @param options.hoursEndpoint {string}
26
- * @param options.authenticationUri {string}
27
- * @param options.wellKnownUri {string}
28
- * @param options.maxQueueCount {string}
29
- * @returns {Promise<{date: Date, availability: (string)}>}
30
- */
31
- module.exports = async function getAvailability({
32
- clientId,
33
- clientSecret,
34
- accessKey,
35
- accessSecret,
36
- maxQueueCount,
37
- skillEndpoint,
38
- hoursEndpoint,
39
- wellKnownUri = 'https://cxone.niceincontact.com/.well-known/cxone-configuration',
40
- authenticationUri = 'https://cxone.niceincontact.com/auth/token'
41
- }) {
42
- const authorisation = 'Basic ' + Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString('base64');
43
-
44
- // Cache authentication and re-authenticate when needed (lasts 1 hour?)
45
- const {
46
- tenantId,
47
- token,
48
- tokenType
49
- } = await authenticate({
50
- authenticationUri,
51
- authorisation,
52
- accessKey,
53
- accessSecret
54
- });
55
- const apiBaseUrl = await getApiBaseUrl({
56
- wellKnownUri,
57
- tenantId
58
- });
59
- const [{
60
- hasCapacity,
61
- hasAgentsAvailable
62
- }, isOpen] = await Promise.all([getActivity({
63
- baseUrl: apiBaseUrl,
64
- tokenType,
65
- token,
66
- skillEndpoint,
67
- maxQueueCount
68
- }), getIsOpen({
69
- baseUrl: apiBaseUrl,
70
- token,
71
- tokenType,
72
- hoursEndpoint
73
- })]);
74
- const isAvailable = isOpen && hasAgentsAvailable && hasCapacity;
75
- const isExistingOnly = isOpen && hasAgentsAvailable && !hasCapacity;
76
- let availability = 'UNAVAILABLE';
77
- if (isAvailable) {
78
- availability = 'AVAILABLE';
79
- } else if (isExistingOnly) {
80
- availability = 'EXISTING';
81
- }
82
- return {
83
- date: new Date(),
84
- availability
85
- };
86
- };
87
-
88
- /***/ }),
89
-
90
- /***/ "./src/server/lib/client.js":
91
- /*!**********************************!*\
92
- !*** ./src/server/lib/client.js ***!
93
- \**********************************/
94
- /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
95
-
96
- const querystring = __webpack_require__(/*! querystring */ "querystring");
97
- const axios = __webpack_require__(/*! axios */ "axios");
98
- const jwtdecode = __webpack_require__(/*! jwt-decode */ "jwt-decode");
99
- const {
100
- isWithinHours
101
- } = __webpack_require__(/*! ./utils.js */ "./src/server/lib/utils.js");
102
- const contentType = 'application/x-www-form-urlencoded';
103
- const authenticate = async ({
104
- authenticationUri,
105
- authorisation,
106
- accessKey,
107
- accessSecret
108
- }) => {
109
- const url = new URL(authenticationUri);
110
- const body = querystring.stringify({
111
- grant_type: 'password',
112
- username: accessKey,
113
- password: accessSecret
114
- });
115
- const auth = await axios.post(url.href, body, {
116
- signal: AbortSignal.timeout(3000),
117
- headers: {
118
- Host: url.host,
119
- 'Content-Type': contentType,
120
- Authorization: authorisation
121
- }
122
- });
123
- return {
124
- token: auth.data.access_token,
125
- tokenType: auth.data.token_type,
126
- tenantId: jwtdecode(auth.data.id_token)?.tenantId
127
- };
128
- };
129
- const getApiBaseUrl = async ({
130
- wellKnownUri,
131
- tenantId
132
- }) => {
133
- const url = new URL(wellKnownUri);
134
- url.searchParams.set('tenantId', tenantId);
135
- const {
136
- data
137
- } = await axios.get(url.href, {
138
- headers: {
139
- Host: url.host
140
- },
141
- signal: AbortSignal.timeout(3000)
142
- });
143
- return data.api_endpoint;
144
- };
145
- const getActivity = async ({
146
- tokenType,
147
- token,
148
- baseUrl,
149
- skillEndpoint,
150
- maxQueueCount
151
- }) => {
152
- const url = new URL(skillEndpoint, baseUrl);
153
- const skill = await axios.get(url.href, {
154
- signal: AbortSignal.timeout(3000),
155
- headers: {
156
- Host: url.host,
157
- Authorization: `${tokenType} ${token}`,
158
- 'Content-Type': contentType
159
- }
160
- });
161
- const activity = skill.data.skillActivity[0];
162
- return {
163
- hasCapacity: activity.queueCount < maxQueueCount,
164
- hasAgentsAvailable: activity.agentsAvailable >= 1
165
- };
166
- };
167
- const getIsOpen = async ({
168
- baseUrl,
169
- token,
170
- tokenType,
171
- hoursEndpoint
172
- }) => {
173
- const url = new URL(hoursEndpoint, baseUrl);
174
- const hours = await axios.get(url.href, {
175
- signal: AbortSignal.timeout(3000),
176
- headers: {
177
- Host: url.host,
178
- Authorization: `${tokenType} ${token}`,
179
- 'Content-Type': contentType
180
- }
181
- });
182
- const days = hours.data.resultSet.hoursOfOperationProfiles[0].days;
183
- return isWithinHours(days);
184
- };
185
- module.exports = {
186
- authenticate,
187
- getApiBaseUrl,
188
- getIsOpen,
189
- getActivity
190
- };
191
-
192
- /***/ }),
193
-
194
- /***/ "./src/server/lib/utils.js":
195
- /*!*********************************!*\
196
- !*** ./src/server/lib/utils.js ***!
197
- \*********************************/
198
- /***/ ((module) => {
199
-
200
- const isWithinHours = (days, date = null) => {
201
- const now = date ? new Date(date) : new Date();
202
- const name = now.toLocaleDateString('en-GB', {
203
- weekday: 'long'
204
- });
205
- const day = days.find(d => d.day.toLowerCase() === name.toLowerCase());
206
- const dateItems = now.toLocaleDateString('en-GB').split('/');
207
- const open = `${dateItems[2]}-${dateItems[1]}-${dateItems[0]}T${day.openTime}`;
208
- const close = `${dateItems[2]}-${dateItems[1]}-${dateItems[0]}T${day.closeTime}`;
209
- return now.getTime() >= Date.parse(open) && now.getTime() <= Date.parse(close);
210
- };
211
- module.exports = {
212
- isWithinHours
213
- };
214
-
215
- /***/ }),
216
-
217
- /***/ "axios":
218
- /*!************************!*\
219
- !*** external "axios" ***!
220
- \************************/
221
- /***/ ((module) => {
222
-
223
- "use strict";
224
- module.exports = require("axios");
225
-
226
- /***/ }),
227
-
228
- /***/ "jwt-decode":
229
- /*!*****************************!*\
230
- !*** external "jwt-decode" ***!
231
- \*****************************/
232
- /***/ ((module) => {
233
-
234
- "use strict";
235
- module.exports = require("jwt-decode");
236
-
237
- /***/ }),
238
-
239
- /***/ "querystring":
240
- /*!******************************!*\
241
- !*** external "querystring" ***!
242
- \******************************/
243
- /***/ ((module) => {
244
-
245
- "use strict";
246
- module.exports = require("querystring");
247
-
248
- /***/ })
249
-
250
- /******/ });
251
- /************************************************************************/
252
- /******/ // The module cache
253
- /******/ var __webpack_module_cache__ = {};
254
- /******/
255
- /******/ // The require function
256
- /******/ function __webpack_require__(moduleId) {
257
- /******/ // Check if module is in cache
258
- /******/ var cachedModule = __webpack_module_cache__[moduleId];
259
- /******/ if (cachedModule !== undefined) {
260
- /******/ return cachedModule.exports;
261
- /******/ }
262
- /******/ // Create a new module (and put it into the cache)
263
- /******/ var module = __webpack_module_cache__[moduleId] = {
264
- /******/ // no module.id needed
265
- /******/ // no module.loaded needed
266
- /******/ exports: {}
267
- /******/ };
268
- /******/
269
- /******/ // Execute the module function
270
- /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
271
- /******/
272
- /******/ // Return the exports of the module
273
- /******/ return module.exports;
274
- /******/ }
275
- /******/
276
- /************************************************************************/
277
- /******/
278
- /******/ // startup
279
- /******/ // Load entry module and return exports
280
- /******/ // This entry module is referenced by other modules so it can't be inlined
281
- /******/ var __webpack_exports__ = __webpack_require__("./src/server/index.js");
282
- /******/ module.exports = __webpack_exports__;
283
- /******/
284
- /******/ })()
285
- ;
286
- //# sourceMappingURL=server.js.map
1
+ (()=>{var e={525:(e,t,n)=>{const{authenticate:o,getApiBaseUrl:a,getIsOpen:i,getActivity:s}=n(398);e.exports=async function({clientId:e,clientSecret:t,accessKey:n,accessSecret:r,maxQueueCount:c,skillEndpoint:u,hoursEndpoint:l,wellKnownUri:p="https://cxone.niceincontact.com/.well-known/cxone-configuration",authenticationUri:d="https://cxone.niceincontact.com/auth/token"}){const h="Basic "+Buffer.from(`${encodeURIComponent(e)}:${encodeURIComponent(t)}`).toString("base64"),{tenantId:m,token:y,tokenType:w}=await o({authenticationUri:d,authorisation:h,accessKey:n,accessSecret:r}),g=await a({wellKnownUri:p,tenantId:m}),[{hasCapacity:A,hasAgentsAvailable:k},f]=await Promise.all([s({baseUrl:g,tokenType:w,token:y,skillEndpoint:u,maxQueueCount:c}),i({baseUrl:g,token:y,tokenType:w,hoursEndpoint:l})]);let x="UNAVAILABLE";return f&&k&&A?x="AVAILABLE":f&&k&&!A&&(x="EXISTING"),{date:new Date,availability:x}}},398:(e,t,n)=>{const o=n(45),a=n(938),i=n(158).decode,{isWithinHours:s}=n(792),r="application/x-www-form-urlencoded";e.exports={authenticate:async({authenticationUri:e,authorisation:t,accessKey:n,accessSecret:s})=>{const c=new URL(e),u=o.stringify({grant_type:"password",username:n,password:s}),l=await a.post(c.href,u,{signal:AbortSignal.timeout(3e3),headers:{Host:c.host,"Content-Type":r,Authorization:t}});return{token:l.data.access_token,tokenType:l.data.token_type,tenantId:i(l.data.id_token)?.tenantId}},getApiBaseUrl:async({wellKnownUri:e,tenantId:t})=>{const n=new URL(e);n.searchParams.set("tenantId",t);const{data:o}=await a.get(n.href,{headers:{Host:n.host},signal:AbortSignal.timeout(3e3)});return o.api_endpoint},getIsOpen:async({baseUrl:e,token:t,tokenType:n,hoursEndpoint:o})=>{const i=new URL(o,e),c=(await a.get(i.href,{signal:AbortSignal.timeout(3e3),headers:{Host:i.host,Authorization:`${n} ${t}`,"Content-Type":r}})).data.resultSet.hoursOfOperationProfiles[0].days;return s(c)},getActivity:async({tokenType:e,token:t,baseUrl:n,skillEndpoint:o,maxQueueCount:i})=>{const s=new URL(o,n),c=(await a.get(s.href,{signal:AbortSignal.timeout(3e3),headers:{Host:s.host,Authorization:`${e} ${t}`,"Content-Type":r}})).data.skillActivity[0];return{hasCapacity:c.queueCount<i,hasAgentsAvailable:c.agentsAvailable>=1}}}},792:(e,t,n)=>{const{DateTime:o}=n(168),a=e=>Number(e.split(":")[0]);e.exports={isWithinHours:(e,t)=>{const n=t?o.fromISO(t):o.local();n.setZone("Europe/London");const i=t?new Date(t):new Date,s=i.toLocaleDateString("en-GB",{weekday:"long"}),r=i.toLocaleDateString("en-GB").split("/"),c=e.find((e=>e.day===s)),u=o.local(Number(r[2]),Number(r[1]),Number(r[0]),a(c.openTime)),l=o.local(Number(r[2]),Number(r[1]),Number(r[0]),a(c.closeTime));return n.diff(u).milliseconds>=0&&n.diff(l).milliseconds<=0}}},938:e=>{"use strict";e.exports=require("axios")},158:e=>{"use strict";e.exports=require("jwt-decode")},168:e=>{"use strict";e.exports=require("luxon")},45:e=>{"use strict";e.exports=require("querystring")}},t={},n=function n(o){var a=t[o];if(void 0!==a)return a.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}(525);module.exports=n})();
package/main.scss CHANGED
@@ -1,12 +1,17 @@
1
1
 
2
2
  // Required GOVUK Styles
3
- @import "govuk-frontend/govuk/core/typography";
4
- @import "govuk-frontend/govuk/utilities/visually-hidden";
5
-
6
- // Components
7
- @import "./src/client/components/availability/availability";
3
+ @import "govuk-frontend/dist/govuk/core/typography";
4
+ @import "govuk-frontend/dist/govuk/core/links";
5
+ @import "govuk-frontend/dist/govuk/utilities/visually-hidden";
6
+ @import "govuk-frontend/dist/govuk/components/button/button";
8
7
 
8
+ // (ITCSS)
9
+ @import "./src/client/scss/settings";
10
+ @import "./src/client/scss/tools";
11
+ @import "./src/client/scss/objects";
12
+ @import "./src/client/scss/utilities";
9
13
 
10
- .wc-scroll-padding {
11
- scroll-padding-bottom: calc(2rem + 20px);
12
- }
14
+ // React components
15
+ @import "./src/client/components/availability/availability";
16
+ @import "./src/client/components/panel/panel";
17
+ @import "./src/client/components/chat/chat";
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@defra/flood-webchat",
3
- "version": "0.0.1-beta.9",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "main": "dist/server.js",
6
6
  "browser": "dist/client.js",
7
7
  "scripts": {
8
8
  "test": "npm run lint && npm run unit-test",
9
9
  "dev": "(cd demo && webpack server)",
10
- "build": "webpack",
10
+ "build": "webpack --config webpack.prod.mjs",
11
11
  "unit-test": "jest",
12
12
  "lint": "npm run lint:js && npm run lint:scss",
13
13
  "lint:fix": "npm run lint:js -- --fix && npm run lint:scss -- --fix",
14
14
  "lint:js": "standard",
15
15
  "lint:scss": "stylelint \"**/*.scss\"",
16
- "prepare": "node -e \"try { require('husky').install() } catch (e) {if (e.code !== 'MODULE_NOT_FOUND') { throw e } }\""
16
+ "prepare": "husky install"
17
17
  },
18
18
  "standard": {
19
19
  "ignore": [
@@ -27,41 +27,48 @@
27
27
  "author": "DEFRA",
28
28
  "license": "OGL-UK-3.0",
29
29
  "devDependencies": {
30
- "@babel/core": "^7.22.10",
31
- "@babel/preset-env": "^7.22.10",
32
- "@babel/preset-react": "^7.22.15",
33
- "@testing-library/react": "^14.0.0",
34
- "@testing-library/user-event": "^14.5.1",
35
- "@types/jest": "^29.5.4",
30
+ "@babel/core": "^7.24.6",
31
+ "@babel/preset-env": "^7.24.6",
32
+ "@babel/preset-react": "^7.24.6",
33
+ "@testing-library/jest-dom": "^6.4.5",
34
+ "@testing-library/react": "^15.0.7",
35
+ "@testing-library/user-event": "^14.5.2",
36
+ "@types/jest": "^29.5.12",
36
37
  "babel-jest": "^29.6.4",
37
38
  "babel-loader": "^9.1.3",
38
- "core-js": "^2.6.11",
39
- "css-loader": "^6.8.1",
40
- "dotenv": "^16.3.1",
39
+ "core-js": "^3.37.1",
40
+ "css-loader": "^7.1.2",
41
+ "dotenv": "^16.4.5",
41
42
  "husky": "^8.0.3",
42
- "jest": "^29.6.3",
43
- "jest-environment-jsdom": "^29.6.3",
43
+ "jest": "^29.7.0",
44
+ "jest-environment-jsdom": "^29.7.0",
44
45
  "mini-css-extract-plugin": "^2.7.6",
45
46
  "node-cache": "^5.1.2",
46
47
  "node-sass": "^9.0.0",
47
48
  "sass": "^1.66.1",
48
- "sass-loader": "^13.3.2",
49
+ "sass-loader": "^14.2.1",
49
50
  "standard": "^17.1.0",
50
- "stylelint": "^15.10.3",
51
+ "stylelint": "^15.11.0",
51
52
  "stylelint-config-gds": "^1.0.0",
52
- "webpack": "^5.88.2",
53
+ "webpack": "^5.91.0",
53
54
  "webpack-cli": "^5.1.4",
54
- "webpack-dev-server": "^4.15.1",
55
+ "webpack-dev-server": "^5.0.4",
56
+ "webpack-merge": "^5.10.0",
55
57
  "webpack-node-externals": "^3.0.0"
56
58
  },
57
59
  "peerDependencies": {
58
- "govuk-frontend": "^4.0.0"
60
+ "govuk-frontend": "^5.4.0"
59
61
  },
60
62
  "dependencies": {
61
- "axios": "^1.5.0",
62
- "jwt-decode": "^3.1.2",
63
+ "@nice-devone/nice-cxone-chat-web-sdk": "^1.18.0",
64
+ "axios": "^1.7.2",
65
+ "html-react-parser": "^5.1.10",
66
+ "jwt-decode": "^4.0.0",
67
+ "lodash.debounce": "^4.0.8",
68
+ "luxon": "^3.4.3",
63
69
  "querystring": "^0.2.1",
64
70
  "react": "^18.2.0",
65
- "react-dom": "^18.2.0"
71
+ "react-dom": "^18.2.0",
72
+ "uuid": "^9.0.1"
66
73
  }
67
74
  }
@@ -1,70 +1,121 @@
1
- import React, { useEffect, useRef, useState } from 'react'
1
+ import React, { useEffect, useRef } from 'react'
2
+
2
3
  import { classnames } from '../../lib/classnames'
3
- import { useMessageThread, useWebchatOpenState } from '../../lib/external-stores'
4
+ import { Panel } from '../panel/panel.jsx'
5
+ import { SkipLink } from '../skip-link.jsx'
6
+
7
+ import { useApp } from '../../store/useApp'
8
+ import { historyPushState, historyReplaceState } from '../../lib/history.js'
9
+ import { LiveRegion } from '../live-region.jsx'
10
+
11
+ export function Availability () {
12
+ const { availability, isChatOpen, setChatVisibility, unseenCount, threadId, setUnseenCount, setInstigatorId, setLiveRegionText } = useApp()
4
13
 
5
- export function Availability (props) {
6
- const [isOpen, setOpen] = useWebchatOpenState()
7
- const [isFixed, setFixed] = useState(false)
8
14
  const buttonRef = useRef()
9
- const onClick = () => {
10
- setOpen(!isOpen)
15
+
16
+ const showUnseenCount = unseenCount > 0 && !isChatOpen
17
+
18
+ const messageText = unseenCount > 1 ? 'new messages' : 'new message'
19
+
20
+ const onClick = e => {
21
+ e.preventDefault()
22
+ setUnseenCount(0)
23
+ setChatVisibility(!isChatOpen)
24
+ setInstigatorId(e.target.id)
25
+
26
+ if (!isChatOpen) {
27
+ historyPushState()
28
+ } else {
29
+ historyReplaceState()
30
+ }
11
31
  }
32
+
12
33
  const onKeyDown = event => {
13
34
  if (event.key === ' ') {
14
35
  event.preventDefault()
15
36
  }
16
37
  }
38
+
17
39
  const onKeyUp = event => {
18
40
  if (event.key === ' ') {
19
- setOpen(!isOpen)
41
+ setUnseenCount(0)
42
+ setChatVisibility(!isChatOpen)
20
43
  }
21
44
  }
22
45
 
23
- const intersectionCallback = entries => {
24
- const [entry] = entries
25
- const isBelowFold = !entry.isIntersecting && entry.boundingClientRect.top > 0
26
- setFixed(!isOpen && isBelowFold)
27
- }
28
-
29
46
  useEffect(() => {
30
- const observer = new window.IntersectionObserver(intersectionCallback, {
31
- rootMargin: '35px'
32
- })
33
- const parentElement = buttonRef.current?.parentElement
34
- if (parentElement) {
35
- observer.observe(parentElement)
47
+ if (showUnseenCount) {
48
+ setLiveRegionText(`Floodline webchat - ${unseenCount} ${messageText}`)
36
49
  }
50
+
37
51
  return () => {
38
- if (parentElement) {
39
- observer.unobserve(parentElement)
40
- }
52
+ setLiveRegionText()
41
53
  }
42
- }, [buttonRef, isOpen])
54
+ }, [unseenCount, isChatOpen])
43
55
 
44
56
  useEffect(() => {
45
- document.documentElement.classList.toggle('wc-scroll-padding', isFixed)
46
- document.body.classList.toggle('wc-scroll-padding', isFixed)
47
- }, [isFixed])
57
+ const onScroll = () => {
58
+ if (!threadId) {
59
+ return
60
+ }
61
+
62
+ if (availability !== 'AVAILABLE') {
63
+ return
64
+ }
65
+
66
+ const rect = buttonRef.current.parentElement.getBoundingClientRect()
67
+
68
+ const isBelowFold = rect.top + 35 > (window.innerHeight || document.documentElement.clientHeight)
69
+ const isFixed = !isChatOpen && isBelowFold
70
+
71
+ document.documentElement.classList.toggle('wc-u-scroll-padding', isFixed)
72
+ document.body.classList.toggle('wc-u-scroll-padding', isFixed)
73
+ buttonRef.current.classList.toggle('wc-availability--fixed', isFixed)
74
+ }
75
+
76
+ document.addEventListener('scroll', onScroll)
77
+ onScroll()
78
+
79
+ return () => {
80
+ document.removeEventListener('scroll', onScroll)
81
+ }
82
+ }, [threadId, isChatOpen, availability])
48
83
 
49
- switch (props.availability) {
84
+ switch (availability) {
50
85
  case 'AVAILABLE':
51
86
  return (
52
- <div
53
- className={classnames('wc-availability', isFixed && 'wc-availability--fixed')}
54
- ref={buttonRef}
55
- >
56
- <div className='wc-availability__inner'>
57
- <a
58
- className='wc-availability__link'
59
- href='#webchat' role='button' draggable='false'
60
- onClick={onClick}
61
- onKeyUp={onKeyUp}
62
- onKeyDown={onKeyDown}
63
- >
64
- <AvailabilityContent />
65
- </a>
87
+ <>
88
+ <div
89
+ className={classnames('wc-availability', isChatOpen && 'wc-open')}
90
+ ref={buttonRef}
91
+ >
92
+ <div className='wc-availability__inner'>
93
+ <a
94
+ id='webchat-start-chat-link'
95
+ className='wc-availability__link'
96
+ href='#webchat'
97
+ draggable='false'
98
+ onClick={onClick}
99
+ onKeyUp={onKeyUp}
100
+ onKeyDown={onKeyDown}
101
+ role='button'
102
+ data-module='govuk-button'
103
+ >
104
+ {!threadId ? <>Start chat</> : <>Show chat</>}
105
+ {showUnseenCount
106
+ ? (
107
+ <span className='wc-availability__unseen'>
108
+ {unseenCount}
109
+ <span className='govuk-visually-hidden'> {messageText}</span>
110
+ </span>
111
+ )
112
+ : null}
113
+ </a>
114
+ </div>
66
115
  </div>
67
- </div>
116
+ {!isChatOpen ? <LiveRegion /> : <Panel />}
117
+ <SkipLink />
118
+ </>
68
119
  )
69
120
  case 'EXISTING':
70
121
  case 'UNAVAILABLE':
@@ -77,25 +128,3 @@ export function Availability (props) {
77
128
  )
78
129
  }
79
130
  }
80
-
81
- function AvailabilityContent () {
82
- const [thread] = useMessageThread()
83
- const unreadMessageCount = thread.filter(message => !message.read).length
84
- if (!thread.length) {
85
- return (
86
- <>
87
- Start Chat
88
- </>
89
- )
90
- }
91
- return (
92
- <>
93
- Show Chat {!!unreadMessageCount && (
94
- <>
95
- <span className='wc-availability__unseen'>{unreadMessageCount}</span>
96
- <span className='govuk-visually-hidden'> {unreadMessageCount === 1 ? 'new message' : 'new messages'}</span>
97
- </>
98
- )}
99
- </>
100
- )
101
- }
@@ -1,3 +1,8 @@
1
+ // stylelint-disable selector-max-id
2
+ #wc-availability {
3
+ margin-bottom: govuk-spacing(4);
4
+ }
5
+ // stylelint-enable selector-max-id
1
6
 
2
7
  .wc-availability__inner {
3
8
  max-width: 960px;
@@ -12,6 +17,10 @@
12
17
  right: 0;
13
18
  padding-left: 15px;
14
19
  padding-right: 15px;
20
+ border: 1px solid transparent;
21
+ @media (forced-colors: active) {
22
+ border-color: currentColor;
23
+ }
15
24
 
16
25
  @include mq ($from: 'tablet') {
17
26
  padding-left: 30px;
@@ -27,52 +36,31 @@
27
36
  }
28
37
 
29
38
  .wc-availability__link {
39
+ @extend %govuk-link;
40
+ display: inline-block;
30
41
  @include govuk-font($size: 19);
31
- position:relative;
32
- display:inline-block;
33
42
  margin-right: 5px;
34
- margin-bottom: 0;
35
- fill: govuk-colour('black');
36
43
 
37
- @include mq ($from: tablet) {
38
- margin-right: 10px;
44
+ &:visited:not(:hover):not(:focus) {
45
+ color: govuk-colour('blue');
39
46
  }
40
47
 
41
48
  .govuk-visually-hidden, .wc-availability__unseen {
42
49
  pointer-events: none;
43
50
  }
44
51
 
45
- &:hover {
46
- color: $govuk-link-hover-colour;
47
- }
48
-
49
- &:active {
50
- color: govuk-colour('black');
51
- }
52
-
53
- &:visited {
54
- color: $govuk-link-colour;
55
- }
56
-
57
- &:focus {
58
- box-shadow: none;
59
- background-color: transparent;
60
- text-decoration: underline;
61
- color: $govuk-focus-colour;
62
- }
63
-
64
- &:hover:not(:focus):not(:active) {
65
- color: $govuk-link-hover-colour;
66
-
67
- .wc-availability__unseen {
68
- background-color: govuk-colour('dark-blue');
69
- }
52
+ &:hover:not(:focus):not(:active) .wc-availability__unseen {
53
+ background-color: govuk-colour('dark-blue');
70
54
  }
71
55
  }
72
56
 
73
57
  .wc-availability__unseen {
74
58
  display: inline-block;
75
59
  position: relative;
60
+ border: 1px solid transparent;
61
+ @media (forced-colors: active) {
62
+ border-color: currentColor;
63
+ }
76
64
  border-radius: 10px;
77
65
  background-color: govuk-colour('black');
78
66
  color: govuk-colour('white');
@@ -86,4 +74,4 @@
86
74
  padding-left: 7px;
87
75
  padding-right: 7px;
88
76
  }
89
- }
77
+ }