@seamly/web-ui 18.1.1 → 18.3.1

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 (165) hide show
  1. package/build/dist/lib/index.debug.js +286 -99
  2. package/build/dist/lib/index.debug.min.js +1 -1
  3. package/build/dist/lib/index.debug.min.js.LICENSE.txt +84 -16
  4. package/build/dist/lib/index.js +4104 -3887
  5. package/build/dist/lib/index.min.js +1 -1
  6. package/build/dist/lib/standalone.js +4351 -4084
  7. package/build/dist/lib/standalone.min.js +1 -1
  8. package/build/dist/lib/style-guide.js +746 -641
  9. package/build/dist/lib/style-guide.min.js +1 -1
  10. package/build/dist/lib/styles.css +1 -1
  11. package/build/dist/lib/utils.js +0 -1
  12. package/build/dist/lib/utils.min.js +1 -1
  13. package/build/dist/translations/de-informal.js +0 -1
  14. package/build/dist/translations/de-informal.min.js +1 -1
  15. package/build/dist/translations/en.js +0 -1
  16. package/build/dist/translations/en.min.js +1 -1
  17. package/build/dist/translations/es-informal.js +0 -1
  18. package/build/dist/translations/es-informal.min.js +1 -1
  19. package/build/dist/translations/nl-formal.js +0 -1
  20. package/build/dist/translations/nl-formal.min.js +1 -1
  21. package/build/dist/translations/nl-informal.js +0 -1
  22. package/build/dist/translations/nl-informal.min.js +1 -1
  23. package/package.json +4 -2
  24. package/src/javascripts/api/index.js +9 -9
  25. package/src/javascripts/api/producer.js +8 -8
  26. package/src/javascripts/config.js +9 -11
  27. package/src/javascripts/domains/app/actions.js +25 -0
  28. package/src/javascripts/domains/app/index.js +3 -0
  29. package/src/javascripts/domains/config/actions.js +6 -0
  30. package/src/javascripts/domains/config/hooks.js +6 -0
  31. package/src/javascripts/domains/config/index.js +8 -0
  32. package/src/javascripts/domains/config/middleware.js +26 -0
  33. package/src/javascripts/domains/config/reducer.js +74 -0
  34. package/src/javascripts/domains/config/selectors.js +23 -0
  35. package/src/javascripts/domains/forms/actions.js +1 -1
  36. package/src/javascripts/domains/forms/hooks.js +10 -14
  37. package/src/javascripts/domains/forms/provider.js +4 -6
  38. package/src/javascripts/domains/forms/selectors.js +3 -3
  39. package/src/javascripts/domains/i18n/index.js +9 -5
  40. package/src/javascripts/domains/interrupt/actions.js +6 -0
  41. package/src/javascripts/domains/interrupt/hooks.js +29 -0
  42. package/src/javascripts/domains/interrupt/index.js +9 -0
  43. package/src/javascripts/domains/interrupt/middleware.js +30 -0
  44. package/src/javascripts/domains/interrupt/reducer.js +22 -0
  45. package/src/javascripts/domains/interrupt/selectors.js +5 -0
  46. package/src/javascripts/domains/options/index.js +1 -0
  47. package/src/javascripts/domains/options/middleware.js +35 -0
  48. package/src/javascripts/domains/redux/create-redux-store.js +14 -6
  49. package/src/javascripts/domains/redux/hooks.js +2 -2
  50. package/src/javascripts/domains/redux/index.js +2 -1
  51. package/src/javascripts/domains/redux/provider.js +5 -0
  52. package/src/javascripts/domains/store/index.js +38 -0
  53. package/src/javascripts/{ui → domains}/store/state-reducer.js +4 -7
  54. package/src/javascripts/domains/translations/actions.js +3 -3
  55. package/src/javascripts/domains/translations/components/chat-status.js +6 -12
  56. package/src/javascripts/domains/translations/components/options-button.js +3 -3
  57. package/src/javascripts/domains/translations/components/options-dialog/form.js +2 -2
  58. package/src/javascripts/domains/translations/components/options-dialog/index.js +2 -5
  59. package/src/javascripts/domains/translations/hooks.js +1 -1
  60. package/src/javascripts/domains/translations/reducer.js +2 -2
  61. package/src/javascripts/domains/translations/selectors.js +2 -2
  62. package/src/javascripts/index.js +17 -5
  63. package/src/javascripts/lib/css.js +5 -5
  64. package/src/javascripts/lib/engine/index.js +38 -11
  65. package/src/javascripts/lib/external-api/index.js +6 -6
  66. package/src/javascripts/lib/i18n.js +2 -2
  67. package/src/javascripts/lib/parse-body.js +1 -1
  68. package/src/javascripts/lib/redux-helpers/index.js +18 -4
  69. package/src/javascripts/lib/split-url-params.js +2 -2
  70. package/src/javascripts/lib/store/providers/app-storage.js +1 -1
  71. package/src/javascripts/lib/store/providers/cookie-storage.js +1 -1
  72. package/src/javascripts/package/utils.js +0 -1
  73. package/src/javascripts/style-guide/components/app.js +5 -12
  74. package/src/javascripts/style-guide/components/links.js +6 -6
  75. package/src/javascripts/style-guide/components/static-core.js +23 -7
  76. package/src/javascripts/style-guide/state-helpers/index.js +1 -1
  77. package/src/javascripts/style-guide/states.js +27 -67
  78. package/src/javascripts/style-guide/style-guide-engine.js +1 -1
  79. package/src/javascripts/ui/components/chat-app.js +2 -2
  80. package/src/javascripts/ui/components/conversation/component-filter.js +2 -2
  81. package/src/javascripts/ui/components/conversation/conversation.js +2 -2
  82. package/src/javascripts/ui/components/conversation/event/card-component.js +29 -4
  83. package/src/javascripts/ui/components/conversation/event/carousel-component/components/pagination.js +2 -2
  84. package/src/javascripts/ui/components/conversation/event/carousel-component/index.js +4 -3
  85. package/src/javascripts/ui/components/conversation/event/carousel-message/components/slide.js +2 -1
  86. package/src/javascripts/ui/components/conversation/event/carousel-message/index.js +2 -2
  87. package/src/javascripts/ui/components/conversation/event/choice-prompt.js +11 -6
  88. package/src/javascripts/ui/components/conversation/event/cta.js +1 -6
  89. package/src/javascripts/ui/components/conversation/event/divider/variants/new-translation.js +1 -1
  90. package/src/javascripts/ui/components/conversation/event/event-participant.js +3 -5
  91. package/src/javascripts/ui/components/conversation/event/hooks/use-event-link-click-handler.js +2 -2
  92. package/src/javascripts/ui/components/conversation/event/hooks/use-formatted-date.js +3 -3
  93. package/src/javascripts/ui/components/conversation/event/hooks/use-text-rendering.js +3 -3
  94. package/src/javascripts/ui/components/conversation/event/participant.js +2 -2
  95. package/src/javascripts/ui/components/conversation/event/upload.js +12 -27
  96. package/src/javascripts/ui/components/conversation/message-container.js +4 -6
  97. package/src/javascripts/ui/components/core/seamly-activity-monitor.js +4 -5
  98. package/src/javascripts/ui/components/core/seamly-core.js +6 -7
  99. package/src/javascripts/ui/components/core/seamly-event-subscriber.js +16 -17
  100. package/src/javascripts/ui/components/core/seamly-file-upload.js +5 -6
  101. package/src/javascripts/ui/components/core/seamly-idle-detach-counter.js +2 -6
  102. package/src/javascripts/ui/components/core/seamly-initializer.js +7 -60
  103. package/src/javascripts/ui/components/core/seamly-instance-functions-loader.js +10 -10
  104. package/src/javascripts/ui/components/core/seamly-live-region.js +1 -1
  105. package/src/javascripts/ui/components/core/seamly-new-notifications.js +1 -1
  106. package/src/javascripts/ui/components/core/seamly-read-state.js +2 -2
  107. package/src/javascripts/ui/components/entry/entry-container.js +7 -10
  108. package/src/javascripts/ui/components/entry/toggle-button.js +24 -10
  109. package/src/javascripts/ui/components/entry/upload/index.js +4 -11
  110. package/src/javascripts/ui/components/faq/faq.js +4 -4
  111. package/src/javascripts/ui/components/form-controls/error.js +22 -0
  112. package/src/javascripts/ui/components/form-controls/file-input.js +3 -9
  113. package/src/javascripts/ui/components/form-controls/select.js +1 -1
  114. package/src/javascripts/ui/components/form-controls/wrapper.js +2 -9
  115. package/src/javascripts/ui/components/layout/agent-info.js +4 -4
  116. package/src/javascripts/ui/components/layout/app-frame.js +5 -5
  117. package/src/javascripts/ui/components/layout/chat-frame.js +3 -5
  118. package/src/javascripts/ui/components/layout/header.js +4 -18
  119. package/src/javascripts/ui/components/layout/privacy-disclaimer.js +2 -2
  120. package/src/javascripts/ui/components/options/cobrowsing.js +3 -7
  121. package/src/javascripts/ui/components/options/options-button.js +9 -13
  122. package/src/javascripts/ui/components/options/options-frame.js +1 -1
  123. package/src/javascripts/ui/components/options/transcript/index.js +2 -2
  124. package/src/javascripts/ui/components/options/transcript/transcript-form.js +1 -1
  125. package/src/javascripts/ui/components/warnings/cobrowsing-active-frame.js +3 -6
  126. package/src/javascripts/ui/components/warnings/idle-detach-warning.js +2 -6
  127. package/src/javascripts/ui/components/widgets/in-out-transition.js +2 -2
  128. package/src/javascripts/ui/components/widgets/lightbox.js +4 -4
  129. package/src/javascripts/ui/components/widgets/modal.js +3 -3
  130. package/src/javascripts/ui/components/widgets/upload-progress.js +2 -13
  131. package/src/javascripts/ui/hooks/component-helper-hooks.js +4 -15
  132. package/src/javascripts/ui/hooks/file-upload-hooks.js +3 -3
  133. package/src/javascripts/ui/hooks/focus-helper-hooks.js +4 -4
  134. package/src/javascripts/ui/hooks/live-region-hooks.js +2 -2
  135. package/src/javascripts/ui/hooks/seamly-entry-hooks.js +7 -6
  136. package/src/javascripts/ui/hooks/seamly-hooks.js +3 -9
  137. package/src/javascripts/ui/hooks/seamly-option-hooks.js +4 -4
  138. package/src/javascripts/ui/hooks/seamly-state-hooks.js +8 -16
  139. package/src/javascripts/ui/hooks/use-event-component-mapping.js +1 -1
  140. package/src/javascripts/ui/hooks/use-seamly-chat.js +1 -0
  141. package/src/javascripts/ui/hooks/use-seamly-commands.js +27 -49
  142. package/src/javascripts/ui/hooks/use-seamly-idle-detach-countdown.js +3 -3
  143. package/src/javascripts/ui/hooks/use-seamly-stored-visibility.js +3 -3
  144. package/src/javascripts/ui/hooks/use-seamly-visibility.js +3 -3
  145. package/src/javascripts/ui/hooks/utility-hooks.js +2 -2
  146. package/src/javascripts/ui/utils/form-utils.js +3 -3
  147. package/src/javascripts/ui/utils/general-utils.js +17 -11
  148. package/src/javascripts/ui/utils/seamly-utils.js +15 -83
  149. package/src/javascripts/ui/utils/validations.js +10 -7
  150. package/src/stylesheets/5-components/_card.scss +0 -1
  151. package/src/stylesheets/5-components/_choice-prompt.scss +5 -0
  152. package/src/stylesheets/5-components/_message.scss +10 -0
  153. package/src/stylesheets/5-components/_options.scss +3 -2
  154. package/translations/de-informal.js +0 -2
  155. package/translations/en.js +0 -2
  156. package/translations/es-informal.js +0 -2
  157. package/translations/nl-formal.js +0 -2
  158. package/translations/nl-informal.js +0 -2
  159. package/webpack/config.common.js +3 -3
  160. package/webpack/config.package.js +4 -4
  161. package/webpack/config.site.js +8 -6
  162. package/CHANGELOG.md +0 -551
  163. package/src/javascripts/ui/components/core/seamly-api.js +0 -44
  164. package/src/javascripts/ui/hooks/use-seamly-interrupt.js +0 -62
  165. package/src/javascripts/ui/store/index.js +0 -37
@@ -1,11 +1,9 @@
1
1
  import { useContext, useEffect, useRef } from 'preact/hooks'
2
2
  import {
3
- useSeamlyConfig,
4
3
  useSeamlyCommands,
5
4
  useSeamlyVisibility,
6
5
  useSeamlyUnreadCount,
7
6
  useSeamlyLayoutMode,
8
- useSeamlyInterrupt,
9
7
  useSeamlyConversationUrl,
10
8
  useSeamlyActivityEventHandler,
11
9
  useSeamlyApiContext,
@@ -13,6 +11,8 @@ import {
13
11
  import { visibilityStates } from '../../utils/seamly-utils'
14
12
  import { SeamlyEventBusContext } from './seamly-api-context'
15
13
  import { useTranslations } from '../../../domains/translations'
14
+ import { useInterrupt } from '../../../domains/interrupt'
15
+ import { useConfig } from '../../../domains/config'
16
16
 
17
17
  function useSeamlyInstanceFunction(functionName, fn, deps = []) {
18
18
  const eventBus = useContext(SeamlyEventBusContext)
@@ -32,7 +32,7 @@ function useSeamlyInstanceFunction(functionName, fn, deps = []) {
32
32
  }
33
33
 
34
34
  const SeamlyInstanceFunctionsLoader = () => {
35
- const config = useSeamlyConfig()
35
+ const config = useConfig()
36
36
  const { sendMessage, sendContext, sendAction } = useSeamlyCommands()
37
37
  const { setVisibility, visible } = useSeamlyVisibility()
38
38
  const currentVisibility = useRef(visible)
@@ -42,7 +42,7 @@ const SeamlyInstanceFunctionsLoader = () => {
42
42
  const previousUnreadCount = useRef(null)
43
43
  const previousVisibilityState = useRef(null)
44
44
  const { isInline, isResolving } = useSeamlyLayoutMode()
45
- const { hasInterrupt } = useSeamlyInterrupt()
45
+ const { hasInterrupt } = useInterrupt()
46
46
  const currentConversationUrl = useSeamlyConversationUrl()
47
47
  const prevConversationUrl = useRef(null)
48
48
  const onActivityHandler = useSeamlyActivityEventHandler()
@@ -54,26 +54,26 @@ const SeamlyInstanceFunctionsLoader = () => {
54
54
 
55
55
  useSeamlyInstanceFunction(
56
56
  'askText',
57
- text => {
57
+ (text) => {
58
58
  sendMessage({ body: text })
59
59
  },
60
60
  [api?.send],
61
61
  )
62
62
  useSeamlyInstanceFunction(
63
63
  'setLocale',
64
- locale => {
64
+ (locale) => {
65
65
  sendContext({ locale })
66
66
  },
67
67
  [api?.send],
68
68
  )
69
69
  useSeamlyInstanceFunction(
70
70
  'setVariables',
71
- variables => {
71
+ (variables) => {
72
72
  sendContext({ variables })
73
73
  },
74
74
  [api?.send],
75
75
  )
76
- useSeamlyInstanceFunction('getVisibility', callback => {
76
+ useSeamlyInstanceFunction('getVisibility', (callback) => {
77
77
  if (callback) {
78
78
  callback(currentVisibility.current)
79
79
  } else {
@@ -84,7 +84,7 @@ const SeamlyInstanceFunctionsLoader = () => {
84
84
  })
85
85
  useSeamlyInstanceFunction(
86
86
  'setVisibility',
87
- visibilityState => {
87
+ (visibilityState) => {
88
88
  if (!Object.values(visibilityStates).includes(visibilityState)) {
89
89
  console.error(
90
90
  'Requested visibility states should be "open", "minimized" ,"hidden" or null.',
@@ -102,7 +102,7 @@ const SeamlyInstanceFunctionsLoader = () => {
102
102
  setVisibility(visibilityState)
103
103
  }
104
104
  },
105
- [api?.send],
105
+ [config?.api],
106
106
  )
107
107
  useSeamlyInstanceFunction(
108
108
  'sendCustomAction',
@@ -16,7 +16,7 @@ const SeamlyLiveRegion = ({ children }) => {
16
16
  const isMounted = useRef(true)
17
17
 
18
18
  const messageSetter = useCallback(
19
- state => {
19
+ (state) => {
20
20
  if (isMounted.current) {
21
21
  setMessage(state)
22
22
  eventBus.emit('aria-live', state)
@@ -21,7 +21,7 @@ const SeamlyNewNotifications = () => {
21
21
  const debounceFunc = useRef(null)
22
22
 
23
23
  const notifyUnread = useCallback(
24
- debounce(eventArray => {
24
+ debounce((eventArray) => {
25
25
  const serverEventCount = eventArray.filter(
26
26
  ({ payload }) => !payload.fromClient && !payload.fromHistory,
27
27
  ).length
@@ -40,13 +40,13 @@ const SeamlyReadState = () => {
40
40
  }
41
41
 
42
42
  const unread = events
43
- .filter(event => {
43
+ .filter((event) => {
44
44
  return (
45
45
  isUnreadMessage(event) &&
46
46
  event.payload.messageStatus === readStates.received
47
47
  )
48
48
  })
49
- .map(event => event.payload.id)
49
+ .map((event) => event.payload.id)
50
50
 
51
51
  if (unread.length > 0) {
52
52
  dispatch({ type: SET_EVENTS_READ, ids: unread })
@@ -5,7 +5,6 @@ import {
5
5
  useSeamlyResumeConversationPrompt,
6
6
  useSkiplinkTargetFocusing,
7
7
  useSeamlyEntry,
8
- useSeamlyConfig,
9
8
  useFileUploadMeta,
10
9
  } from '../../hooks/seamly-hooks'
11
10
  import { entryTypes } from '../../utils/seamly-utils'
@@ -15,18 +14,16 @@ import ResumeConversationPrompt from '../warnings/resume-conversation-prompt'
15
14
  import TextEntry from './text-entry'
16
15
  import UploadToggle from './upload-toggle'
17
16
  import Upload from './upload'
17
+ import { useConfig } from '../../../domains/config'
18
18
 
19
19
  const EntryContainer = () => {
20
20
  const entryContainer = useRef(null)
21
21
  const { hasCountdown } = useSeamlyIdleDetachCountdown()
22
22
  const [showCountdown, setShowCountDown] = useState(hasCountdown)
23
- const {
24
- hasPrompt: hasResumeConversationPrompt,
25
- } = useSeamlyResumeConversationPrompt()
26
- const [
27
- showResumeConversationPrompt,
28
- setShowResumeConversationPrompt,
29
- ] = useState(hasResumeConversationPrompt)
23
+ const { hasPrompt: hasResumeConversationPrompt } =
24
+ useSeamlyResumeConversationPrompt()
25
+ const [showResumeConversationPrompt, setShowResumeConversationPrompt] =
26
+ useState(hasResumeConversationPrompt)
30
27
  const focusSkiplinkTarget = useSkiplinkTargetFocusing()
31
28
  const containedFocus = useRef(false)
32
29
  const { activeEntry } = useSeamlyEntry()
@@ -35,7 +32,7 @@ const EntryContainer = () => {
35
32
  upload: Upload,
36
33
  })
37
34
  const [renderEntry, setRenderEntry] = useState(() => activeEntry)
38
- const config = useSeamlyConfig()
35
+ const config = useConfig()
39
36
  const { accountAllowsUploads } = useFileUploadMeta()
40
37
 
41
38
  const focusFn = useCallback(() => {
@@ -49,7 +46,7 @@ const EntryContainer = () => {
49
46
  const { entry } = customComponents || {}
50
47
 
51
48
  if (entry) {
52
- setEntryComponents(c => ({
49
+ setEntryComponents((c) => ({
53
50
  ...c,
54
51
  ...entry,
55
52
  }))
@@ -7,9 +7,10 @@ import {
7
7
  useFocusIfSeamlyContainedFocus,
8
8
  useSeamlyCurrentAgent,
9
9
  useSeamlyHeaderData,
10
- useSeamlyInterrupt,
11
10
  useGeneratedId,
11
+ useSeamlyStateContext,
12
12
  } from '../../hooks/seamly-hooks'
13
+ import { useInterrupt } from '../../../domains/interrupt'
13
14
 
14
15
  const ToggleButton = ({ onOpenChat }) => {
15
16
  const { t } = useI18n()
@@ -17,11 +18,13 @@ const ToggleButton = ({ onOpenChat }) => {
17
18
  const { isOpen } = useSeamlyVisibility()
18
19
  const prevIsOpen = useRef(null)
19
20
  const buttonRef = useRef(null)
21
+ const lastEventRef = useRef()
20
22
  const focusSkiplinkTarget = useSkiplinkTargetFocusing()
21
23
  const focusIfContained = useFocusIfSeamlyContainedFocus()
22
24
  const currentAgent = useSeamlyCurrentAgent()
23
25
  const agentSubtitle = useSeamlyHeaderData().subTitle
24
- const { hasInterrupt } = useSeamlyInterrupt()
26
+ const { hasInterrupt } = useInterrupt()
27
+ const { headerCollapseButtonId } = useSeamlyStateContext()
25
28
 
26
29
  const showAgentInfo = currentAgent && !hasInterrupt
27
30
 
@@ -35,13 +38,23 @@ const ToggleButton = ({ onOpenChat }) => {
35
38
  prevIsOpen.current = isOpen
36
39
  }, [isOpen, focusIfContained])
37
40
 
38
- const onMouseUpHandler = () => {
39
- // Sets focus on the input when opening through mouse interaction.
40
- // This avoids focus hijacking for keyboard users.
41
- // TODO: function is executed before the component is rendered, needs to be fixed.
42
- focusSkiplinkTarget()
41
+ const handleMouseUp = () => {
42
+ lastEventRef.current = 'mouse'
43
+ }
44
+ const handleKeyUp = () => {
45
+ lastEventRef.current = 'key'
46
+ }
47
+ const handleClick = () => {
48
+ onOpenChat()
49
+ if (lastEventRef.current === 'mouse') {
50
+ // Sets focus on the input when opening through mouse interaction.
51
+ // This avoids focus hijacking for keyboard users.
52
+ // TODO: function is executed before the component is rendered, needs to be fixed.
53
+ focusSkiplinkTarget()
54
+ } else if (lastEventRef.current === 'key') {
55
+ focusIfContained(headerCollapseButtonId)
56
+ }
43
57
  }
44
-
45
58
  return (
46
59
  <div className={className('toggle-button')}>
47
60
  <div id={titleId}>
@@ -59,9 +72,10 @@ const ToggleButton = ({ onOpenChat }) => {
59
72
  type="button"
60
73
  aria-labelledby={titleId}
61
74
  className={className('toggle-button__button')}
62
- onClick={onOpenChat}
63
75
  ref={buttonRef}
64
- onMouseUp={onMouseUpHandler}
76
+ onMouseUp={handleMouseUp}
77
+ onKeyUp={handleKeyUp}
78
+ onClick={handleClick}
65
79
  />
66
80
  </div>
67
81
  )
@@ -30,11 +30,8 @@ const Upload = () => {
30
30
  const skiplinkTargetId = useSkiplink()
31
31
  const focusSkiplinkTarget = useSkiplinkTargetFocusing()
32
32
  // This hook should be refactored at some point
33
- const {
34
- serviceAllowsUploads,
35
- allowedMimeTypes,
36
- maxSize,
37
- } = useFileUploadMeta()
33
+ const { serviceAllowsUploads, allowedMimeTypes, maxSize } =
34
+ useFileUploadMeta()
38
35
  const cancelButtonRef = useRef(null)
39
36
  const canUpload = useRef(serviceAllowsUploads)
40
37
 
@@ -43,12 +40,8 @@ const Upload = () => {
43
40
 
44
41
  const hasError = false
45
42
 
46
- const {
47
- selectedFileName,
48
- uploadHandle,
49
- hasServerError,
50
- progress,
51
- } = useSingleFileUpload(formName, fileInputName)
43
+ const { selectedFileName, uploadHandle, hasServerError, progress } =
44
+ useSingleFileUpload(formName, fileInputName)
52
45
  const notificationId = useGeneratedId()
53
46
  const prevIsComplete = useRef(true)
54
47
 
@@ -12,7 +12,6 @@ import { useGeneratedId } from '../../hooks/utility-hooks'
12
12
  import { useSkiplinkTargetFocusing } from '../../hooks/focus-helper-hooks'
13
13
  import { useSeamlyHasUserResponded } from '../../hooks/seamly-api-hooks'
14
14
  import { useLiveRegion } from '../../hooks/live-region-hooks'
15
- import useSeamlyInterrupt from '../../hooks/use-seamly-interrupt'
16
15
  import useSeamlyIdleDetachCountdown from '../../hooks/use-seamly-idle-detach-countdown'
17
16
  import useSeamlyResumeConversationPrompt from '../../hooks/use-seamly-resume-conversation-prompt'
18
17
  import { useI18n } from '../../../domains/i18n'
@@ -20,6 +19,7 @@ import InOutTransition, {
20
19
  transitionStartStates,
21
20
  } from '../widgets/in-out-transition'
22
21
  import { useTranslatedEventData } from '../../../domains/translations'
22
+ import { useInterrupt } from '../../../domains/interrupt'
23
23
 
24
24
  const Faq = () => {
25
25
  const { t } = useI18n()
@@ -27,7 +27,7 @@ const Faq = () => {
27
27
  const sectionId = useGeneratedId()
28
28
  const focusSkiplinkTarget = useSkiplinkTargetFocusing()
29
29
  const { sendPolite } = useLiveRegion()
30
- const { hasInterrupt } = useSeamlyInterrupt()
30
+ const { hasInterrupt } = useInterrupt()
31
31
  const { hasCountdown, endCountdown } = useSeamlyIdleDetachCountdown()
32
32
  const { hasPrompt, continueChat } = useSeamlyResumeConversationPrompt()
33
33
 
@@ -42,7 +42,7 @@ const Faq = () => {
42
42
  classNames: [
43
43
  itemBaseClass,
44
44
  ...categories.map(
45
- cat =>
45
+ (cat) =>
46
46
  `faqs__item--${String(cat)
47
47
  .toLowerCase()
48
48
  .replace(/[^a-z0-9_\\-]/, '')}`,
@@ -138,7 +138,7 @@ const Faq = () => {
138
138
  )}
139
139
  {!!renderedFaqList.length && (
140
140
  <ul className={className('faqs__list')}>
141
- {renderedFaqList.map(faq => (
141
+ {renderedFaqList.map((faq) => (
142
142
  <li key={faq.id.toString()} className={className(faq.classNames)}>
143
143
  <button
144
144
  type="button"
@@ -0,0 +1,22 @@
1
+ import { useState, useEffect } from 'preact/hooks'
2
+ import { className } from '../../../lib/css'
3
+ import Icon from '../layout/icon'
4
+
5
+ export default function Error({ id, error }) {
6
+ const [isAvailable, setIsAvailable] = useState(false)
7
+ useEffect(() => {
8
+ const timerId = setTimeout(() => setIsAvailable(true), 300) // 300 = magic number, could be less or more
9
+ return () => clearTimeout(timerId) // clear timer if error is mounted+unmounted within 300
10
+ }, [])
11
+
12
+ return (
13
+ <div aria-live="assertive" aria-atomic="true">
14
+ {isAvailable && error && (
15
+ <span id={id} className={className('error')}>
16
+ <Icon name="error" size="16" />
17
+ {error}
18
+ </span>
19
+ )}
20
+ </div>
21
+ )
22
+ }
@@ -3,6 +3,7 @@ import { className } from '../../../lib/css'
3
3
  import { useGeneratedId } from '../../hooks/seamly-hooks'
4
4
  import { useFormControl, useFormContext } from '../../../domains/forms'
5
5
  import Icon from '../layout/icon'
6
+ import Error from './error'
6
7
 
7
8
  export default function FileInput({
8
9
  id,
@@ -38,7 +39,7 @@ export default function FileInput({
38
39
  }, [setFocusWithin, onBlur])
39
40
 
40
41
  const handleChange = useCallback(
41
- e => {
42
+ (e) => {
42
43
  const customEvent = {
43
44
  target: {
44
45
  value: e.target.files,
@@ -57,14 +58,7 @@ export default function FileInput({
57
58
  {contentHint}
58
59
  </span>
59
60
  )}
60
- <div aria-live="assertive" aria-atomic="true">
61
- {hasError && (
62
- <span id={errorId} className={className('error')}>
63
- <Icon name="error" size="16" />
64
- {error}
65
- </span>
66
- )}
67
- </div>
61
+ <Error id={errorId} error={hasError && error} />
68
62
  <div
69
63
  className={className([
70
64
  'file-upload',
@@ -43,7 +43,7 @@ const Select = ({
43
43
  {...field}
44
44
  {...restProps}
45
45
  >
46
- {options.map(option => (
46
+ {options.map((option) => (
47
47
  <option key={option.value} value={option.value}>
48
48
  {option.label}
49
49
  </option>
@@ -1,5 +1,5 @@
1
1
  import { className } from '../../../lib/css'
2
- import Icon from '../layout/icon'
2
+ import Error from './error'
3
3
 
4
4
  const FormControlWrapper = ({
5
5
  contentHint,
@@ -23,14 +23,7 @@ const FormControlWrapper = ({
23
23
  {contentHint}
24
24
  </span>
25
25
  )}
26
- <div aria-live="assertive" aria-atomic="true">
27
- {!validity && (
28
- <span id={`${id}-error`} className={className('error')}>
29
- <Icon name="error" size="16" />
30
- {errorText}
31
- </span>
32
- )}
33
- </div>
26
+ <Error id={`${id}-error`} error={!validity && errorText} />
34
27
  {children}
35
28
  </>
36
29
  )
@@ -4,11 +4,11 @@ import {
4
4
  useSeamlyUnreadCount,
5
5
  useSeamlyVisibility,
6
6
  useSeamlyCurrentAgent,
7
- useSeamlyInterrupt,
8
- useSeamlyConfig,
9
7
  } from '../../hooks/seamly-hooks'
10
8
  import { className } from '../../../lib/css'
11
9
  import { useI18n } from '../../../domains/i18n'
10
+ import { useInterrupt } from '../../../domains/interrupt'
11
+ import { useConfig } from '../../../domains/config'
12
12
 
13
13
  const AgentInfo = () => {
14
14
  const { t } = useI18n()
@@ -16,8 +16,8 @@ const AgentInfo = () => {
16
16
  const unreadMessageCount = useSeamlyUnreadCount()
17
17
  const { isOpen } = useSeamlyVisibility()
18
18
  const currentAgent = useSeamlyCurrentAgent()
19
- const { hasInterrupt } = useSeamlyInterrupt()
20
- const { defaults } = useSeamlyConfig()
19
+ const { hasInterrupt } = useInterrupt()
20
+ const { defaults } = useConfig()
21
21
 
22
22
  const { startChatIcon } = defaults || {}
23
23
 
@@ -2,7 +2,6 @@ import { useState, useEffect, useCallback } from 'preact/hooks'
2
2
  import { className } from '../../../lib/css'
3
3
  import {
4
4
  useSeamlyAppContainerClassNames,
5
- useSeamlyConfig,
6
5
  useSeamlyVisibility,
7
6
  useSeamlyLayoutMode,
8
7
  useSeamlyHasUserResponded,
@@ -10,11 +9,12 @@ import {
10
9
  } from '../../hooks/seamly-hooks'
11
10
  import Faq from '../faq/faq'
12
11
  import { visibilityStates } from '../../utils/seamly-utils'
12
+ import { useConfig } from '../../../domains/config'
13
13
 
14
14
  const AppFrame = ({ children }) => {
15
15
  const [, setSeamlyContainerElement] = useSeamlyContainerElement()
16
16
  const { isOpen, isVisible, setVisibility } = useSeamlyVisibility()
17
- const { context, zIndex, showFaq } = useSeamlyConfig()
17
+ const { context, zIndex, showFaq } = useConfig()
18
18
  const { isModal, isInline } = useSeamlyLayoutMode()
19
19
  const appContainerClassNames = useSeamlyAppContainerClassNames()
20
20
  const userResponded = useSeamlyHasUserResponded()
@@ -22,7 +22,7 @@ const AppFrame = ({ children }) => {
22
22
  const { locale } = context || {}
23
23
 
24
24
  const containerElementRef = useCallback(
25
- container => {
25
+ (container) => {
26
26
  setSeamlyContainerElement(container)
27
27
  },
28
28
  [setSeamlyContainerElement],
@@ -47,14 +47,14 @@ const AppFrame = ({ children }) => {
47
47
  classNames.push('app--user-responded')
48
48
  }
49
49
 
50
- const onKeyDownHandler = e => {
50
+ const onKeyDownHandler = (e) => {
51
51
  if ((e.code && e.code === 'Escape') || e.keyCode === 27)
52
52
  if (!isInline && isOpen) {
53
53
  setVisibility(visibilityStates.minimized)
54
54
  }
55
55
  }
56
56
 
57
- const onClickHandler = e => {
57
+ const onClickHandler = (e) => {
58
58
  if (isModal) {
59
59
  e.stopPropagation()
60
60
  }
@@ -1,14 +1,12 @@
1
1
  import { className } from '../../../lib/css'
2
- import {
3
- useSeamlyInterrupt,
4
- useSeamlyVisibility,
5
- } from '../../hooks/seamly-hooks'
2
+ import { useSeamlyVisibility } from '../../hooks/seamly-hooks'
6
3
  import CobrowsingActiveFrame from '../warnings/cobrowsing-active-frame'
7
4
  import AppOptions from '../app-options'
8
5
  import { ChatStatus as TranslationsChatStatus } from '../../../domains/translations'
6
+ import { useInterrupt } from '../../../domains/interrupt'
9
7
 
10
8
  function ChatFrame({ children, interruptComponent: InterruptComponent }) {
11
- const { hasInterrupt, meta } = useSeamlyInterrupt()
9
+ const { hasInterrupt, meta } = useInterrupt()
12
10
  const { isOpen } = useSeamlyVisibility()
13
11
 
14
12
  const getContent = () => {
@@ -1,27 +1,12 @@
1
- import { useLayoutEffect, useRef } from 'preact/hooks'
1
+ import { useRef } from 'preact/hooks'
2
2
  import { className } from '../../../lib/css'
3
3
  import Icon from './icon'
4
4
  import { useI18n } from '../../../domains/i18n'
5
- import {
6
- useSeamlyVisibility,
7
- useFocusIfSeamlyContainedFocus,
8
- } from '../../hooks/seamly-hooks'
5
+ import { useSeamlyStateContext } from '../../hooks/seamly-hooks'
9
6
 
10
7
  const Header = ({ children, onCloseChat }) => {
11
- const { isOpen } = useSeamlyVisibility()
12
- const prevIsOpen = useRef(null)
8
+ const { headerCollapseButtonId } = useSeamlyStateContext()
13
9
  const closeButton = useRef(null)
14
- const focusIfContained = useFocusIfSeamlyContainedFocus()
15
-
16
- useLayoutEffect(() => {
17
- // Because we can open the app from the external API we
18
- // need to determine if current keyboard focus resides inside
19
- // the Seamly app first otherwise focus will be hijacked
20
- if (isOpen && prevIsOpen.current === false) {
21
- focusIfContained(closeButton.current)
22
- }
23
- prevIsOpen.current = isOpen
24
- }, [isOpen, focusIfContained])
25
10
 
26
11
  const { t } = useI18n()
27
12
  return (
@@ -33,6 +18,7 @@ const Header = ({ children, onCloseChat }) => {
33
18
  className={className('button', 'header-controls__collapse')}
34
19
  onClick={onCloseChat}
35
20
  ref={closeButton}
21
+ id={headerCollapseButtonId}
36
22
  >
37
23
  <Icon name="chevronDown" size="32" alt={t('header.collapseApp')} />
38
24
  </button>
@@ -1,10 +1,10 @@
1
1
  import { className } from '../../../lib/css'
2
- import { useSeamlyDisclaimerState } from '../../hooks/seamly-hooks'
3
2
  import { useI18n } from '../../../domains/i18n'
3
+ import { useConfig } from '../../../domains/config'
4
4
 
5
5
  const PrivacyDisclaimer = () => {
6
6
  const { t } = useI18n()
7
- const showDisclaimer = useSeamlyDisclaimerState()
7
+ const { showDisclaimer } = useConfig()
8
8
 
9
9
  return (
10
10
  showDisclaimer && (
@@ -18,12 +18,8 @@ import {
18
18
  const Cobrowsing = () => {
19
19
  const { t } = useI18n()
20
20
  const cobrowsingDescriptionId = useGeneratedId()
21
- const {
22
- userSelectedOptions,
23
- features,
24
- setUserSelectedOption,
25
- hideOption,
26
- } = useSeamlyOptions()
21
+ const { userSelectedOptions, features, setUserSelectedOption, hideOption } =
22
+ useSeamlyOptions()
27
23
  const { enabled: canActivateCobrowsing } = features.cobrowsing || {}
28
24
  const prevCanActivateCobrowsing = useRef(null)
29
25
  const [cobrowsingToggleActive, setCobrowsingToggleActive] = useState(
@@ -53,7 +49,7 @@ const Cobrowsing = () => {
53
49
  prevCanActivateCobrowsing.current = canActivateCobrowsing
54
50
  }, [canActivateCobrowsing, setUserSelectedOption, sendAssertive, t])
55
51
 
56
- const toggleAndFocus = isCancel => {
52
+ const toggleAndFocus = (isCancel) => {
57
53
  if (!isCancel && !userSelectedOptions.cobrowsing) {
58
54
  focusContainer()
59
55
  } else {
@@ -12,12 +12,8 @@ import { getKey, keyNames, focusElement } from '../../utils/general-utils'
12
12
 
13
13
  const OptionsButton = () => {
14
14
  const { t } = useI18n()
15
- const {
16
- menuOptions,
17
- showOption,
18
- panelActive,
19
- hideOption,
20
- } = useSeamlyOptions()
15
+ const { menuOptions, showOption, panelActive, hideOption } =
16
+ useSeamlyOptions()
21
17
  const { id } = useOptionButton()
22
18
  const focusOutDelayTimeoutID = useRef(null)
23
19
 
@@ -48,7 +44,7 @@ const OptionsButton = () => {
48
44
  requestAnimationFrame(() => {
49
45
  requestAnimationFrame(() => {
50
46
  const firstActiveOptionIndex = menuOptions.findIndex(
51
- option => option.available,
47
+ (option) => option.available,
52
48
  )
53
49
  const focusIndex =
54
50
  firstActiveOptionIndex === -1 ? 0 : firstActiveOptionIndex
@@ -64,13 +60,13 @@ const OptionsButton = () => {
64
60
  hideOption()
65
61
  }
66
62
  if (multiMenu) {
67
- setMenuIsOpen(o => !o)
63
+ setMenuIsOpen((o) => !o)
68
64
  } else if (firstOption.available) {
69
65
  showOption(firstOption.name)
70
66
  }
71
67
  }
72
68
 
73
- const onMainKeyDownHandler = e => {
69
+ const onMainKeyDownHandler = (e) => {
74
70
  if (!menuIsOpen) {
75
71
  return
76
72
  }
@@ -89,7 +85,7 @@ const OptionsButton = () => {
89
85
  }
90
86
  }
91
87
 
92
- const onButtonKeyDownHandler = e => {
88
+ const onButtonKeyDownHandler = (e) => {
93
89
  if (getKey(e) === keyNames.ArrowDown) {
94
90
  setMenuIsOpen(true)
95
91
  e.preventDefault()
@@ -194,7 +190,7 @@ const OptionsButton = () => {
194
190
  >
195
191
  <button
196
192
  type="button"
197
- ref={item => {
193
+ ref={(item) => {
198
194
  menuItemButtons.current[i] = item
199
195
  }}
200
196
  className={className([
@@ -202,8 +198,8 @@ const OptionsButton = () => {
202
198
  'button--secondary',
203
199
  ...(available ? [] : ['button--disabled']),
204
200
  ])}
205
- onKeyDown={e => onMenuItemKeyDownHandler(e, i)}
206
- onKeyPress={e => onKeyPressHandler(e, i)}
201
+ onKeyDown={(e) => onMenuItemKeyDownHandler(e, i)}
202
+ onKeyPress={(e) => onKeyPressHandler(e, i)}
207
203
  onClick={() => onMenuItemClickHandler(name, available)}
208
204
  aria-disabled={!available ? 'true' : null}
209
205
  >
@@ -61,7 +61,7 @@ const OptionsFrame = ({
61
61
  onClick={onCancelHandler}
62
62
  aria-describedby={mainHeadingId}
63
63
  className={className('button', 'options__close')}
64
- ref={btn => {
64
+ ref={(btn) => {
65
65
  if (cancelButtonRef) {
66
66
  cancelButtonRef.current = btn
67
67
  }
@@ -38,8 +38,8 @@ const Transcript = () => {
38
38
  )
39
39
 
40
40
  const handleSubmit = useCallback(
41
- values => {
42
- const emailAddress = values[controlName]
41
+ (values) => {
42
+ const emailAddress = values[controlName].trim()
43
43
  sendAction({
44
44
  type: actionTypes.sendTranscript,
45
45
  body: { emailAddress },