@seamly/web-ui 19.1.1 → 20.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 (186) hide show
  1. package/CHANGELOG.md +625 -0
  2. package/build/dist/lib/components.js +2 -1
  3. package/build/dist/lib/components.min.js +1 -1
  4. package/build/dist/lib/index.debug.js +183 -128
  5. package/build/dist/lib/index.debug.min.js +1 -1
  6. package/build/dist/lib/index.debug.min.js.LICENSE.txt +45 -25
  7. package/build/dist/lib/index.js +7290 -7753
  8. package/build/dist/lib/index.min.js +1 -1
  9. package/build/dist/lib/index.min.js.LICENSE.txt +0 -5
  10. package/build/dist/lib/standalone.js +5785 -6255
  11. package/build/dist/lib/standalone.min.js +1 -1
  12. package/build/dist/lib/standalone.min.js.LICENSE.txt +0 -5
  13. package/build/dist/lib/style-guide.js +10834 -4971
  14. package/build/dist/lib/style-guide.min.js +2 -1
  15. package/build/dist/lib/style-guide.min.js.LICENSE.txt +9 -0
  16. package/build/dist/lib/styles.css +1 -1
  17. package/package.json +1 -2
  18. package/src/icons/icon_file-32.svg +1 -1
  19. package/src/javascripts/api/errors/seamly-base-error.js +7 -0
  20. package/src/javascripts/api/index.js +48 -37
  21. package/src/javascripts/api/producer.js +5 -1
  22. package/src/javascripts/config.js +1 -5
  23. package/src/javascripts/domains/app/actions.js +22 -5
  24. package/src/javascripts/domains/config/actions.js +3 -0
  25. package/src/javascripts/domains/config/reducer.js +9 -0
  26. package/src/javascripts/domains/errors/index.js +5 -4
  27. package/src/javascripts/domains/forms/hooks.js +3 -1
  28. package/src/javascripts/domains/forms/provider.js +12 -0
  29. package/src/javascripts/domains/forms/reducer.js +2 -0
  30. package/src/javascripts/domains/i18n/hooks.js +2 -1
  31. package/src/javascripts/domains/i18n/reducer.js +2 -0
  32. package/src/javascripts/domains/interrupt/reducer.js +2 -2
  33. package/src/javascripts/domains/options/middleware.js +15 -31
  34. package/src/javascripts/domains/store/index.js +2 -1
  35. package/src/javascripts/domains/store/state-reducer.js +3 -8
  36. package/src/javascripts/domains/translations/components/options-dialog/form.js +1 -1
  37. package/src/javascripts/domains/translations/components/options-dialog/index.js +15 -1
  38. package/src/javascripts/domains/translations/reducer.js +2 -0
  39. package/src/javascripts/domains/visibility/actions.js +1 -1
  40. package/src/javascripts/domains/visibility/hooks.js +10 -8
  41. package/src/javascripts/domains/visibility/utils.js +1 -2
  42. package/src/javascripts/index.js +5 -3
  43. package/src/javascripts/lib/css.js +7 -1
  44. package/src/javascripts/lib/engine/index.js +4 -3
  45. package/src/javascripts/lib/external-api/index.js +38 -29
  46. package/src/javascripts/package/components.js +2 -1
  47. package/src/javascripts/style-guide/components/app.js +1 -1
  48. package/src/javascripts/style-guide/components/static-core.js +18 -4
  49. package/src/javascripts/style-guide/states.js +203 -298
  50. package/src/javascripts/ui/components/chat-app.js +1 -1
  51. package/src/javascripts/ui/components/conversation/component-filter.js +6 -0
  52. package/src/javascripts/ui/components/conversation/event/carousel-component/index.js +8 -1
  53. package/src/javascripts/ui/components/conversation/event/carousel-message/components/slide.js +2 -3
  54. package/src/javascripts/ui/components/conversation/event/conversation-suggestions.js +70 -0
  55. package/src/javascripts/ui/components/conversation/event/participant.js +2 -5
  56. package/src/javascripts/ui/components/conversation/event/splash.js +26 -0
  57. package/src/javascripts/ui/components/conversation/event/text.js +1 -2
  58. package/src/javascripts/ui/components/core/seamly-core.js +12 -9
  59. package/src/javascripts/ui/components/core/seamly-event-subscriber.js +4 -10
  60. package/src/javascripts/ui/components/core/seamly-instance-functions-loader.js +1 -8
  61. package/src/javascripts/ui/components/entry/entry-container.js +5 -3
  62. package/src/javascripts/ui/components/entry/text-entry/index.js +7 -1
  63. package/src/javascripts/ui/components/entry/text-entry/text-entry-form.js +5 -1
  64. package/src/javascripts/ui/components/entry/toggle-button.js +4 -2
  65. package/src/javascripts/ui/components/entry/upload/file-upload-form.js +1 -1
  66. package/src/javascripts/ui/components/form-controls/error.js +6 -2
  67. package/src/javascripts/ui/components/form-controls/form.js +26 -3
  68. package/src/javascripts/ui/components/layout/app-frame.js +24 -15
  69. package/src/javascripts/ui/components/layout/chat-frame.js +0 -2
  70. package/src/javascripts/ui/components/layout/modal-wrapper.js +0 -80
  71. package/src/javascripts/ui/components/layout/pre-chat-messages.js +45 -0
  72. package/src/javascripts/ui/components/options/options-frame.js +9 -4
  73. package/src/javascripts/ui/components/options/options.js +1 -4
  74. package/src/javascripts/ui/components/options/transcript/index.js +15 -1
  75. package/src/javascripts/ui/components/options/transcript/transcript-form.js +1 -1
  76. package/src/javascripts/ui/components/suggestions/index.js +174 -0
  77. package/src/javascripts/ui/components/suggestions/suggestions-item.js +40 -0
  78. package/src/javascripts/ui/components/suggestions/suggestions-list.js +24 -0
  79. package/src/javascripts/ui/components/view/app-view.js +21 -0
  80. package/src/javascripts/ui/components/view/deprecated-view.js +30 -0
  81. package/src/javascripts/ui/components/view/index.js +27 -0
  82. package/src/javascripts/ui/components/view/inline-view.js +45 -0
  83. package/src/javascripts/ui/components/view/window-view/collapse-button.js +20 -0
  84. package/src/javascripts/ui/components/view/window-view/index.js +82 -0
  85. package/src/javascripts/ui/components/view/window-view/window-open-button.js +68 -0
  86. package/src/javascripts/ui/components/widgets/lightbox.js +7 -2
  87. package/src/javascripts/ui/hooks/component-helper-hooks.js +0 -9
  88. package/src/javascripts/ui/hooks/seamly-hooks.js +0 -1
  89. package/src/javascripts/ui/hooks/seamly-state-hooks.js +28 -4
  90. package/src/javascripts/ui/hooks/use-seamly-chat.js +12 -3
  91. package/src/javascripts/ui/hooks/use-seamly-commands.js +4 -31
  92. package/src/javascripts/ui/utils/seamly-utils.js +2 -14
  93. package/src/stylesheets/1-settings/_animations.scss +0 -6
  94. package/src/stylesheets/1-settings/_config.scss +34 -35
  95. package/src/stylesheets/2-tools/_functions.scss +0 -5
  96. package/src/stylesheets/2-tools/_mixins.scss +4 -16
  97. package/src/stylesheets/3-app/_app.scss +78 -135
  98. package/src/stylesheets/4-base/_a11y.scss +0 -3
  99. package/src/stylesheets/4-base/_elements.scss +0 -11
  100. package/src/stylesheets/4-base/_formelements.scss +4 -14
  101. package/src/stylesheets/5-components/_avatar.scss +2 -44
  102. package/src/stylesheets/5-components/_buttons.scss +6 -45
  103. package/src/stylesheets/5-components/_chat-status.scss +14 -38
  104. package/src/stylesheets/5-components/_choice-prompt.scss +33 -2
  105. package/src/stylesheets/5-components/_collapse-button.scss +16 -0
  106. package/src/stylesheets/5-components/_conversation.scss +26 -2
  107. package/src/stylesheets/5-components/_disclaimer.scss +10 -12
  108. package/src/stylesheets/5-components/_divider.scss +7 -4
  109. package/src/stylesheets/5-components/_error.scss +1 -1
  110. package/src/stylesheets/5-components/_form.scss +9 -0
  111. package/src/stylesheets/5-components/_icon.scss +10 -1
  112. package/src/stylesheets/5-components/_idle.scss +0 -8
  113. package/src/stylesheets/5-components/_input.scss +14 -20
  114. package/src/stylesheets/5-components/_interrupt.scss +0 -2
  115. package/src/stylesheets/5-components/_loader.scss +0 -32
  116. package/src/stylesheets/5-components/_message-author.scss +40 -0
  117. package/src/stylesheets/5-components/_message-body.scss +194 -0
  118. package/src/stylesheets/5-components/_message-card.scss +55 -0
  119. package/src/stylesheets/5-components/_message-carousel.scss +143 -0
  120. package/src/stylesheets/5-components/_message-count.scss +11 -28
  121. package/src/stylesheets/5-components/_message-cta.scss +23 -0
  122. package/src/stylesheets/5-components/_message-info.scss +11 -0
  123. package/src/stylesheets/5-components/_message-translation-info.scss +17 -0
  124. package/src/stylesheets/5-components/_message.scss +13 -364
  125. package/src/stylesheets/5-components/_modal.scss +28 -58
  126. package/src/stylesheets/5-components/_notification.scss +0 -5
  127. package/src/stylesheets/5-components/_options.scss +27 -42
  128. package/src/stylesheets/5-components/_pre-chat-messages.scss +30 -0
  129. package/src/stylesheets/5-components/_prompt.scss +0 -8
  130. package/src/stylesheets/5-components/_skip-link.scss +3 -3
  131. package/src/stylesheets/5-components/_suggestions.scss +96 -0
  132. package/src/stylesheets/5-components/_unstarted.scss +50 -0
  133. package/src/stylesheets/5-components/_upload.scss +26 -28
  134. package/src/stylesheets/5-components/_window-open-button.scss +39 -0
  135. package/src/stylesheets/6-webui-only/_hover.scss +151 -0
  136. package/src/stylesheets/6-webui-only/_scrollbar.scss +31 -0
  137. package/src/stylesheets/7-deprecated/1-settings/_animations.scss +43 -0
  138. package/src/stylesheets/7-deprecated/1-settings/_config.scss +105 -0
  139. package/src/stylesheets/7-deprecated/2-tools/_functions.scss +22 -0
  140. package/src/stylesheets/7-deprecated/2-tools/_mixins.scss +77 -0
  141. package/src/stylesheets/7-deprecated/3-app/_app.scss +214 -0
  142. package/src/stylesheets/7-deprecated/4-base/_a11y.scss +14 -0
  143. package/src/stylesheets/7-deprecated/4-base/_elements.scss +21 -0
  144. package/src/stylesheets/7-deprecated/4-base/_formelements.scss +57 -0
  145. package/src/stylesheets/{5-components → 7-deprecated/5-components}/_agent-info.scss +0 -0
  146. package/src/stylesheets/7-deprecated/5-components/_avatar.scss +64 -0
  147. package/src/stylesheets/7-deprecated/5-components/_buttons.scss +94 -0
  148. package/src/stylesheets/{5-components → 7-deprecated/5-components}/_card.scss +0 -0
  149. package/src/stylesheets/{5-components → 7-deprecated/5-components}/_carousel.scss +0 -0
  150. package/src/stylesheets/7-deprecated/5-components/_character-limit.scss +36 -0
  151. package/src/stylesheets/{5-components/_cobrowsing.scss → 7-deprecated/5-components/_chat-status.scss} +18 -16
  152. package/src/stylesheets/7-deprecated/5-components/_choice-prompt.scss +27 -0
  153. package/src/stylesheets/7-deprecated/5-components/_collapse-button.scss +17 -0
  154. package/src/stylesheets/7-deprecated/5-components/_conversation.scss +44 -0
  155. package/src/stylesheets/7-deprecated/5-components/_disclaimer.scss +36 -0
  156. package/src/stylesheets/7-deprecated/5-components/_divider.scss +91 -0
  157. package/src/stylesheets/7-deprecated/5-components/_error.scss +24 -0
  158. package/src/stylesheets/{5-components → 7-deprecated/5-components}/_faq.scss +8 -3
  159. package/src/stylesheets/{5-components → 7-deprecated/5-components}/_header-controls.scss +0 -0
  160. package/src/stylesheets/7-deprecated/5-components/_icon.scss +4 -0
  161. package/src/stylesheets/7-deprecated/5-components/_idle.scss +61 -0
  162. package/src/stylesheets/7-deprecated/5-components/_input.scss +78 -0
  163. package/src/stylesheets/7-deprecated/5-components/_interrupt.scss +35 -0
  164. package/src/stylesheets/7-deprecated/5-components/_loader.scss +78 -0
  165. package/src/stylesheets/7-deprecated/5-components/_message-count.scss +41 -0
  166. package/src/stylesheets/7-deprecated/5-components/_message.scss +385 -0
  167. package/src/stylesheets/7-deprecated/5-components/_modal.scss +138 -0
  168. package/src/stylesheets/7-deprecated/5-components/_notification.scss +20 -0
  169. package/src/stylesheets/7-deprecated/5-components/_options.scss +286 -0
  170. package/src/stylesheets/7-deprecated/5-components/_prompt.scss +44 -0
  171. package/src/stylesheets/7-deprecated/5-components/_skip-link.scss +21 -0
  172. package/src/stylesheets/{5-components → 7-deprecated/5-components}/_svg-graphic.scss +0 -0
  173. package/src/stylesheets/7-deprecated/5-components/_upload.scss +213 -0
  174. package/src/stylesheets/deprecated-view.scss +64 -0
  175. package/src/stylesheets/styles-webui-only.scss +3 -0
  176. package/src/stylesheets/styles.scss +15 -25
  177. package/webpack/config.site.js +4 -0
  178. package/webpack/defaults.js +5 -0
  179. package/src/.DS_Store +0 -0
  180. package/src/javascripts/ui/components/conversation/event/hooks/use-text-rendering.js +0 -35
  181. package/src/javascripts/ui/components/faq/faq.js +0 -162
  182. package/src/javascripts/ui/components/layout/view.js +0 -36
  183. package/src/javascripts/ui/components/options/cobrowsing.js +0 -110
  184. package/src/javascripts/ui/components/warnings/cobrowsing-active-frame.js +0 -29
  185. package/src/javascripts/ui/components/warnings/cobrowsing-active.js +0 -33
  186. package/src/stylesheets/5-components/_modal_mode.scss +0 -108
@@ -0,0 +1,45 @@
1
+ import { className } from '../../../lib/css'
2
+ import { useInterrupt } from '../../../domains/interrupt'
3
+ import { useConfig } from '../../../domains/config'
4
+ import useEventComponentMapping from '../../hooks/use-event-component-mapping'
5
+ import { useVisibility } from '../../../domains/visibility'
6
+ import InOutTransition, {
7
+ transitionStartStates,
8
+ } from '../widgets/in-out-transition'
9
+
10
+ export default function PreChatMessages() {
11
+ const { preChatEvents, layoutMode } = useConfig()
12
+ const { hasInterrupt } = useInterrupt()
13
+ const { isOpen } = useVisibility()
14
+
15
+ const isVisible = !(hasInterrupt || !preChatEvents.length || isOpen)
16
+
17
+ return (
18
+ <InOutTransition
19
+ isActive={isVisible}
20
+ transitionStartState={transitionStartStates.notRendered}
21
+ >
22
+ <ul
23
+ className={className(
24
+ 'pre-chat-messages',
25
+ `pre-chat-messages--${layoutMode}`,
26
+ )}
27
+ aria-hidden={!isVisible}
28
+ >
29
+ {preChatEvents.map((event) => (
30
+ <li
31
+ className={className('pre-chat-messages__message')}
32
+ key={event.payload.id}
33
+ >
34
+ <PreChatMessageEvent event={event} />
35
+ </li>
36
+ ))}
37
+ </ul>
38
+ </InOutTransition>
39
+ )
40
+ }
41
+
42
+ export function PreChatMessageEvent({ event }) {
43
+ const [Component] = useEventComponentMapping(event)
44
+ return <Component event={event} />
45
+ }
@@ -9,6 +9,7 @@ import { className } from '../../../lib/css'
9
9
  import { focusElement } from '../../utils/general-utils'
10
10
 
11
11
  const OptionsFrame = ({
12
+ className: givenClassName,
12
13
  children,
13
14
  onCancel,
14
15
  headingText,
@@ -44,10 +45,14 @@ const OptionsFrame = ({
44
45
 
45
46
  return (
46
47
  <section
47
- className={className('options', {
48
- 'options--right': position !== 'left',
49
- 'options--left': position === 'left',
50
- })}
48
+ className={className(
49
+ 'options',
50
+ {
51
+ 'options--right': position !== 'left',
52
+ 'options--left': position === 'left',
53
+ },
54
+ givenClassName,
55
+ )}
51
56
  aria-labelledby={mainHeadingId}
52
57
  tabIndex="-1"
53
58
  ref={container}
@@ -1,10 +1,8 @@
1
1
  import { useRef } from 'preact/hooks'
2
- import Cobrowsing from './cobrowsing'
3
2
  import Transcript from './transcript'
4
3
  import { useSeamlyOptions } from '../../hooks/seamly-hooks'
5
4
 
6
5
  const mapper = {
7
- cobrowsing: Cobrowsing,
8
6
  sendTranscript: Transcript,
9
7
  }
10
8
 
@@ -18,8 +16,7 @@ const Options = () => {
18
16
  if (optionActive) {
19
17
  prevRenderOption.current = optionActive
20
18
  }
21
-
22
- return <RenderOption />
19
+ return RenderOption ? <RenderOption /> : null
23
20
  }
24
21
 
25
22
  export default Options
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo } from 'preact/hooks'
1
+ import { useCallback, useMemo, useState } from 'preact/hooks'
2
2
  import { useI18n } from '../../../../domains/i18n'
3
3
  import OptionsFrame from '../options-frame'
4
4
  import { className } from '../../../../lib/css'
@@ -18,6 +18,7 @@ const formId = 'sendTranscript'
18
18
  const controlName = 'email'
19
19
 
20
20
  const Transcript = () => {
21
+ const [errorClass, setErrorClass] = useState(undefined)
21
22
  const { hideOption } = useSeamlyOptions()
22
23
  const { focusButton } = useOptionButton()
23
24
  const { t } = useI18n()
@@ -50,8 +51,20 @@ const Transcript = () => {
50
51
  [sendAction, hideOption, focusButton],
51
52
  )
52
53
 
54
+ const handleError = useCallback(
55
+ ({ isValid, isSubmitted }) => {
56
+ if (isSubmitted && !isValid) {
57
+ setErrorClass('options--error')
58
+ } else {
59
+ setErrorClass(undefined)
60
+ }
61
+ },
62
+ [setErrorClass],
63
+ )
64
+
53
65
  return (
54
66
  <OptionsFrame
67
+ className={errorClass}
55
68
  headingText={t('options.sendTranscript.title')}
56
69
  cancelButtonText={t('options.cancelButtonText')}
57
70
  >
@@ -65,6 +78,7 @@ const Transcript = () => {
65
78
  formId={formId}
66
79
  onSubmit={handleSubmit}
67
80
  validationSchema={validationSchema}
81
+ onError={handleError}
68
82
  >
69
83
  <TranscriptForm
70
84
  controlName={controlName}
@@ -6,7 +6,7 @@ import Input from '../../form-controls/input'
6
6
  export default function TranscriptForm({ controlName, describedById }) {
7
7
  const { t } = useI18n()
8
8
  return (
9
- <Form noValidate="true">
9
+ <Form noValidate="true" className={className('options__form')}>
10
10
  <Input
11
11
  name={controlName}
12
12
  type="email"
@@ -0,0 +1,174 @@
1
+ import { useEffect, useRef, useCallback, useMemo } from 'preact/hooks'
2
+ import { className } from '../../../lib/css'
3
+ import { useI18n } from '../../../domains/i18n'
4
+ import { useSeamlyServiceData } from '../../hooks/seamly-state-hooks'
5
+ import { useTranslatedEventData } from '../../../domains/translations'
6
+ import { useInterrupt } from '../../../domains/interrupt'
7
+ import { useGeneratedId } from '../../hooks/utility-hooks'
8
+ import useSeamlyIdleDetachCountdown from '../../hooks/use-seamly-idle-detach-countdown'
9
+ import useSeamlyResumeConversationPrompt from '../../hooks/use-seamly-resume-conversation-prompt'
10
+ import useSeamlyCommands from '../../hooks/use-seamly-commands'
11
+ import { useSkiplinkTargetFocusing } from '../../hooks/focus-helper-hooks'
12
+ import { actionTypes } from '../../utils/seamly-utils'
13
+ import { useUserHasResponded } from '../../../domains/app'
14
+ import { useVisibility, visibilityStates } from '../../../domains/visibility'
15
+ import InOutTransition, {
16
+ transitionStartStates,
17
+ } from '../widgets/in-out-transition'
18
+ import { useLiveRegion } from '../../hooks/live-region-hooks'
19
+ import { runIfElementContainsOrHasFocus } from '../../utils/general-utils'
20
+ import SuggestionsList from './suggestions-list'
21
+ import { useConfig } from '../../../domains/config'
22
+
23
+ const Suggestions = ({ isAside }) => {
24
+ // generic hooks
25
+ const { layoutMode } = useConfig()
26
+ const { t } = useI18n()
27
+ const { sendAction, addMessageBubble } = useSeamlyCommands()
28
+ const { isOpen, setVisibility } = useVisibility()
29
+ // a11y hooks
30
+ const sectionId = useGeneratedId()
31
+ const focusSkiplinkTarget = useSkiplinkTargetFocusing()
32
+ const containerRef = useRef(null)
33
+ const { sendPolite } = useLiveRegion()
34
+ // interrupt & countdown hooks
35
+ const { hasInterrupt } = useInterrupt()
36
+ const { hasCountdown, endCountdown } = useSeamlyIdleDetachCountdown()
37
+ const { hasPrompt, continueChat } = useSeamlyResumeConversationPrompt()
38
+ // data hooks
39
+ const hasResponded = useUserHasResponded()
40
+ const payload = useSeamlyServiceData('suggestion')
41
+ const [eventBody] = useTranslatedEventData({ payload })
42
+ const suggestions = useMemo(
43
+ () => (payload && !hasInterrupt ? eventBody : []),
44
+ [payload, hasInterrupt, eventBody],
45
+ )
46
+
47
+ const prevSuggestions = useRef(null)
48
+ const prevHasSuggestions = useRef(false)
49
+
50
+ const previousRenderedSuggestions = useRef([])
51
+ const hasSuggestions = !!suggestions.length
52
+ const hideSuggestions =
53
+ layoutMode === 'inline'
54
+ ? (hasResponded || isOpen) && !isAside
55
+ : hasResponded
56
+ const prevHideSuggestions = useRef(hideSuggestions)
57
+ const showSuggestionsContainer = hasSuggestions && !hideSuggestions
58
+ const renderedSuggestions = hasSuggestions
59
+ ? suggestions
60
+ : previousRenderedSuggestions.current
61
+ previousRenderedSuggestions.current = renderedSuggestions
62
+
63
+ // click handler
64
+ const handleClick = useCallback(
65
+ ({ id, question }) => {
66
+ if (hasCountdown) {
67
+ endCountdown(true)
68
+ }
69
+
70
+ if (hasPrompt) {
71
+ continueChat()
72
+ }
73
+
74
+ // @todo Refactor to 'suggestionclick'
75
+ sendAction({
76
+ type: actionTypes.custom,
77
+ originMessage: payload.id,
78
+ body: {
79
+ type: 'faqclick',
80
+ body: {
81
+ faqId: id,
82
+ faqQuestion: question,
83
+ },
84
+ },
85
+ })
86
+
87
+ addMessageBubble(question)
88
+ if (!isOpen) {
89
+ setVisibility(visibilityStates.open)
90
+ }
91
+ focusSkiplinkTarget()
92
+ },
93
+ [
94
+ addMessageBubble,
95
+ continueChat,
96
+ endCountdown,
97
+ focusSkiplinkTarget,
98
+ hasCountdown,
99
+ hasPrompt,
100
+ payload,
101
+ sendAction,
102
+ setVisibility,
103
+ isOpen,
104
+ ],
105
+ )
106
+
107
+ useEffect(() => {
108
+ if (prevSuggestions.current !== suggestions && !hideSuggestions) {
109
+ if (hasSuggestions) {
110
+ const politeText = prevHasSuggestions.current
111
+ ? t('suggestions.srUpdatedText')
112
+ : t('suggestions.srAvailableText')
113
+ setTimeout(() => {
114
+ sendPolite(politeText)
115
+ }, 30)
116
+ } else if (prevHasSuggestions.current) {
117
+ sendPolite(t('suggestions.srUnavailableText'))
118
+ }
119
+
120
+ prevSuggestions.current = suggestions
121
+ }
122
+
123
+ if (!prevHideSuggestions.current && hideSuggestions) {
124
+ runIfElementContainsOrHasFocus(containerRef.current, focusSkiplinkTarget)
125
+ sendPolite(t('suggestions.srUnavailableText'))
126
+ } else if (!hasSuggestions && prevHasSuggestions.current) {
127
+ runIfElementContainsOrHasFocus(containerRef.current, focusSkiplinkTarget)
128
+ }
129
+ prevHasSuggestions.current = hasSuggestions
130
+ prevHideSuggestions.current = hideSuggestions
131
+ }, [
132
+ suggestions,
133
+ hasSuggestions,
134
+ hideSuggestions,
135
+ focusSkiplinkTarget,
136
+ sendPolite,
137
+ t,
138
+ ])
139
+
140
+ const headingText = t('suggestions.headingText')
141
+ const footerText = t('suggestions.footerText')
142
+ const ContainerElement = headingText ? 'section' : 'div'
143
+
144
+ return (
145
+ <InOutTransition
146
+ isActive={showSuggestionsContainer}
147
+ transitionStartState={transitionStartStates.notRendered}
148
+ >
149
+ <ContainerElement
150
+ className={className('suggestions')}
151
+ aria-labelledby={headingText ? sectionId : null}
152
+ ref={containerRef}
153
+ >
154
+ {headingText && (
155
+ <h2 id={sectionId} className={className('suggestions__heading')}>
156
+ {headingText}
157
+ </h2>
158
+ )}
159
+ {!!renderedSuggestions.length && (
160
+ <SuggestionsList
161
+ hasIcon={true}
162
+ suggestions={renderedSuggestions}
163
+ onClickSuggestion={handleClick}
164
+ />
165
+ )}
166
+ {footerText && !isOpen && (
167
+ <p className={className('suggestions__footer')}>{footerText}</p>
168
+ )}
169
+ </ContainerElement>
170
+ </InOutTransition>
171
+ )
172
+ }
173
+
174
+ export default Suggestions
@@ -0,0 +1,40 @@
1
+ import { useMemo, useCallback } from 'preact/hooks'
2
+ import { className } from '../../../lib/css'
3
+ import Icon from '../layout/icon'
4
+
5
+ export const mapCategoryToClass = (category) =>
6
+ `suggestions__item--${String(category)
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9_\\-]/, '')}`
9
+
10
+ const SuggestionsItem = ({
11
+ id,
12
+ categories = [],
13
+ question,
14
+ onClick,
15
+ hasIcon,
16
+ }) => {
17
+ const mappedClassNames = useMemo(() => {
18
+ return ['suggestions-item', ...categories.map(mapCategoryToClass)]
19
+ }, [categories])
20
+
21
+ const handleClick = useCallback(() => {
22
+ if (onClick) {
23
+ onClick({ id, question })
24
+ }
25
+ }, [id, question, onClick])
26
+ return (
27
+ <li className={className(mappedClassNames)}>
28
+ <button
29
+ type="button"
30
+ onClick={handleClick}
31
+ className={className('button', 'button--primary')}
32
+ >
33
+ {hasIcon && <Icon name="chevronRight" size="8" />}
34
+ {question}
35
+ </button>
36
+ </li>
37
+ )
38
+ }
39
+
40
+ export default SuggestionsItem
@@ -0,0 +1,24 @@
1
+ import { className } from '../../../lib/css'
2
+ import SuggestionsItem from './suggestions-item'
3
+
4
+ const SuggestionsList = ({
5
+ className: givenClassName,
6
+ suggestions = [],
7
+ onClickSuggestion,
8
+ hasIcon,
9
+ }) => {
10
+ return (
11
+ <ul className={className('suggestions__list', givenClassName)}>
12
+ {suggestions.map((suggestion) => (
13
+ <SuggestionsItem
14
+ hasIcon={hasIcon}
15
+ key={suggestion.id}
16
+ onClick={onClickSuggestion}
17
+ {...suggestion}
18
+ />
19
+ ))}
20
+ </ul>
21
+ )
22
+ }
23
+
24
+ export default SuggestionsList
@@ -0,0 +1,21 @@
1
+ import AppFrame from '../layout/app-frame'
2
+ import Interrupt from '../layout/interrupt'
3
+ import Conversation from '../conversation/conversation'
4
+ import EntryContainer from '../entry/entry-container'
5
+ import ChatFrame from '../layout/chat-frame'
6
+ import useSeamlyChat from '../../hooks/use-seamly-chat'
7
+
8
+ const AppView = () => {
9
+ useSeamlyChat()
10
+
11
+ return (
12
+ <AppFrame>
13
+ <ChatFrame interruptComponent={Interrupt}>
14
+ <Conversation />
15
+ <EntryContainer />
16
+ </ChatFrame>
17
+ </AppFrame>
18
+ )
19
+ }
20
+
21
+ export default AppView
@@ -0,0 +1,30 @@
1
+ import AppFrame from '../layout/app-frame'
2
+ import ChatFrame from '../layout/chat-frame'
3
+ import AgentInfo from '../layout/agent-info'
4
+ import Header from '../layout/header'
5
+ import Conversation from '../conversation/conversation'
6
+ import EntryContainer from '../entry/entry-container'
7
+ import Interrupt from '../layout/interrupt'
8
+ import { useSeamlyChat } from '../../hooks/seamly-hooks'
9
+ import { useVisibility } from '../../../domains/visibility'
10
+
11
+ const DeprecatedView = () => {
12
+ const { isVisible } = useVisibility()
13
+ const { closeChat } = useSeamlyChat()
14
+
15
+ return (
16
+ isVisible && (
17
+ <AppFrame isDeprecated={true}>
18
+ <Header onCloseChat={closeChat}>
19
+ <AgentInfo />
20
+ </Header>
21
+ <ChatFrame interruptComponent={Interrupt}>
22
+ <Conversation />
23
+ <EntryContainer />
24
+ </ChatFrame>
25
+ </AppFrame>
26
+ )
27
+ )
28
+ }
29
+
30
+ export default DeprecatedView
@@ -0,0 +1,27 @@
1
+ import { useConfig } from '../../../domains/config'
2
+ import AppView from './app-view'
3
+ import InlineView from './inline-view'
4
+ import WindowView from './window-view'
5
+ import { useSeamlyStateContext } from '../../hooks/seamly-state-hooks'
6
+ import { visibilityStates } from '../../../domains/visibility'
7
+
8
+ const ViewComponentsMap = {
9
+ app: AppView,
10
+ inline: InlineView,
11
+ window: WindowView,
12
+ }
13
+
14
+ const View = () => {
15
+ const { layoutMode } = useConfig()
16
+ const { visible } = useSeamlyStateContext()
17
+ const ViewComponent = ViewComponentsMap[layoutMode]
18
+
19
+ if (!ViewComponent) {
20
+ console.warn('"layoutMode" should be one of "app", "inline" or "window"')
21
+ return null
22
+ }
23
+
24
+ return visible !== visibilityStates.hidden && <ViewComponent />
25
+ }
26
+
27
+ export default View
@@ -0,0 +1,45 @@
1
+ import { className } from '../../../lib/css'
2
+ import AppFrame from '../layout/app-frame'
3
+ import Interrupt from '../layout/interrupt'
4
+ import Conversation from '../conversation/conversation'
5
+ import EntryContainer from '../entry/entry-container'
6
+ import ChatFrame from '../layout/chat-frame'
7
+ import useSeamlyChat from '../../hooks/use-seamly-chat'
8
+ import { useVisibility } from '../../../domains/visibility'
9
+ import PreChatMessages from '../layout/pre-chat-messages'
10
+ import Suggestions from '../suggestions'
11
+ import InOutTransition, {
12
+ transitionStartStates,
13
+ } from '../widgets/in-out-transition'
14
+
15
+ const InlineView = () => {
16
+ useSeamlyChat()
17
+ const { isOpen } = useVisibility()
18
+
19
+ return (
20
+ <>
21
+ <InOutTransition
22
+ isActive={!isOpen}
23
+ transitionStartState={transitionStartStates.rendered}
24
+ >
25
+ <div className={className('unstarted-wrapper')}>
26
+ <PreChatMessages />
27
+ <Suggestions />
28
+ </div>
29
+ </InOutTransition>
30
+ <InOutTransition
31
+ isActive={isOpen}
32
+ transitionStartState={transitionStartStates.rendered}
33
+ >
34
+ <AppFrame>
35
+ <ChatFrame interruptComponent={Interrupt}>
36
+ {isOpen && <Conversation />}
37
+ <EntryContainer />
38
+ </ChatFrame>
39
+ </AppFrame>
40
+ </InOutTransition>
41
+ </>
42
+ )
43
+ }
44
+
45
+ export default InlineView
@@ -0,0 +1,20 @@
1
+ import { useCallback } from 'preact/hooks'
2
+ import { className } from '../../../../lib/css'
3
+ import Icon from '../../layout/icon'
4
+ import { useI18n } from '../../../../domains/i18n'
5
+
6
+ const CollapseButton = ({ onClick }) => {
7
+ const { t } = useI18n()
8
+ const handleClick = useCallback(() => onClick(), [onClick])
9
+ return (
10
+ <button
11
+ type="button"
12
+ className={className('button', 'collapse-button')}
13
+ onClick={handleClick}
14
+ >
15
+ <Icon name="chevronDown" size="32" alt={t('window.srCollapseButton')} />
16
+ </button>
17
+ )
18
+ }
19
+
20
+ export default CollapseButton
@@ -0,0 +1,82 @@
1
+ import { useMemo } from 'preact/hooks'
2
+ import { useSeamlyChat } from '../../../hooks/seamly-hooks'
3
+ import { useVisibility } from '../../../../domains/visibility'
4
+ import AppFrame from '../../layout/app-frame'
5
+ import ChatFrame from '../../layout/chat-frame'
6
+ import Interrupt from '../../layout/interrupt'
7
+ import Conversation from '../../conversation/conversation'
8
+ import EntryContainer from '../../entry/entry-container'
9
+ import WindowOpenButton from './window-open-button'
10
+ import CollapseButton from './collapse-button'
11
+ import InOutTransition, {
12
+ transitionStartStates,
13
+ } from '../../widgets/in-out-transition'
14
+ import PreChatMessages from '../../layout/pre-chat-messages'
15
+ import { useUserHasResponded } from '../../../../domains/app'
16
+ import { className } from '../../../../lib/css'
17
+ import Splash from '../../conversation/event/splash'
18
+ import { useI18n } from '../../../../domains/i18n'
19
+
20
+ const WindowView = () => {
21
+ const { openChat, closeChat } = useSeamlyChat()
22
+ const { isOpen } = useVisibility()
23
+ const hasResponded = useUserHasResponded()
24
+ const { t } = useI18n()
25
+ const continueChatText = t('window.chat.continue')
26
+ const continueChatEvent = useMemo(
27
+ () => ({
28
+ payload: {
29
+ body: {
30
+ text: continueChatText,
31
+ },
32
+ },
33
+ }),
34
+ [continueChatText],
35
+ )
36
+ return (
37
+ <>
38
+ <WindowOpenButton onClick={openChat} />
39
+ <InOutTransition
40
+ isActive={!isOpen && !hasResponded}
41
+ transitionStartState={transitionStartStates.rendered}
42
+ >
43
+ <div
44
+ className={className(
45
+ 'unstarted-wrapper',
46
+ 'unstarted-wrapper--window',
47
+ )}
48
+ >
49
+ <PreChatMessages />
50
+ </div>
51
+ </InOutTransition>
52
+ <InOutTransition
53
+ isActive={!isOpen && hasResponded}
54
+ transitionStartState={transitionStartStates.notRendered}
55
+ >
56
+ <div
57
+ className={className(
58
+ 'unstarted-wrapper',
59
+ 'unstarted-wrapper--window',
60
+ 'unstarted-wrapper--continue',
61
+ )}
62
+ >
63
+ <Splash event={continueChatEvent} />
64
+ </div>
65
+ </InOutTransition>
66
+ <InOutTransition
67
+ isActive={isOpen}
68
+ transitionStartState={transitionStartStates.notRendered}
69
+ >
70
+ <AppFrame>
71
+ {isOpen && <CollapseButton onClick={closeChat} />}
72
+ <ChatFrame interruptComponent={Interrupt}>
73
+ <Conversation />
74
+ <EntryContainer />
75
+ </ChatFrame>
76
+ </AppFrame>
77
+ </InOutTransition>
78
+ </>
79
+ )
80
+ }
81
+
82
+ export default WindowView
@@ -0,0 +1,68 @@
1
+ import { useCallback } from 'preact/hooks'
2
+ import { className } from '../../../../lib/css'
3
+ import {
4
+ useSeamlyCurrentAgent,
5
+ useSeamlyUnreadCount,
6
+ useSkiplinkTargetFocusing,
7
+ } from '../../../hooks/seamly-hooks'
8
+ import { useI18n } from '../../../../domains/i18n'
9
+ import { useConfig } from '../../../../domains/config'
10
+ import { useVisibility } from '../../../../domains/visibility'
11
+ import Icon from '../../layout/icon'
12
+ import { useInterrupt } from '../../../../domains/interrupt'
13
+ import InOutTransition, {
14
+ transitionStartStates,
15
+ } from '../../widgets/in-out-transition'
16
+ import { useSeamlyHasConversation } from '../../../hooks/seamly-api-hooks'
17
+
18
+ const ButtonIcon = () => {
19
+ const currentAgent = useSeamlyCurrentAgent()
20
+ const { hasInterrupt } = useInterrupt()
21
+ const { defaults } = useConfig()
22
+ const startChatIcon = defaults?.startChatIcon
23
+ const avatar = currentAgent && !hasInterrupt ? currentAgent.avatar : undefined
24
+ return avatar || startChatIcon ? (
25
+ <img
26
+ className={className(avatar ? 'avatar' : 'icon')}
27
+ src={avatar || startChatIcon}
28
+ alt=""
29
+ />
30
+ ) : (
31
+ <Icon name="avatar" size="32" />
32
+ )
33
+ }
34
+
35
+ const WindowOpenButton = ({ onClick }) => {
36
+ const hasConversation = useSeamlyHasConversation()
37
+ const { t } = useI18n()
38
+ const ariaLabel = hasConversation
39
+ ? t('window.openButton.srContinue')
40
+ : t('window.openButton.srStart')
41
+ const focusSkiplinkTarget = useSkiplinkTargetFocusing()
42
+ const { isOpen } = useVisibility()
43
+ const count = useSeamlyUnreadCount()
44
+ const handleClick = useCallback(() => {
45
+ onClick()
46
+ focusSkiplinkTarget()
47
+ }, [focusSkiplinkTarget, onClick])
48
+ return (
49
+ <InOutTransition
50
+ isActive={!isOpen}
51
+ transitionStartState={transitionStartStates.rendered}
52
+ >
53
+ <button
54
+ className={className('window-open-button')}
55
+ aria-label={ariaLabel}
56
+ aria-hidden={isOpen}
57
+ onClick={handleClick}
58
+ >
59
+ <span className={className('message-count')} aria-hidden="true">
60
+ {!!count && count}
61
+ </span>
62
+ <ButtonIcon />
63
+ </button>
64
+ </InOutTransition>
65
+ )
66
+ }
67
+
68
+ export default WindowOpenButton