@seamly/web-ui 18.2.0 → 19.0.0-beta.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 (194) hide show
  1. package/build/dist/lib/index.debug.js +598 -136
  2. package/build/dist/lib/index.debug.min.js +1 -1
  3. package/build/dist/lib/index.debug.min.js.LICENSE.txt +190 -22
  4. package/build/dist/lib/index.js +4745 -4468
  5. package/build/dist/lib/index.min.js +1 -1
  6. package/build/dist/lib/index.min.js.LICENSE.txt +1 -1
  7. package/build/dist/lib/standalone.js +4839 -4465
  8. package/build/dist/lib/standalone.min.js +1 -1
  9. package/build/dist/lib/standalone.min.js.LICENSE.txt +1 -1
  10. package/build/dist/lib/style-guide.js +1770 -980
  11. package/build/dist/lib/style-guide.min.js +1 -1
  12. package/build/dist/lib/styles.css +1 -1
  13. package/build/dist/lib/utils.js +0 -1
  14. package/build/dist/lib/utils.min.js +1 -1
  15. package/package.json +29 -29
  16. package/src/javascripts/api/index.js +33 -48
  17. package/src/javascripts/api/producer.js +9 -12
  18. package/src/javascripts/config.js +9 -11
  19. package/src/javascripts/domains/app/actions.js +43 -0
  20. package/src/javascripts/domains/app/hooks.js +6 -0
  21. package/src/javascripts/domains/app/index.js +6 -0
  22. package/src/javascripts/domains/app/reducer.js +16 -0
  23. package/src/javascripts/domains/app/selectors.js +8 -0
  24. package/src/javascripts/domains/app/utils.js +4 -0
  25. package/src/javascripts/domains/config/actions.js +4 -0
  26. package/src/javascripts/domains/config/hooks.js +6 -0
  27. package/src/javascripts/domains/config/index.js +8 -0
  28. package/src/javascripts/domains/config/middleware.js +22 -0
  29. package/src/javascripts/domains/config/reducer.js +63 -0
  30. package/src/javascripts/domains/config/selectors.js +23 -0
  31. package/src/javascripts/domains/config/utils.js +4 -0
  32. package/src/javascripts/domains/forms/actions.js +2 -4
  33. package/src/javascripts/domains/forms/hooks.js +10 -14
  34. package/src/javascripts/domains/forms/provider.js +4 -6
  35. package/src/javascripts/domains/forms/reducer.js +1 -2
  36. package/src/javascripts/domains/forms/selectors.js +4 -4
  37. package/src/javascripts/domains/forms/utils.js +5 -0
  38. package/src/javascripts/domains/i18n/actions.js +35 -0
  39. package/src/javascripts/domains/i18n/hooks.js +38 -0
  40. package/src/javascripts/domains/i18n/index.js +5 -80
  41. package/src/javascripts/domains/i18n/reducer.js +58 -0
  42. package/src/javascripts/domains/i18n/selectors.js +15 -0
  43. package/src/javascripts/domains/i18n/utils.js +9 -0
  44. package/src/javascripts/domains/interrupt/actions.js +4 -0
  45. package/src/javascripts/domains/interrupt/hooks.js +29 -0
  46. package/src/javascripts/domains/interrupt/index.js +9 -0
  47. package/src/javascripts/domains/interrupt/middleware.js +30 -0
  48. package/src/javascripts/domains/interrupt/reducer.js +21 -0
  49. package/src/javascripts/domains/interrupt/selectors.js +6 -0
  50. package/src/javascripts/domains/interrupt/utils.js +4 -0
  51. package/src/javascripts/domains/options/index.js +1 -0
  52. package/src/javascripts/domains/options/middleware.js +35 -0
  53. package/src/javascripts/domains/redux/create-redux-store.js +14 -6
  54. package/src/javascripts/domains/redux/hooks.js +3 -2
  55. package/src/javascripts/domains/redux/index.js +2 -1
  56. package/src/javascripts/domains/redux/provider.js +5 -0
  57. package/src/javascripts/domains/store/index.js +44 -0
  58. package/src/javascripts/{ui → domains}/store/state-reducer.js +4 -7
  59. package/src/javascripts/domains/translations/actions.js +4 -6
  60. package/src/javascripts/domains/translations/components/chat-status.js +7 -13
  61. package/src/javascripts/domains/translations/components/options-button.js +3 -3
  62. package/src/javascripts/domains/translations/components/options-dialog/form.js +12 -7
  63. package/src/javascripts/domains/translations/components/options-dialog/index.js +2 -5
  64. package/src/javascripts/domains/translations/hooks.js +1 -1
  65. package/src/javascripts/domains/translations/index.js +1 -0
  66. package/src/javascripts/domains/translations/middleware.js +43 -0
  67. package/src/javascripts/domains/translations/reducer.js +4 -11
  68. package/src/javascripts/domains/translations/selectors.js +3 -3
  69. package/src/javascripts/domains/translations/utils.js +4 -0
  70. package/src/javascripts/index.js +20 -5
  71. package/src/javascripts/lib/css.js +5 -5
  72. package/src/javascripts/lib/engine/index.js +39 -11
  73. package/src/javascripts/lib/external-api/index.js +6 -6
  74. package/src/javascripts/lib/mutex.js +30 -0
  75. package/src/javascripts/lib/parse-body.js +1 -1
  76. package/src/javascripts/lib/redux-helpers/index.js +25 -8
  77. package/src/javascripts/lib/split-url-params.js +2 -2
  78. package/src/javascripts/lib/store/providers/app-storage.js +1 -1
  79. package/src/javascripts/lib/store/providers/cookie-storage.js +1 -1
  80. package/src/javascripts/package/utils.js +0 -1
  81. package/src/javascripts/style-guide/components/app.js +12 -14
  82. package/src/javascripts/style-guide/components/links.js +6 -6
  83. package/src/javascripts/style-guide/components/static-core.js +32 -10
  84. package/src/javascripts/style-guide/state-helpers/index.js +1 -1
  85. package/src/javascripts/style-guide/states.js +29 -71
  86. package/src/javascripts/style-guide/style-guide-engine.js +13 -12
  87. package/src/javascripts/ui/components/chat-app.js +2 -2
  88. package/src/javascripts/ui/components/conversation/component-filter.js +2 -2
  89. package/src/javascripts/ui/components/conversation/conversation.js +2 -2
  90. package/src/javascripts/ui/components/conversation/event/card-component.js +24 -3
  91. package/src/javascripts/ui/components/conversation/event/carousel-component/components/pagination.js +2 -2
  92. package/src/javascripts/ui/components/conversation/event/carousel-component/index.js +4 -3
  93. package/src/javascripts/ui/components/conversation/event/carousel-message/components/slide.js +2 -1
  94. package/src/javascripts/ui/components/conversation/event/carousel-message/index.js +2 -2
  95. package/src/javascripts/ui/components/conversation/event/choice-prompt.js +5 -5
  96. package/src/javascripts/ui/components/conversation/event/divider/variants/new-translation.js +2 -2
  97. package/src/javascripts/ui/components/conversation/event/event-participant.js +3 -5
  98. package/src/javascripts/ui/components/conversation/event/hooks/use-event-link-click-handler.js +2 -2
  99. package/src/javascripts/ui/components/conversation/event/hooks/use-formatted-date.js +3 -3
  100. package/src/javascripts/ui/components/conversation/event/hooks/use-text-rendering.js +3 -3
  101. package/src/javascripts/ui/components/conversation/event/participant.js +2 -2
  102. package/src/javascripts/ui/components/conversation/event/upload.js +12 -27
  103. package/src/javascripts/ui/components/conversation/message-container.js +4 -6
  104. package/src/javascripts/ui/components/core/seamly-activity-monitor.js +4 -5
  105. package/src/javascripts/ui/components/core/seamly-core.js +6 -7
  106. package/src/javascripts/ui/components/core/seamly-event-subscriber.js +18 -17
  107. package/src/javascripts/ui/components/core/seamly-file-upload.js +5 -6
  108. package/src/javascripts/ui/components/core/seamly-idle-detach-counter.js +2 -6
  109. package/src/javascripts/ui/components/core/seamly-initializer.js +7 -60
  110. package/src/javascripts/ui/components/core/seamly-instance-functions-loader.js +10 -16
  111. package/src/javascripts/ui/components/core/seamly-live-region.js +1 -1
  112. package/src/javascripts/ui/components/core/seamly-new-notifications.js +5 -6
  113. package/src/javascripts/ui/components/core/seamly-read-state.js +8 -6
  114. package/src/javascripts/ui/components/entry/entry-container.js +7 -10
  115. package/src/javascripts/ui/components/entry/text-entry/hooks.js +6 -4
  116. package/src/javascripts/ui/components/entry/text-entry/text-entry-form.js +10 -3
  117. package/src/javascripts/ui/components/entry/toggle-button.js +24 -10
  118. package/src/javascripts/ui/components/entry/upload/file-upload-form.js +6 -3
  119. package/src/javascripts/ui/components/entry/upload/index.js +11 -13
  120. package/src/javascripts/ui/components/faq/faq.js +6 -6
  121. package/src/javascripts/ui/components/form-controls/error.js +22 -0
  122. package/src/javascripts/ui/components/form-controls/file-input.js +3 -9
  123. package/src/javascripts/ui/components/form-controls/select.js +1 -1
  124. package/src/javascripts/ui/components/form-controls/wrapper.js +2 -9
  125. package/src/javascripts/ui/components/layout/agent-info.js +4 -4
  126. package/src/javascripts/ui/components/layout/app-frame.js +15 -12
  127. package/src/javascripts/ui/components/layout/chat-frame.js +3 -5
  128. package/src/javascripts/ui/components/layout/header.js +4 -18
  129. package/src/javascripts/ui/components/layout/interrupt.js +6 -2
  130. package/src/javascripts/ui/components/layout/privacy-disclaimer.js +2 -2
  131. package/src/javascripts/ui/components/options/cobrowsing.js +3 -7
  132. package/src/javascripts/ui/components/options/options-button.js +9 -13
  133. package/src/javascripts/ui/components/options/options-frame.js +1 -1
  134. package/src/javascripts/ui/components/options/transcript/index.js +2 -2
  135. package/src/javascripts/ui/components/options/transcript/transcript-form.js +1 -1
  136. package/src/javascripts/ui/components/warnings/cobrowsing-active-frame.js +3 -6
  137. package/src/javascripts/ui/components/warnings/idle-detach-warning.js +2 -6
  138. package/src/javascripts/ui/components/warnings/resume-conversation-prompt.js +1 -1
  139. package/src/javascripts/ui/components/widgets/in-out-transition.js +2 -2
  140. package/src/javascripts/ui/components/widgets/lightbox.js +4 -4
  141. package/src/javascripts/ui/components/widgets/modal.js +3 -3
  142. package/src/javascripts/ui/components/widgets/upload-progress.js +3 -14
  143. package/src/javascripts/ui/hooks/component-helper-hooks.js +4 -15
  144. package/src/javascripts/ui/hooks/file-upload-hooks.js +3 -3
  145. package/src/javascripts/ui/hooks/focus-helper-hooks.js +4 -4
  146. package/src/javascripts/ui/hooks/live-region-hooks.js +2 -2
  147. package/src/javascripts/ui/hooks/seamly-api-hooks.js +0 -6
  148. package/src/javascripts/ui/hooks/seamly-entry-hooks.js +22 -25
  149. package/src/javascripts/ui/hooks/seamly-hooks.js +3 -10
  150. package/src/javascripts/ui/hooks/seamly-option-hooks.js +4 -4
  151. package/src/javascripts/ui/hooks/seamly-state-hooks.js +8 -16
  152. package/src/javascripts/ui/hooks/use-event-component-mapping.js +1 -1
  153. package/src/javascripts/ui/hooks/use-seamly-chat.js +1 -0
  154. package/src/javascripts/ui/hooks/use-seamly-commands.js +31 -54
  155. package/src/javascripts/ui/hooks/use-seamly-idle-detach-countdown.js +3 -3
  156. package/src/javascripts/ui/hooks/use-seamly-stored-visibility.js +3 -3
  157. package/src/javascripts/ui/hooks/use-seamly-visibility.js +6 -8
  158. package/src/javascripts/ui/hooks/use-single-file-upload.js +4 -1
  159. package/src/javascripts/ui/hooks/utility-hooks.js +2 -2
  160. package/src/javascripts/ui/utils/form-utils.js +3 -3
  161. package/src/javascripts/ui/utils/general-utils.js +21 -22
  162. package/src/javascripts/ui/utils/seamly-utils.js +15 -83
  163. package/src/javascripts/ui/utils/validations.js +10 -7
  164. package/src/stylesheets/1-settings/_config.scss +2 -1
  165. package/src/stylesheets/3-app/_app.scss +3 -4
  166. package/src/stylesheets/5-components/_card.scss +0 -1
  167. package/src/stylesheets/5-components/_faq.scss +3 -8
  168. package/src/stylesheets/5-components/_message.scss +10 -0
  169. package/src/stylesheets/5-components/_modal.scss +3 -3
  170. package/src/stylesheets/5-components/_options.scss +3 -2
  171. package/webpack/config.common.js +3 -3
  172. package/webpack/config.package.js +4 -22
  173. package/webpack/config.site.js +8 -6
  174. package/webpack/defaults.js +0 -3
  175. package/CHANGELOG.md +0 -561
  176. package/build/dist/translations/de-informal.js +0 -275
  177. package/build/dist/translations/de-informal.min.js +0 -1
  178. package/build/dist/translations/en.js +0 -275
  179. package/build/dist/translations/en.min.js +0 -1
  180. package/build/dist/translations/es-informal.js +0 -281
  181. package/build/dist/translations/es-informal.min.js +0 -1
  182. package/build/dist/translations/nl-formal.js +0 -275
  183. package/build/dist/translations/nl-formal.min.js +0 -1
  184. package/build/dist/translations/nl-informal.js +0 -275
  185. package/build/dist/translations/nl-informal.min.js +0 -1
  186. package/src/javascripts/lib/i18n.js +0 -46
  187. package/src/javascripts/ui/components/core/seamly-api.js +0 -44
  188. package/src/javascripts/ui/hooks/use-seamly-interrupt.js +0 -62
  189. package/src/javascripts/ui/store/index.js +0 -37
  190. package/translations/de-informal.js +0 -237
  191. package/translations/en.js +0 -234
  192. package/translations/es-informal.js +0 -243
  193. package/translations/nl-formal.js +0 -230
  194. package/translations/nl-informal.js +0 -230
@@ -33,18 +33,25 @@ export default function TextEntryForm({ controlName, skipLinkId }) {
33
33
 
34
34
  const handleFocus = useCallback(() => {
35
35
  if (reachedCharacterWarning) {
36
- sendAssertive(t('input.srCharacterLimitText', remainingChars))
36
+ sendAssertive(t('input.srCharacterLimitText', { limit: remainingChars }))
37
37
  }
38
38
 
39
39
  emitEvent('ui.inputFocus')
40
40
  }, [t, sendAssertive, reachedCharacterWarning, remainingChars, emitEvent])
41
41
  const placeholder = useMemo(
42
42
  () =>
43
- t('input.inputPlaceholder', hasCharacterLimit ? characterLimit : null),
43
+ t('input.inputPlaceholder', {
44
+ hasLimit: hasCharacterLimit,
45
+ limit: hasCharacterLimit ? characterLimit : null,
46
+ }),
44
47
  [t, hasCharacterLimit, characterLimit],
45
48
  )
46
49
  const labelText = useMemo(
47
- () => t('input.inputLabel', hasCharacterLimit ? characterLimit : null),
50
+ () =>
51
+ t('input.inputLabel', {
52
+ hasLimit: hasCharacterLimit,
53
+ limit: hasCharacterLimit ? characterLimit : null,
54
+ }),
48
55
  [t, hasCharacterLimit, characterLimit],
49
56
  )
50
57
  // When the input holds a value, the component should be blocked from switching
@@ -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
  )
@@ -15,8 +15,8 @@ export default function FileInputForm({
15
15
  }) {
16
16
  const { t } = useI18n()
17
17
  const [{ value: fileList }] = useFormControl(controlName)
18
- const selectedFileName =
19
- fileList && fileList.length > 0 ? fileList[0].name : ''
18
+ const hasFile = fileList && fileList.length > 0
19
+ const selectedFileName = hasFile ? fileList[0].name : ''
20
20
 
21
21
  return (
22
22
  <Form className={className('input', 'input--file')}>
@@ -25,7 +25,10 @@ export default function FileInputForm({
25
25
  id={skiplinkId}
26
26
  accept={accept}
27
27
  labelText={t('fileUpload.labelText')}
28
- outputText={t('fileUpload.selectedText', selectedFileName)}
28
+ outputText={t('fileUpload.selectedText', {
29
+ hasFile,
30
+ filename: selectedFileName,
31
+ })}
29
32
  contentHint={contentHint}
30
33
  />
31
34
  <div className={className('upload__button-container')}>
@@ -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,16 +40,14 @@ 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 { hasFile, selectedFileName, uploadHandle, hasServerError, progress } =
44
+ useSingleFileUpload(formName, fileInputName)
52
45
  const notificationId = useGeneratedId()
53
46
  const prevIsComplete = useRef(true)
54
47
 
55
- const contentHintText = t('fileUpload.contentHint', formatBytes(maxSize))
48
+ const contentHintText = t('fileUpload.contentHint', {
49
+ size: formatBytes(maxSize),
50
+ })
56
51
  const prevContentHintText = useRef('')
57
52
  const containerRef = useRef(null)
58
53
 
@@ -184,7 +179,10 @@ const Upload = () => {
184
179
  contentHint={contentHintText}
185
180
  isComplete={isComplete}
186
181
  isUploading={isUploading}
187
- outputText={t('fileUpload.selectedText', selectedFileName)}
182
+ outputText={t('fileUpload.selectedText', {
183
+ hasFile,
184
+ filename: selectedFileName,
185
+ })}
188
186
  onClickCancel={handleOnClickCancel}
189
187
  />
190
188
  )}
@@ -10,9 +10,7 @@ import {
10
10
  } from '../../hooks/seamly-state-hooks'
11
11
  import { useGeneratedId } from '../../hooks/utility-hooks'
12
12
  import { useSkiplinkTargetFocusing } from '../../hooks/focus-helper-hooks'
13
- import { useSeamlyHasUserResponded } from '../../hooks/seamly-api-hooks'
14
13
  import { useLiveRegion } from '../../hooks/live-region-hooks'
15
- import useSeamlyInterrupt from '../../hooks/use-seamly-interrupt'
16
14
  import useSeamlyIdleDetachCountdown from '../../hooks/use-seamly-idle-detach-countdown'
17
15
  import useSeamlyResumeConversationPrompt from '../../hooks/use-seamly-resume-conversation-prompt'
18
16
  import { useI18n } from '../../../domains/i18n'
@@ -20,6 +18,8 @@ import InOutTransition, {
20
18
  transitionStartStates,
21
19
  } from '../widgets/in-out-transition'
22
20
  import { useTranslatedEventData } from '../../../domains/translations'
21
+ import { useInterrupt } from '../../../domains/interrupt'
22
+ import { useUserHasResponded } from '../../../domains/app'
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_\\-]/, '')}`,
@@ -55,7 +55,7 @@ const Faq = () => {
55
55
  const prevHasFaqs = useRef(false)
56
56
 
57
57
  const { isInline } = useSeamlyLayoutMode()
58
- const hasResponded = useSeamlyHasUserResponded()
58
+ const hasResponded = useUserHasResponded()
59
59
  const hideForWindow = !isInline && hasResponded
60
60
  const prevHideForWindow = useRef(hideForWindow)
61
61
 
@@ -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
 
@@ -1,40 +1,43 @@
1
- import { useState, useEffect, useCallback } from 'preact/hooks'
1
+ import { useCallback, useMemo } 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
- useSeamlyHasUserResponded,
9
7
  useSeamlyContainerElement,
10
8
  } from '../../hooks/seamly-hooks'
11
9
  import Faq from '../faq/faq'
12
10
  import { visibilityStates } from '../../utils/seamly-utils'
11
+ import { useConfig } from '../../../domains/config'
12
+ import { useUserHasResponded } from '../../../domains/app'
13
+ import { useI18n } from '../../../domains/i18n'
13
14
 
14
15
  const AppFrame = ({ children }) => {
15
16
  const [, setSeamlyContainerElement] = useSeamlyContainerElement()
16
17
  const { isOpen, isVisible, setVisibility } = useSeamlyVisibility()
17
- const { context, zIndex, showFaq } = useSeamlyConfig()
18
+ const { zIndex, showFaq } = useConfig()
18
19
  const { isModal, isInline } = useSeamlyLayoutMode()
19
20
  const appContainerClassNames = useSeamlyAppContainerClassNames()
20
- const userResponded = useSeamlyHasUserResponded()
21
- const [blockLang, setBlockLang] = useState(undefined)
22
- const { locale } = context || {}
21
+ const userResponded = useUserHasResponded()
22
+ const { locale } = useI18n()
23
23
 
24
24
  const containerElementRef = useCallback(
25
- container => {
25
+ (container) => {
26
26
  setSeamlyContainerElement(container)
27
27
  },
28
28
  [setSeamlyContainerElement],
29
29
  )
30
30
 
31
- useEffect(() => {
31
+ const blockLang = useMemo(() => {
32
32
  if (locale) {
33
33
  const htmlElementLang = document
34
34
  .querySelector('html')
35
35
  .getAttribute('lang')
36
- if (!htmlElementLang || htmlElementLang !== locale) setBlockLang(locale)
36
+ if (htmlElementLang !== locale) {
37
+ return locale
38
+ }
37
39
  }
40
+ return undefined
38
41
  }, [locale])
39
42
 
40
43
  const classNames = ['app', ...appContainerClassNames]
@@ -47,14 +50,14 @@ const AppFrame = ({ children }) => {
47
50
  classNames.push('app--user-responded')
48
51
  }
49
52
 
50
- const onKeyDownHandler = e => {
53
+ const onKeyDownHandler = (e) => {
51
54
  if ((e.code && e.code === 'Escape') || e.keyCode === 27)
52
55
  if (!isInline && isOpen) {
53
56
  setVisibility(visibilityStates.minimized)
54
57
  }
55
58
  }
56
59
 
57
- const onClickHandler = e => {
60
+ const onClickHandler = (e) => {
58
61
  if (isModal) {
59
62
  e.stopPropagation()
60
63
  }
@@ -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>
@@ -25,14 +25,18 @@ const Interrupt = ({
25
25
  useEffect(() => {
26
26
  if (isExpiredError) {
27
27
  seamlyCommands[action]()
28
- } else if (srText) {
28
+ }
29
+ }, [action, seamlyCommands, isExpiredError])
30
+
31
+ useEffect(() => {
32
+ if (!isExpiredError && srText) {
29
33
  // Wait for live regions to stabilise in case this occurs
30
34
  // at an initial render
31
35
  setTimeout(() => {
32
36
  sendPolite(srText)
33
37
  }, 200)
34
38
  }
35
- }, [sendPolite, seamlyCommands, srText, isExpiredError, action])
39
+ }, [sendPolite, srText, isExpiredError])
36
40
 
37
41
  const onClickHandler = () => {
38
42
  seamlyCommands[action]()
@@ -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 },
@@ -9,7 +9,7 @@ export default function TranscriptForm({ controlName, describedById }) {
9
9
  <Form noValidate="true">
10
10
  <Input
11
11
  name={controlName}
12
- type="text"
12
+ type="email"
13
13
  className={className('transcript__input')}
14
14
  aria-describedby={describedById}
15
15
  labelClass={className('label')}
@@ -1,16 +1,13 @@
1
1
  import { useEffect, useRef } from 'preact/hooks'
2
2
  import CobrowsingActive from './cobrowsing-active'
3
- import {
4
- useLiveRegion,
5
- useSeamlyInterrupt,
6
- useSeamlyOptions,
7
- } from '../../hooks/seamly-hooks'
3
+ import { useLiveRegion, useSeamlyOptions } from '../../hooks/seamly-hooks'
8
4
  import { useI18n } from '../../../domains/i18n'
5
+ import { useInterrupt } from '../../../domains/interrupt'
9
6
 
10
7
  const CobrowsingActiveFrame = () => {
11
8
  const { t } = useI18n()
12
9
  const { userSelectedOptions } = useSeamlyOptions()
13
- const { hasInterrupt } = useSeamlyInterrupt()
10
+ const { hasInterrupt } = useInterrupt()
14
11
  const { cobrowsing } = userSelectedOptions
15
12
  const prevCobrowsing = useRef(cobrowsing)
16
13
  const { sendPolite } = useLiveRegion()