@seamly/web-ui 20.0.0-beta.2 → 20.0.0-beta.3

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.
@@ -0,0 +1,162 @@
1
+ import { useEffect, useRef, useMemo } from 'preact/hooks'
2
+ import { className } from '../../../lib/css'
3
+ import Icon from '../layout/icon'
4
+ import { actionTypes } from '../../utils/seamly-utils'
5
+ import { runIfElementContainsOrHasFocus } from '../../utils/general-utils'
6
+ import useSeamlyCommands from '../../hooks/use-seamly-commands'
7
+ import {
8
+ useSeamlyServiceData,
9
+ useSeamlyLayoutMode,
10
+ } from '../../hooks/seamly-state-hooks'
11
+ import { useGeneratedId } from '../../hooks/utility-hooks'
12
+ import { useSkiplinkTargetFocusing } from '../../hooks/focus-helper-hooks'
13
+ import { useLiveRegion } from '../../hooks/live-region-hooks'
14
+ import useSeamlyIdleDetachCountdown from '../../hooks/use-seamly-idle-detach-countdown'
15
+ import useSeamlyResumeConversationPrompt from '../../hooks/use-seamly-resume-conversation-prompt'
16
+ import { useI18n } from '../../../domains/i18n'
17
+ import InOutTransition, {
18
+ transitionStartStates,
19
+ } from '../widgets/in-out-transition'
20
+ import { useTranslatedEventData } from '../../../domains/translations'
21
+ import { useInterrupt } from '../../../domains/interrupt'
22
+ import { useUserHasResponded } from '../../../domains/app'
23
+
24
+ const Faq = () => {
25
+ const { t } = useI18n()
26
+ const { sendAction, addMessageBubble } = useSeamlyCommands()
27
+ const sectionId = useGeneratedId()
28
+ const focusSkiplinkTarget = useSkiplinkTargetFocusing()
29
+ const { sendPolite } = useLiveRegion()
30
+ const { hasInterrupt } = useInterrupt()
31
+ const { hasCountdown, endCountdown } = useSeamlyIdleDetachCountdown()
32
+ const { hasPrompt, continueChat } = useSeamlyResumeConversationPrompt()
33
+
34
+ const lastFaqEventPayload = useSeamlyServiceData('suggestion')
35
+ const [eventBody] = useTranslatedEventData({ payload: lastFaqEventPayload })
36
+ const faqs = useMemo(() => {
37
+ const newFaqs = lastFaqEventPayload && !hasInterrupt ? eventBody : []
38
+ const itemBaseClass = `faqs__item`
39
+ return newFaqs.map(({ categories = [], ...faqRest }) => ({
40
+ ...faqRest,
41
+ categories,
42
+ classNames: [
43
+ itemBaseClass,
44
+ ...categories.map(
45
+ (cat) =>
46
+ `faqs__item--${String(cat)
47
+ .toLowerCase()
48
+ .replace(/[^a-z0-9_\\-]/, '')}`,
49
+ ),
50
+ ],
51
+ }))
52
+ }, [lastFaqEventPayload, hasInterrupt, eventBody])
53
+
54
+ const prevFaqs = useRef(null)
55
+ const prevHasFaqs = useRef(false)
56
+
57
+ const { isInline } = useSeamlyLayoutMode()
58
+ const hasResponded = useUserHasResponded()
59
+ const hideForWindow = !isInline && hasResponded
60
+ const prevHideForWindow = useRef(hideForWindow)
61
+
62
+ const hasFaqs = !!faqs.length
63
+ const showFaqContainer = hasFaqs && !hideForWindow
64
+ const previousRenderedFaqList = useRef([])
65
+ const renderedFaqList = hasFaqs ? faqs : previousRenderedFaqList.current
66
+ previousRenderedFaqList.current = renderedFaqList
67
+
68
+ const containerRef = useRef(null)
69
+
70
+ useEffect(() => {
71
+ if (prevFaqs.current !== faqs && !hideForWindow) {
72
+ if (hasFaqs) {
73
+ const politeText = prevHasFaqs.current
74
+ ? t('faq.srUpdatedText')
75
+ : t('faq.srAvailableText')
76
+ setTimeout(() => {
77
+ sendPolite(politeText)
78
+ }, 30)
79
+ } else if (prevHasFaqs.current) {
80
+ sendPolite(t('faq.srUnavailableText'))
81
+ }
82
+ prevFaqs.current = faqs
83
+ }
84
+
85
+ if (!prevHideForWindow.current && hideForWindow) {
86
+ runIfElementContainsOrHasFocus(containerRef.current, focusSkiplinkTarget)
87
+ sendPolite(t('faq.srUnavailableText'))
88
+ } else if (!hasFaqs && prevHasFaqs.current) {
89
+ runIfElementContainsOrHasFocus(containerRef.current, focusSkiplinkTarget)
90
+ }
91
+
92
+ prevHasFaqs.current = hasFaqs
93
+ prevHideForWindow.current = hideForWindow
94
+ }, [hasFaqs, faqs, hideForWindow, focusSkiplinkTarget, sendPolite, t])
95
+
96
+ const onFaqClickHandler = ({ id, question }) => {
97
+ if (hasCountdown) {
98
+ endCountdown(true)
99
+ }
100
+
101
+ if (hasPrompt) {
102
+ continueChat()
103
+ }
104
+
105
+ sendAction({
106
+ type: actionTypes.custom,
107
+ originMessage: lastFaqEventPayload.id,
108
+ body: {
109
+ type: 'faqclick',
110
+ body: {
111
+ faqId: id,
112
+ faqQuestion: question,
113
+ },
114
+ },
115
+ })
116
+
117
+ addMessageBubble(question)
118
+ focusSkiplinkTarget()
119
+ }
120
+
121
+ const headingText = t('faq.headingText')
122
+ const ContainerElement = headingText ? 'section' : 'div'
123
+
124
+ return (
125
+ <InOutTransition
126
+ isActive={showFaqContainer}
127
+ transitionStartState={transitionStartStates.notRendered}
128
+ >
129
+ <ContainerElement
130
+ className={className('faqs')}
131
+ aria-labelledby={headingText ? sectionId : null}
132
+ ref={containerRef}
133
+ >
134
+ {headingText && (
135
+ <h2 id={sectionId} className={className('faqs__heading')}>
136
+ {headingText}
137
+ </h2>
138
+ )}
139
+ {!!renderedFaqList.length && (
140
+ <ul className={className('faqs__list')}>
141
+ {renderedFaqList.map((faq) => (
142
+ <li key={faq.id.toString()} className={className(faq.classNames)}>
143
+ <button
144
+ type="button"
145
+ onClick={() => {
146
+ onFaqClickHandler(faq)
147
+ }}
148
+ className={className('button', 'button--secondary')}
149
+ >
150
+ <Icon name="chevronRight" size="8" />
151
+ {faq.question}
152
+ </button>
153
+ </li>
154
+ ))}
155
+ </ul>
156
+ )}
157
+ </ContainerElement>
158
+ </InOutTransition>
159
+ )
160
+ }
161
+
162
+ export default Faq
@@ -0,0 +1,86 @@
1
+ import { useCallback, useMemo } from 'preact/hooks'
2
+ import { className } from '../../../lib/css'
3
+ import {
4
+ useSeamlyAppContainerClassNames,
5
+ useSeamlyLayoutMode,
6
+ useSeamlyContainerElement,
7
+ } from '../../hooks/seamly-hooks'
8
+ import Faq from '../faq/faq'
9
+ import { useConfig } from '../../../domains/config'
10
+ import { useUserHasResponded } from '../../../domains/app'
11
+ import { useI18n } from '../../../domains/i18n'
12
+ import { useVisibility, visibilityStates } from '../../../domains/visibility'
13
+
14
+ const DeprecatedAppFrame = ({ children }) => {
15
+ const [, setSeamlyContainerElement] = useSeamlyContainerElement()
16
+ const { isOpen, isVisible, setVisibility } = useVisibility()
17
+ const { zIndex, showFaq, layoutMode } = useConfig()
18
+ const { isModal, isInline } = useSeamlyLayoutMode()
19
+ const appContainerClassNames = useSeamlyAppContainerClassNames()
20
+ const userResponded = useUserHasResponded()
21
+ const { locale } = useI18n()
22
+
23
+ const containerElementRef = useCallback(
24
+ (container) => {
25
+ setSeamlyContainerElement(container)
26
+ },
27
+ [setSeamlyContainerElement],
28
+ )
29
+
30
+ const blockLang = useMemo(() => {
31
+ if (locale) {
32
+ const htmlElementLang = document
33
+ .querySelector('html')
34
+ .getAttribute('lang')
35
+ if (htmlElementLang !== locale) {
36
+ return locale
37
+ }
38
+ }
39
+ return undefined
40
+ }, [locale])
41
+
42
+ const classNames = ['app', 'app--deprecated', ...appContainerClassNames]
43
+
44
+ if (!isOpen && layoutMode === 'window') {
45
+ classNames.push('app--collapsed')
46
+ }
47
+
48
+ if (userResponded) {
49
+ classNames.push('app--user-responded')
50
+ }
51
+
52
+ classNames.push(`app--layout-${layoutMode}`)
53
+
54
+ const onKeyDownHandler = (e) => {
55
+ if ((e.code && e.code === 'Escape') || e.keyCode === 27)
56
+ if (!isInline && isOpen) {
57
+ setVisibility(visibilityStates.minimized)
58
+ }
59
+ }
60
+
61
+ const onClickHandler = (e) => {
62
+ if (isModal) {
63
+ e.stopPropagation()
64
+ }
65
+ }
66
+
67
+ return (
68
+ isVisible && (
69
+ <section
70
+ className={className(classNames)}
71
+ onKeyDown={onKeyDownHandler}
72
+ onClick={onClickHandler}
73
+ lang={blockLang}
74
+ tabIndex="-1"
75
+ ref={containerElementRef}
76
+ style={{ zIndex }}
77
+ data-nosnippet
78
+ >
79
+ <div className={className('app-wrapper')}>{children}</div>
80
+ {showFaq && <Faq />}
81
+ </section>
82
+ )
83
+ )
84
+ }
85
+
86
+ export default DeprecatedAppFrame
@@ -1,4 +1,4 @@
1
- import AppFrame from '../layout/app-frame'
1
+ import DeprecatedAppFrame from '../layout/deprecated-app-frame'
2
2
  import ChatFrame from '../layout/chat-frame'
3
3
  import AgentInfo from '../layout/agent-info'
4
4
  import Header from '../layout/header'
@@ -7,14 +7,16 @@ import EntryContainer from '../entry/entry-container'
7
7
  import Interrupt from '../layout/interrupt'
8
8
  import { useSeamlyChat } from '../../hooks/seamly-hooks'
9
9
  import { useVisibility } from '../../../domains/visibility'
10
+ import DeprecatedToggleButton from '../entry/toggle-button'
10
11
 
11
12
  const DeprecatedView = () => {
12
13
  const { isVisible } = useVisibility()
13
- const { closeChat } = useSeamlyChat()
14
+ const { closeChat, openChat } = useSeamlyChat()
14
15
 
15
16
  return (
16
17
  isVisible && (
17
- <AppFrame isDeprecated={true}>
18
+ <DeprecatedAppFrame>
19
+ <DeprecatedToggleButton onOpenChat={openChat} />
18
20
  <Header onCloseChat={closeChat}>
19
21
  <AgentInfo />
20
22
  </Header>
@@ -22,7 +24,7 @@ const DeprecatedView = () => {
22
24
  <Conversation />
23
25
  <EntryContainer />
24
26
  </ChatFrame>
25
- </AppFrame>
27
+ </DeprecatedAppFrame>
26
28
  )
27
29
  )
28
30
  }
@@ -3,17 +3,15 @@
3
3
 
4
4
  // BASE
5
5
  // ----
6
- .#{$n}-error__message {
7
- display: flex;
8
- align-items: flex-start;
6
+ .#{$n}-error {
7
+ display: block;
9
8
  width: 100%;
10
- margin: 0 0 $spacer * 0.5;
11
- padding: $spacer * 0.25 $spacer * 0.5;
12
- border-radius: $borderradius-small;
13
- background-color: rgba($negative, 0.1);
14
- color: $negative-dark;
15
- font-size: $fontsize-small;
16
- font-weight: $fontweight-bold;
9
+ margin: 0 0 $spacer * 0.25;
10
+
11
+ &:empty {
12
+ display: none;
13
+ margin: 0;
14
+ }
17
15
 
18
16
  .#{$n}-icon {
19
17
  flex: 0 0 16px;
@@ -21,4 +19,16 @@
21
19
  height: 16px;
22
20
  margin-right: $spacer * 0.25;
23
21
  }
22
+
23
+ .#{$n}-error__message {
24
+ display: flex;
25
+ align-items: flex-start;
26
+ width: 100%;
27
+ padding: $spacer * 0.25 $spacer * 0.5;
28
+ border-radius: $borderradius-small;
29
+ background-color: rgba($negative, 0.1);
30
+ color: $negative-dark;
31
+ font-size: $fontsize-small;
32
+ font-weight: $fontweight-bold;
33
+ }
24
34
  }
@@ -4,16 +4,14 @@
4
4
  // BASE
5
5
  // ----
6
6
  .#{$n}-error {
7
- display: flex;
8
- align-items: flex-start;
7
+ display: block;
9
8
  width: 100%;
10
- margin: 0 0 $spacer * 0.5;
11
- padding: $spacer * 0.25 $spacer * 0.5;
12
- border-radius: $borderradius-small;
13
- background-color: rgba($negative, 0.1);
14
- color: $negative-dark;
15
- font-size: $fontsize-small;
16
- font-weight: $fontweight-bold;
9
+ margin: 0 0 $spacer * 0.25;
10
+
11
+ &:empty {
12
+ display: none;
13
+ margin: 0;
14
+ }
17
15
 
18
16
  .#{$n}-icon {
19
17
  flex: 0 0 16px;
@@ -21,4 +19,16 @@
21
19
  height: 16px;
22
20
  margin-right: $spacer * 0.25;
23
21
  }
22
+
23
+ .#{$n}-error__message {
24
+ display: flex;
25
+ align-items: flex-start;
26
+ width: 100%;
27
+ padding: $spacer * 0.25 $spacer * 0.5;
28
+ border-radius: $borderradius-small;
29
+ background-color: rgba($negative, 0.1);
30
+ color: $negative-dark;
31
+ font-size: $fontsize-small;
32
+ font-weight: $fontweight-bold;
33
+ }
24
34
  }
@@ -3,7 +3,7 @@
3
3
 
4
4
  // BASE
5
5
  // ----
6
- .#{$n}-input {
6
+ .#{$n}-entry-form {
7
7
  display: flex;
8
8
  flex: 1 1 100%;
9
9
  width: 100%;
@@ -191,8 +191,6 @@
191
191
  display: flex;
192
192
  align-items: center;
193
193
  justify-content: center;
194
- width: 100%;
195
- height: 100%;
196
194
  }
197
195
 
198
196
  .#{$n}-options__body form {
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable */
2
2
  const path = require('path')
3
+ const webpack = require('webpack')
3
4
  const webpackMerge = require('webpack-merge').merge
4
5
  const site = require('@seamly/doc-site/lib/config/site')
5
6
  const BundleAnalyzerPlugin =
@@ -28,7 +29,12 @@ module.exports = (env = {}, argv = {}, configOverrides = {}) => {
28
29
 
29
30
  const PRODUCTION = [argv.mode, process.env.NODE_ENV].includes('production')
30
31
 
31
- const plugins = []
32
+ const plugins = [
33
+ new webpack.DefinePlugin({
34
+ 'process.env.API_DOMAIN': JSON.stringify(process.env.API_DOMAIN),
35
+ }),
36
+ ]
37
+
32
38
  if (env.analyze) {
33
39
  plugins.push(new BundleAnalyzerPlugin())
34
40
  }
@@ -26,6 +26,7 @@ module.exports = (env = {}, argv = {}, configOverrides = {}, options = {}) => {
26
26
  entry: {
27
27
  // Demo and test files
28
28
  'tests/index': path.join(PUBLIC_ROOT, '/tests/index.js'),
29
+ 'tests/deprecated': path.join(PUBLIC_ROOT, '/tests/deprecated.js'),
29
30
  'tests/style-guide': path.join(PUBLIC_ROOT, '/tests/style-guide.js'),
30
31
  'tests/init-with-callback/index': path.join(
31
32
  PUBLIC_ROOT,