@stack-spot/portal-components 2.27.0 → 2.27.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +635 -621
- package/dist/components/AnimatedHeight.d.ts +1 -1
- package/dist/components/AnimatedHeight.js +26 -26
- package/dist/components/AsyncContent.d.ts +1 -1
- package/dist/components/AsyncContent.js +1 -1
- package/dist/components/BannerWarning.d.ts +1 -1
- package/dist/components/BannerWarning.js +1 -1
- package/dist/components/Breadcrumb/index.d.ts +2 -2
- package/dist/components/Breadcrumb/index.js +1 -1
- package/dist/components/Breadcrumb/styled.js +31 -31
- package/dist/components/ButtonLoading.d.ts +1 -1
- package/dist/components/ButtonLoading.js +1 -1
- package/dist/components/ChatBot.d.ts +1 -1
- package/dist/components/ChatBot.js +1 -1
- package/dist/components/ContentValidateFilter.d.ts +1 -1
- package/dist/components/ContentValidateFilter.js +1 -1
- package/dist/components/FadingOverflow.d.ts +1 -1
- package/dist/components/FadingOverflow.js +69 -69
- package/dist/components/FileTreeView/More.d.ts +1 -1
- package/dist/components/FileTreeView/More.js +1 -1
- package/dist/components/FileTreeView/index.d.ts +1 -1
- package/dist/components/FileTreeView/index.js +1 -1
- package/dist/components/InfiniteScroll.d.ts +1 -1
- package/dist/components/InfiniteScroll.js +1 -1
- package/dist/components/InfoMaintenanceBanner.d.ts +1 -1
- package/dist/components/InfoMaintenanceBanner.js +2 -2
- package/dist/components/LazyMarkdown/BlockquoteMd.d.ts +1 -1
- package/dist/components/LazyMarkdown/BlockquoteMd.js +1 -1
- package/dist/components/LazyMarkdown/CodeViewer.d.ts +1 -1
- package/dist/components/LazyMarkdown/CodeViewer.js +76 -76
- package/dist/components/LazyMarkdown/Markdown.d.ts +1 -1
- package/dist/components/LazyMarkdown/Markdown.js +1 -1
- package/dist/components/LazyMarkdown/MarkdownButton.d.ts +1 -1
- package/dist/components/LazyMarkdown/MarkdownButton.js +1 -1
- package/dist/components/LazyMarkdown/Video.d.ts +1 -1
- package/dist/components/LazyMarkdown/Video.js +1 -1
- package/dist/components/LazyMarkdown/index.d.ts +1 -1
- package/dist/components/LazyMarkdown/index.js +1 -1
- package/dist/components/Placeholder.d.ts +7 -3
- package/dist/components/Placeholder.d.ts.map +1 -1
- package/dist/components/Placeholder.js +3 -3
- package/dist/components/Placeholder.js.map +1 -1
- package/dist/components/ScrollView.js +16 -16
- package/dist/components/Select/BadgeItem.d.ts +1 -1
- package/dist/components/Select/BadgeItem.js +1 -1
- package/dist/components/Select/ClearInput.d.ts +1 -1
- package/dist/components/Select/ClearInput.js +1 -1
- package/dist/components/Select/CloseItem.d.ts +1 -1
- package/dist/components/Select/CloseItem.js +1 -1
- package/dist/components/Select/CreatableSelect.js +1 -1
- package/dist/components/Select/CustomMenu.d.ts +1 -1
- package/dist/components/Select/CustomMenu.js +1 -1
- package/dist/components/Select/LabelItem.d.ts +1 -1
- package/dist/components/Select/LabelItem.js +1 -1
- package/dist/components/Select/MultiValue.d.ts +1 -1
- package/dist/components/Select/MultiValue.js +1 -1
- package/dist/components/Select/SelectInfiniteScroll.d.ts +1 -1
- package/dist/components/Select/SelectInfiniteScroll.js +1 -1
- package/dist/components/Select/SelectSearch.d.ts +1 -1
- package/dist/components/Select/SelectSearch.js +1 -1
- package/dist/components/SelectionList.d.ts +1 -1
- package/dist/components/SelectionList.js +61 -61
- package/dist/components/StatusCircle.d.ts +1 -1
- package/dist/components/StatusCircle.js +6 -6
- package/dist/components/Stepper/Navigation.js +4 -4
- package/dist/components/Stepper/Step.js +3 -3
- package/dist/components/Stepper/Stepper.js +6 -6
- package/dist/components/Stepper/headers.js +22 -22
- package/dist/components/Table/HeaderItem.js +1 -1
- package/dist/components/Table/SettingsVerticalMenu.d.ts +1 -1
- package/dist/components/Table/SettingsVerticalMenu.js +1 -1
- package/dist/components/Table/StyledLinkTable.d.ts +1 -1
- package/dist/components/Table/StyledLinkTable.js +5 -5
- package/dist/components/Table/TableData.d.ts +1 -1
- package/dist/components/Table/TableData.js +25 -25
- package/dist/components/TimelineSection.d.ts +1 -1
- package/dist/components/TimelineSection.js +14 -14
- package/dist/components/error/ErrorFeedback.d.ts +1 -1
- package/dist/components/error/ErrorFeedback.js +35 -35
- package/dist/components/error/NotFound.d.ts +1 -1
- package/dist/components/error/NotFound.js +1 -1
- package/dist/components/error/UnderMaintenance.d.ts +1 -1
- package/dist/components/error/UnderMaintenance.js +1 -1
- package/dist/components/form/Form/Form.d.ts +1 -1
- package/dist/components/form/Form/Form.js +1 -1
- package/dist/components/form/Form/FormGroup.d.ts +2 -2
- package/dist/components/form/Form/FormGroup.js +1 -1
- package/dist/components/form/SearchInput.d.ts +1 -1
- package/dist/components/form/SearchInput.js +1 -1
- package/dist/components/form/Select/CustomSelect.d.ts +1 -1
- package/dist/components/form/Select/CustomSelect.js +1 -1
- package/dist/components/form/Select/DetailedSelect.d.ts +1 -1
- package/dist/components/form/Select/DetailedSelect.js +1 -1
- package/dist/components/form/Select/Select.d.ts +1 -1
- package/dist/components/form/Select/Select.js +1 -1
- package/dist/components/form/Select/styled.js +161 -161
- package/dist/components/form/Select/utils.js +1 -1
- package/dist/components/notification/NotificationComponent.d.ts +1 -1
- package/dist/components/notification/NotificationComponent.js +54 -54
- package/dist/components/notification/NotificationItem.d.ts +1 -1
- package/dist/components/notification/NotificationItem.d.ts.map +1 -1
- package/dist/components/notification/NotificationItem.js +11 -5
- package/dist/components/notification/NotificationItem.js.map +1 -1
- package/dist/components/notification/NotificationList.d.ts +1 -1
- package/dist/components/notification/NotificationList.d.ts.map +1 -1
- package/dist/components/notification/NotificationList.js +44 -44
- package/dist/components/notification/NotificationList.js.map +1 -1
- package/dist/components/notification/NotificationPlaceholder.d.ts +1 -1
- package/dist/components/notification/NotificationPlaceholder.d.ts.map +1 -1
- package/dist/components/notification/NotificationPlaceholder.js +2 -2
- package/dist/components/notification/NotificationPlaceholder.js.map +1 -1
- package/dist/containers/NotificationsPage.d.ts +1 -1
- package/dist/containers/NotificationsPage.d.ts.map +1 -1
- package/dist/containers/NotificationsPage.js +24 -11
- package/dist/containers/NotificationsPage.js.map +1 -1
- package/dist/context/anchor.d.ts +1 -1
- package/dist/context/anchor.js +1 -1
- package/dist/context/loading.d.ts +1 -1
- package/dist/context/loading.js +1 -1
- package/dist/context/notification/context.d.ts +1 -1
- package/dist/context/notification/context.js +1 -1
- package/dist/context/notification/types.d.ts +1 -0
- package/dist/context/notification/types.d.ts.map +1 -1
- package/dist/hooks/date.js +1 -1
- package/dist/hooks/service-now.js +28 -28
- package/dist/svg/AI.d.ts +1 -1
- package/dist/svg/AI.js +1 -1
- package/dist/svg/CS.d.ts +1 -1
- package/dist/svg/CS.js +1 -1
- package/dist/svg/EDP.d.ts +1 -1
- package/dist/svg/EDP.js +1 -1
- package/dist/svg/Forbidden.d.ts +1 -1
- package/dist/svg/Forbidden.js +1 -1
- package/dist/svg/GenericPlaceholder.d.ts +4 -2
- package/dist/svg/GenericPlaceholder.d.ts.map +1 -1
- package/dist/svg/GenericPlaceholder.js +2 -2
- package/dist/svg/GenericPlaceholder.js.map +1 -1
- package/dist/svg/HUB.d.ts +1 -1
- package/dist/svg/HUB.js +1 -1
- package/dist/svg/Logo.d.ts +1 -1
- package/dist/svg/Logo.js +1 -1
- package/dist/svg/MiniLogo.d.ts +1 -1
- package/dist/svg/MiniLogo.js +1 -1
- package/dist/svg/NotFound.d.ts +1 -1
- package/dist/svg/NotFound.js +1 -1
- package/dist/svg/ServerError.d.ts +1 -1
- package/dist/svg/ServerError.js +1 -1
- package/dist/svg/Unauthenticated.d.ts +1 -1
- package/dist/svg/Unauthenticated.js +1 -1
- package/package.json +6 -6
- package/readme.md +66 -66
- package/src/components/AnimatedHeight.tsx +174 -174
- package/src/components/AsyncContent.tsx +78 -78
- package/src/components/BannerWarning.tsx +91 -91
- package/src/components/Breadcrumb/index.tsx +76 -76
- package/src/components/Breadcrumb/styled.ts +37 -37
- package/src/components/ButtonLoading.tsx +29 -29
- package/src/components/ChatBot.tsx +82 -82
- package/src/components/ContentValidateFilter.tsx +15 -15
- package/src/components/FadingOverflow.tsx +265 -265
- package/src/components/FileTreeView/More.tsx +114 -114
- package/src/components/FileTreeView/index.tsx +186 -186
- package/src/components/InfiniteScroll.tsx +24 -24
- package/src/components/InfoMaintenanceBanner.tsx +29 -29
- package/src/components/LazyMarkdown/BlockquoteMd.tsx +107 -107
- package/src/components/LazyMarkdown/CodeViewer.tsx +161 -161
- package/src/components/LazyMarkdown/Markdown.tsx +122 -122
- package/src/components/LazyMarkdown/MarkdownButton.tsx +24 -24
- package/src/components/LazyMarkdown/Video.tsx +13 -13
- package/src/components/LazyMarkdown/index.tsx +21 -21
- package/src/components/Placeholder.tsx +123 -118
- package/src/components/ScrollView.tsx +57 -57
- package/src/components/Select/BadgeItem.tsx +58 -58
- package/src/components/Select/ClearInput.tsx +24 -24
- package/src/components/Select/CloseItem.tsx +38 -38
- package/src/components/Select/CreatableSelect.tsx +155 -155
- package/src/components/Select/CustomMenu.tsx +16 -16
- package/src/components/Select/LabelItem.tsx +8 -8
- package/src/components/Select/MultiValue.tsx +49 -49
- package/src/components/Select/SelectInfiniteScroll.tsx +82 -82
- package/src/components/Select/SelectSearch.tsx +195 -195
- package/src/components/Select/index.tsx +7 -7
- package/src/components/Select/types.ts +8 -8
- package/src/components/SelectionList.tsx +427 -427
- package/src/components/StatusCircle.tsx +67 -67
- package/src/components/Stepper/Navigation.tsx +97 -97
- package/src/components/Stepper/Step.tsx +30 -30
- package/src/components/Stepper/Stepper.tsx +113 -113
- package/src/components/Stepper/headers.tsx +64 -64
- package/src/components/Stepper/index.ts +3 -3
- package/src/components/Table/HeaderItem.tsx +52 -52
- package/src/components/Table/SettingsVerticalMenu.tsx +50 -50
- package/src/components/Table/StyledLinkTable.tsx +22 -22
- package/src/components/Table/TableData.tsx +251 -251
- package/src/components/Table/index.tsx +2 -2
- package/src/components/TimelineSection.tsx +66 -66
- package/src/components/error/ErrorFeedback.tsx +217 -217
- package/src/components/error/NotFound.tsx +24 -24
- package/src/components/error/UnderMaintenance.tsx +30 -30
- package/src/components/error/index.ts +4 -4
- package/src/components/form/Form/Form.tsx +101 -101
- package/src/components/form/Form/FormGroup.tsx +221 -221
- package/src/components/form/Form/index.ts +2 -2
- package/src/components/form/SearchInput.tsx +69 -69
- package/src/components/form/Select/CustomSelect.tsx +232 -232
- package/src/components/form/Select/DetailedSelect.tsx +85 -85
- package/src/components/form/Select/Select.tsx +67 -67
- package/src/components/form/Select/index.ts +4 -4
- package/src/components/form/Select/styled.ts +165 -165
- package/src/components/form/Select/types.ts +112 -112
- package/src/components/form/Select/utils.tsx +28 -28
- package/src/components/notification/NotificationComponent.tsx +340 -340
- package/src/components/notification/NotificationItem.tsx +345 -336
- package/src/components/notification/NotificationList.tsx +179 -178
- package/src/components/notification/NotificationPlaceholder.tsx +44 -43
- package/src/components/notification/types.ts +72 -72
- package/src/containers/NotificationsPage.tsx +119 -98
- package/src/context/anchor.tsx +37 -37
- package/src/context/loading.tsx +36 -36
- package/src/context/notification/LazyNotificationList.ts +103 -103
- package/src/context/notification/NotificationController.ts +104 -104
- package/src/context/notification/context.tsx +23 -23
- package/src/context/notification/hooks.ts +98 -98
- package/src/context/notification/types.ts +66 -65
- package/src/hooks/date.ts +31 -31
- package/src/hooks/keyboard.tsx +128 -128
- package/src/hooks/manual-render.tsx +10 -10
- package/src/hooks/service-now.tsx +233 -233
- package/src/hooks/text.tsx +30 -30
- package/src/hooks/title.tsx +28 -28
- package/src/hooks/use-effect-once.tsx +43 -43
- package/src/index.ts +19 -19
- package/src/notifications.ts +11 -11
- package/src/svg/AI.tsx +41 -41
- package/src/svg/CS.tsx +48 -48
- package/src/svg/EDP.tsx +31 -31
- package/src/svg/Forbidden.tsx +22 -22
- package/src/svg/GenericPlaceholder.tsx +20 -20
- package/src/svg/HUB.tsx +48 -48
- package/src/svg/Logo.tsx +16 -16
- package/src/svg/MiniLogo.tsx +12 -12
- package/src/svg/NotFound.tsx +16 -16
- package/src/svg/ServerError.tsx +33 -33
- package/src/svg/Unauthenticated.tsx +16 -16
- package/src/svg/index.ts +11 -11
- package/src/utils/accessibility.ts +135 -135
- package/src/utils/cookie.ts +73 -73
- package/src/utils/promise.ts +5 -5
- package/src/utils/read-file.ts +16 -16
- package/tsconfig.json +10 -10
|
@@ -1,233 +1,233 @@
|
|
|
1
|
-
|
|
2
|
-
import { Dictionary, getLanguage, ptEn, translate } from '@stack-spot/portal-translate'
|
|
3
|
-
import { pull } from 'lodash'
|
|
4
|
-
import { useState } from 'react'
|
|
5
|
-
import { useEffectOnce } from './use-effect-once'
|
|
6
|
-
|
|
7
|
-
const CHAT_IFRAME_ID = 'sn_va_web_client'
|
|
8
|
-
export const CHAT_BUTTON_ID = 'service-now-button'
|
|
9
|
-
export const CHAT_BUTTON_WRAPPER_ID = 'service-now-content'
|
|
10
|
-
|
|
11
|
-
const closeButtonStyle = `
|
|
12
|
-
position: absolute;
|
|
13
|
-
bottom: -64px;
|
|
14
|
-
right: 60px;
|
|
15
|
-
width: 20px;
|
|
16
|
-
height: 20px;
|
|
17
|
-
display: flex;
|
|
18
|
-
align-items: center;
|
|
19
|
-
justify-content: center;
|
|
20
|
-
background-color: #BCBCCF;
|
|
21
|
-
border: none;
|
|
22
|
-
outline: none;
|
|
23
|
-
border-radius: 50%;
|
|
24
|
-
font: 400 20px Roboto, sans-serif;
|
|
25
|
-
color: #0B0B0E;
|
|
26
|
-
cursor: pointer;
|
|
27
|
-
transform: scale(0);
|
|
28
|
-
transition: transform 0.2s;
|
|
29
|
-
z-index: 999999;
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
const iframeStyle = `
|
|
33
|
-
position: fixed;
|
|
34
|
-
display: none;
|
|
35
|
-
right: 15px;
|
|
36
|
-
bottom: 88px;
|
|
37
|
-
max-height: calc(100vh - 152px);
|
|
38
|
-
border-radius: 12px;
|
|
39
|
-
width: 380px;
|
|
40
|
-
height: 600px;
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
type ChatVisibilityListener = (visible: boolean) => void
|
|
44
|
-
|
|
45
|
-
interface Options {
|
|
46
|
-
/**
|
|
47
|
-
* Function to run when the chat button is hidden, i.e. when the user clicks the close button that appears when the chat button is
|
|
48
|
-
* hovered.
|
|
49
|
-
*/
|
|
50
|
-
onClose?: () => void,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const HOVER_AREA_PX = 100
|
|
54
|
-
const LOCAL_STORAGE_SERVICE_NOW_VALUE = 'serviceNowChatButtonVisible'
|
|
55
|
-
const chatVisibilityListeners: ChatVisibilityListener[] = []
|
|
56
|
-
|
|
57
|
-
const serviceNowReference = 'https://zup.service-now.com/sn_va_web_client_app_embed.do?sysparm_branding_key=stackspotvirtualagent&sysparm_topic=f2b636c11bb50e50fe670dcbe54bcb1d'
|
|
58
|
-
|
|
59
|
-
function loadScript(src: string): Promise<void> {
|
|
60
|
-
return new Promise<void>((resolve, reject) => {
|
|
61
|
-
const iframe = document.createElement('iframe')
|
|
62
|
-
const serviceNowContent = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
|
|
63
|
-
iframe.setAttribute('id', CHAT_IFRAME_ID)
|
|
64
|
-
iframe.setAttribute('src', src)
|
|
65
|
-
iframe.style.cssText = iframeStyle
|
|
66
|
-
|
|
67
|
-
iframe.onload = () => resolve()
|
|
68
|
-
iframe.onerror = (error) => reject(error)
|
|
69
|
-
|
|
70
|
-
serviceNowContent && serviceNowContent.appendChild(iframe)
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @returns the iframe loaded by service now, i.e. the chat window; or null if the chat window doesn't exist.
|
|
76
|
-
*/
|
|
77
|
-
export function getServiceNowLauncher() {
|
|
78
|
-
return document.getElementById(CHAT_IFRAME_ID)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Opens the chat window and makes the chat button visible.
|
|
83
|
-
*/
|
|
84
|
-
export function openServiceNowChat() {
|
|
85
|
-
hideOrShowServiceNowContent('visible')
|
|
86
|
-
showChatWindow()
|
|
87
|
-
showServiceNowChatButton()
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function getServiceNowButton() {
|
|
91
|
-
return document.getElementById(CHAT_BUTTON_ID)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function hideOrShowServiceNowContent(action: 'visible' | 'hidden') {
|
|
95
|
-
const content = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
|
|
96
|
-
if (content) {
|
|
97
|
-
content.style.visibility = action
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function toggleChatWindow(show: boolean) {
|
|
102
|
-
const launcher = getServiceNowLauncher()
|
|
103
|
-
const content = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
|
|
104
|
-
if (!launcher || !content) return
|
|
105
|
-
const [action, display] = show ? ['add', 'block'] as const : ['remove', 'none'] as const
|
|
106
|
-
launcher.classList[action]('show')
|
|
107
|
-
content.classList[action]('opened')
|
|
108
|
-
launcher.style.display = display
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Shows the chat window, i.e. shows the service now iframe and switch the icon in the chat button to "close".
|
|
113
|
-
*/
|
|
114
|
-
export function showChatWindow() {
|
|
115
|
-
toggleChatWindow(true)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Hides the chat window, i.e. hides the service now iframe and switch the icon in the chat button to "chat".
|
|
120
|
-
*/
|
|
121
|
-
export function hideChatWindow() {
|
|
122
|
-
toggleChatWindow(false)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Adds a listener that is called whenever the visibility of the chat button changes.
|
|
127
|
-
*
|
|
128
|
-
* The listener is called with a single parameter which indicates if the button is visible or not.
|
|
129
|
-
* @param listener the listener to register.
|
|
130
|
-
* @returns a function that, when called, removes the listener.
|
|
131
|
-
*/
|
|
132
|
-
function addChatVisibilityListener(listener: ChatVisibilityListener) {
|
|
133
|
-
chatVisibilityListeners.push(listener)
|
|
134
|
-
return () => pull(chatVisibilityListeners, listener)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function renderCloseButton(onClose?: () => void) {
|
|
138
|
-
const serviceNowButton = getServiceNowButton()
|
|
139
|
-
const anchor = serviceNowButton?.parentElement
|
|
140
|
-
if (!anchor) return
|
|
141
|
-
const close = document.createElement('button')
|
|
142
|
-
close.setAttribute('id', 'close-service-now')
|
|
143
|
-
close.style.cssText = closeButtonStyle
|
|
144
|
-
close.textContent = '-'
|
|
145
|
-
|
|
146
|
-
close.addEventListener('click', () => hideServiceNowChatButton(onClose))
|
|
147
|
-
anchor.appendChild(close)
|
|
148
|
-
|
|
149
|
-
serviceNowButton.addEventListener('mouseenter', () => {
|
|
150
|
-
const t = translate(dictionary, getLanguage(ptEn))
|
|
151
|
-
close.setAttribute('title', t.hide)
|
|
152
|
-
close.style.transform = 'scale(1)'
|
|
153
|
-
|
|
154
|
-
const onMouseMove = (ev: MouseEvent) => {
|
|
155
|
-
if (ev.clientX < window.innerWidth - HOVER_AREA_PX || ev.clientY < window.innerHeight - HOVER_AREA_PX) {
|
|
156
|
-
close.style.transform = 'scale(0)'
|
|
157
|
-
window.removeEventListener('mousemove', onMouseMove)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
window.addEventListener('mousemove', onMouseMove)
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function showServiceNowChatButton() {
|
|
165
|
-
getServiceNowButton()?.classList?.add('show')
|
|
166
|
-
localStorage.setItem(LOCAL_STORAGE_SERVICE_NOW_VALUE, 'true')
|
|
167
|
-
chatVisibilityListeners.forEach(l => l(true))
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function hideServiceNowChatButton(onClose?: () => void) {
|
|
171
|
-
hideOrShowServiceNowContent('hidden')
|
|
172
|
-
hideChatWindow()
|
|
173
|
-
getServiceNowButton()?.classList?.remove('show')
|
|
174
|
-
localStorage.setItem(LOCAL_STORAGE_SERVICE_NOW_VALUE, 'false')
|
|
175
|
-
onClose?.()
|
|
176
|
-
chatVisibilityListeners.forEach(l => l(false))
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function isServiceNowChatButtonVisible() {
|
|
180
|
-
return localStorage.getItem(LOCAL_STORAGE_SERVICE_NOW_VALUE) !== 'false'
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function initializeChat({ onClose }: Options = {}) {
|
|
184
|
-
try {
|
|
185
|
-
await loadScript(serviceNowReference)
|
|
186
|
-
if (isServiceNowChatButtonVisible()) showServiceNowChatButton()
|
|
187
|
-
else hideServiceNowChatButton()
|
|
188
|
-
renderCloseButton(onClose)
|
|
189
|
-
} catch {
|
|
190
|
-
const t = translate(dictionary, getLanguage(ptEn))
|
|
191
|
-
// eslint-disable-next-line no-console
|
|
192
|
-
console.warn(t.errorLoadScript)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* A react hook that indicates whether the chat button is visible or not. This is updated whenever the value changes.
|
|
198
|
-
* @returns true if the chat button is visible and false otherwise.
|
|
199
|
-
*/
|
|
200
|
-
export function useServiceNowChatButtonVisibility() {
|
|
201
|
-
const [visible, setVisible] = useState(isServiceNowChatButtonVisible())
|
|
202
|
-
useEffectOnce(() => addChatVisibilityListener(setVisible))
|
|
203
|
-
return visible
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Starts Service Now:
|
|
208
|
-
*
|
|
209
|
-
* - Creates the chat window feature by loading the required script.
|
|
210
|
-
* - Adds a "minimize" button on top of the chat button (left bottom corner). This button appears whenever the user hovers the chat button.
|
|
211
|
-
* If clicked, the chat button is hidden.
|
|
212
|
-
* @param options
|
|
213
|
-
*/
|
|
214
|
-
export function useServiceNowEffect(options?: Options) {
|
|
215
|
-
useEffectOnce(() => {
|
|
216
|
-
initializeChat(options)
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export const dictionary = {
|
|
221
|
-
en: {
|
|
222
|
-
errorLoadScript: 'An error occurred while loading the script:',
|
|
223
|
-
hide: 'Hide chat button',
|
|
224
|
-
hiddenTitle: 'The chat button is now hidden',
|
|
225
|
-
hiddenMessage: 'To see it again, please select the option "Chat with us" in the menu just under this message.',
|
|
226
|
-
},
|
|
227
|
-
pt: {
|
|
228
|
-
errorLoadScript: 'Ocorreu um erro ao carregar o script:',
|
|
229
|
-
hide: 'Esconder o botão de chat',
|
|
230
|
-
hiddenTitle: 'O botão de chat foi escondido',
|
|
231
|
-
hiddenMessage: 'Para vê-lo novamente, por favor escolha a opção "Converse conosco" no menu logo em baixo desta mensagem.',
|
|
232
|
-
},
|
|
233
|
-
} satisfies Dictionary
|
|
1
|
+
|
|
2
|
+
import { Dictionary, getLanguage, ptEn, translate } from '@stack-spot/portal-translate'
|
|
3
|
+
import { pull } from 'lodash'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { useEffectOnce } from './use-effect-once'
|
|
6
|
+
|
|
7
|
+
const CHAT_IFRAME_ID = 'sn_va_web_client'
|
|
8
|
+
export const CHAT_BUTTON_ID = 'service-now-button'
|
|
9
|
+
export const CHAT_BUTTON_WRAPPER_ID = 'service-now-content'
|
|
10
|
+
|
|
11
|
+
const closeButtonStyle = `
|
|
12
|
+
position: absolute;
|
|
13
|
+
bottom: -64px;
|
|
14
|
+
right: 60px;
|
|
15
|
+
width: 20px;
|
|
16
|
+
height: 20px;
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
background-color: #BCBCCF;
|
|
21
|
+
border: none;
|
|
22
|
+
outline: none;
|
|
23
|
+
border-radius: 50%;
|
|
24
|
+
font: 400 20px Roboto, sans-serif;
|
|
25
|
+
color: #0B0B0E;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
transform: scale(0);
|
|
28
|
+
transition: transform 0.2s;
|
|
29
|
+
z-index: 999999;
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
const iframeStyle = `
|
|
33
|
+
position: fixed;
|
|
34
|
+
display: none;
|
|
35
|
+
right: 15px;
|
|
36
|
+
bottom: 88px;
|
|
37
|
+
max-height: calc(100vh - 152px);
|
|
38
|
+
border-radius: 12px;
|
|
39
|
+
width: 380px;
|
|
40
|
+
height: 600px;
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
type ChatVisibilityListener = (visible: boolean) => void
|
|
44
|
+
|
|
45
|
+
interface Options {
|
|
46
|
+
/**
|
|
47
|
+
* Function to run when the chat button is hidden, i.e. when the user clicks the close button that appears when the chat button is
|
|
48
|
+
* hovered.
|
|
49
|
+
*/
|
|
50
|
+
onClose?: () => void,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const HOVER_AREA_PX = 100
|
|
54
|
+
const LOCAL_STORAGE_SERVICE_NOW_VALUE = 'serviceNowChatButtonVisible'
|
|
55
|
+
const chatVisibilityListeners: ChatVisibilityListener[] = []
|
|
56
|
+
|
|
57
|
+
const serviceNowReference = 'https://zup.service-now.com/sn_va_web_client_app_embed.do?sysparm_branding_key=stackspotvirtualagent&sysparm_topic=f2b636c11bb50e50fe670dcbe54bcb1d'
|
|
58
|
+
|
|
59
|
+
function loadScript(src: string): Promise<void> {
|
|
60
|
+
return new Promise<void>((resolve, reject) => {
|
|
61
|
+
const iframe = document.createElement('iframe')
|
|
62
|
+
const serviceNowContent = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
|
|
63
|
+
iframe.setAttribute('id', CHAT_IFRAME_ID)
|
|
64
|
+
iframe.setAttribute('src', src)
|
|
65
|
+
iframe.style.cssText = iframeStyle
|
|
66
|
+
|
|
67
|
+
iframe.onload = () => resolve()
|
|
68
|
+
iframe.onerror = (error) => reject(error)
|
|
69
|
+
|
|
70
|
+
serviceNowContent && serviceNowContent.appendChild(iframe)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @returns the iframe loaded by service now, i.e. the chat window; or null if the chat window doesn't exist.
|
|
76
|
+
*/
|
|
77
|
+
export function getServiceNowLauncher() {
|
|
78
|
+
return document.getElementById(CHAT_IFRAME_ID)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Opens the chat window and makes the chat button visible.
|
|
83
|
+
*/
|
|
84
|
+
export function openServiceNowChat() {
|
|
85
|
+
hideOrShowServiceNowContent('visible')
|
|
86
|
+
showChatWindow()
|
|
87
|
+
showServiceNowChatButton()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getServiceNowButton() {
|
|
91
|
+
return document.getElementById(CHAT_BUTTON_ID)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hideOrShowServiceNowContent(action: 'visible' | 'hidden') {
|
|
95
|
+
const content = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
|
|
96
|
+
if (content) {
|
|
97
|
+
content.style.visibility = action
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function toggleChatWindow(show: boolean) {
|
|
102
|
+
const launcher = getServiceNowLauncher()
|
|
103
|
+
const content = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
|
|
104
|
+
if (!launcher || !content) return
|
|
105
|
+
const [action, display] = show ? ['add', 'block'] as const : ['remove', 'none'] as const
|
|
106
|
+
launcher.classList[action]('show')
|
|
107
|
+
content.classList[action]('opened')
|
|
108
|
+
launcher.style.display = display
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Shows the chat window, i.e. shows the service now iframe and switch the icon in the chat button to "close".
|
|
113
|
+
*/
|
|
114
|
+
export function showChatWindow() {
|
|
115
|
+
toggleChatWindow(true)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Hides the chat window, i.e. hides the service now iframe and switch the icon in the chat button to "chat".
|
|
120
|
+
*/
|
|
121
|
+
export function hideChatWindow() {
|
|
122
|
+
toggleChatWindow(false)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Adds a listener that is called whenever the visibility of the chat button changes.
|
|
127
|
+
*
|
|
128
|
+
* The listener is called with a single parameter which indicates if the button is visible or not.
|
|
129
|
+
* @param listener the listener to register.
|
|
130
|
+
* @returns a function that, when called, removes the listener.
|
|
131
|
+
*/
|
|
132
|
+
function addChatVisibilityListener(listener: ChatVisibilityListener) {
|
|
133
|
+
chatVisibilityListeners.push(listener)
|
|
134
|
+
return () => pull(chatVisibilityListeners, listener)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderCloseButton(onClose?: () => void) {
|
|
138
|
+
const serviceNowButton = getServiceNowButton()
|
|
139
|
+
const anchor = serviceNowButton?.parentElement
|
|
140
|
+
if (!anchor) return
|
|
141
|
+
const close = document.createElement('button')
|
|
142
|
+
close.setAttribute('id', 'close-service-now')
|
|
143
|
+
close.style.cssText = closeButtonStyle
|
|
144
|
+
close.textContent = '-'
|
|
145
|
+
|
|
146
|
+
close.addEventListener('click', () => hideServiceNowChatButton(onClose))
|
|
147
|
+
anchor.appendChild(close)
|
|
148
|
+
|
|
149
|
+
serviceNowButton.addEventListener('mouseenter', () => {
|
|
150
|
+
const t = translate(dictionary, getLanguage(ptEn))
|
|
151
|
+
close.setAttribute('title', t.hide)
|
|
152
|
+
close.style.transform = 'scale(1)'
|
|
153
|
+
|
|
154
|
+
const onMouseMove = (ev: MouseEvent) => {
|
|
155
|
+
if (ev.clientX < window.innerWidth - HOVER_AREA_PX || ev.clientY < window.innerHeight - HOVER_AREA_PX) {
|
|
156
|
+
close.style.transform = 'scale(0)'
|
|
157
|
+
window.removeEventListener('mousemove', onMouseMove)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
window.addEventListener('mousemove', onMouseMove)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function showServiceNowChatButton() {
|
|
165
|
+
getServiceNowButton()?.classList?.add('show')
|
|
166
|
+
localStorage.setItem(LOCAL_STORAGE_SERVICE_NOW_VALUE, 'true')
|
|
167
|
+
chatVisibilityListeners.forEach(l => l(true))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hideServiceNowChatButton(onClose?: () => void) {
|
|
171
|
+
hideOrShowServiceNowContent('hidden')
|
|
172
|
+
hideChatWindow()
|
|
173
|
+
getServiceNowButton()?.classList?.remove('show')
|
|
174
|
+
localStorage.setItem(LOCAL_STORAGE_SERVICE_NOW_VALUE, 'false')
|
|
175
|
+
onClose?.()
|
|
176
|
+
chatVisibilityListeners.forEach(l => l(false))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isServiceNowChatButtonVisible() {
|
|
180
|
+
return localStorage.getItem(LOCAL_STORAGE_SERVICE_NOW_VALUE) !== 'false'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function initializeChat({ onClose }: Options = {}) {
|
|
184
|
+
try {
|
|
185
|
+
await loadScript(serviceNowReference)
|
|
186
|
+
if (isServiceNowChatButtonVisible()) showServiceNowChatButton()
|
|
187
|
+
else hideServiceNowChatButton()
|
|
188
|
+
renderCloseButton(onClose)
|
|
189
|
+
} catch {
|
|
190
|
+
const t = translate(dictionary, getLanguage(ptEn))
|
|
191
|
+
// eslint-disable-next-line no-console
|
|
192
|
+
console.warn(t.errorLoadScript)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A react hook that indicates whether the chat button is visible or not. This is updated whenever the value changes.
|
|
198
|
+
* @returns true if the chat button is visible and false otherwise.
|
|
199
|
+
*/
|
|
200
|
+
export function useServiceNowChatButtonVisibility() {
|
|
201
|
+
const [visible, setVisible] = useState(isServiceNowChatButtonVisible())
|
|
202
|
+
useEffectOnce(() => addChatVisibilityListener(setVisible))
|
|
203
|
+
return visible
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Starts Service Now:
|
|
208
|
+
*
|
|
209
|
+
* - Creates the chat window feature by loading the required script.
|
|
210
|
+
* - Adds a "minimize" button on top of the chat button (left bottom corner). This button appears whenever the user hovers the chat button.
|
|
211
|
+
* If clicked, the chat button is hidden.
|
|
212
|
+
* @param options
|
|
213
|
+
*/
|
|
214
|
+
export function useServiceNowEffect(options?: Options) {
|
|
215
|
+
useEffectOnce(() => {
|
|
216
|
+
initializeChat(options)
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const dictionary = {
|
|
221
|
+
en: {
|
|
222
|
+
errorLoadScript: 'An error occurred while loading the script:',
|
|
223
|
+
hide: 'Hide chat button',
|
|
224
|
+
hiddenTitle: 'The chat button is now hidden',
|
|
225
|
+
hiddenMessage: 'To see it again, please select the option "Chat with us" in the menu just under this message.',
|
|
226
|
+
},
|
|
227
|
+
pt: {
|
|
228
|
+
errorLoadScript: 'Ocorreu um erro ao carregar o script:',
|
|
229
|
+
hide: 'Esconder o botão de chat',
|
|
230
|
+
hiddenTitle: 'O botão de chat foi escondido',
|
|
231
|
+
hiddenMessage: 'Para vê-lo novamente, por favor escolha a opção "Converse conosco" no menu logo em baixo desta mensagem.',
|
|
232
|
+
},
|
|
233
|
+
} satisfies Dictionary
|
package/src/hooks/text.tsx
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Checks if the text fits completely in the space available.
|
|
5
|
-
* @returns true if it fits, false otherwise.
|
|
6
|
-
*/
|
|
7
|
-
export function useCheckTextOverflow() {
|
|
8
|
-
const [overflow, setOverflow] = useState<boolean>(false)
|
|
9
|
-
const ref = useRef<HTMLParagraphElement>(null)
|
|
10
|
-
|
|
11
|
-
const checkOverflow = () => {
|
|
12
|
-
if (!ref.current) {
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const hasOverflow = ref.current.offsetWidth < ref.current.scrollWidth
|
|
17
|
-
|
|
18
|
-
if (hasOverflow === overflow) {
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
setOverflow(hasOverflow)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
checkOverflow()
|
|
27
|
-
}, [ref.current])
|
|
28
|
-
|
|
29
|
-
return { overflow, ref }
|
|
30
|
-
}
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if the text fits completely in the space available.
|
|
5
|
+
* @returns true if it fits, false otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export function useCheckTextOverflow() {
|
|
8
|
+
const [overflow, setOverflow] = useState<boolean>(false)
|
|
9
|
+
const ref = useRef<HTMLParagraphElement>(null)
|
|
10
|
+
|
|
11
|
+
const checkOverflow = () => {
|
|
12
|
+
if (!ref.current) {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const hasOverflow = ref.current.offsetWidth < ref.current.scrollWidth
|
|
17
|
+
|
|
18
|
+
if (hasOverflow === overflow) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setOverflow(hasOverflow)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
checkOverflow()
|
|
27
|
+
}, [ref.current])
|
|
28
|
+
|
|
29
|
+
return { overflow, ref }
|
|
30
|
+
}
|
package/src/hooks/title.tsx
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
let originalTitle: string | undefined
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A React effect for prefixing the page title with the string passed as parameter followed by " - ".
|
|
7
|
-
*
|
|
8
|
-
* This returns a dispose function that changes the title back to its original value.
|
|
9
|
-
* @param title the prefix to add to the page title.
|
|
10
|
-
* @returns a dispose function.
|
|
11
|
-
*/
|
|
12
|
-
export function titleEffect(title: string | undefined | null) {
|
|
13
|
-
originalTitle ??= document.title
|
|
14
|
-
document.title = title ? `${title} - ${originalTitle}` : originalTitle
|
|
15
|
-
return () => {
|
|
16
|
-
document.title = originalTitle ?? ''
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Changes the page title to have the string passed as parameter as a prefix followed by " - " and the original title.
|
|
22
|
-
*
|
|
23
|
-
* Once disposed, this hook changes the title back to its original value.
|
|
24
|
-
* @param title the prefix to add to the page title.
|
|
25
|
-
*/
|
|
26
|
-
export function useTitleEffect(title: string | undefined | null) {
|
|
27
|
-
useEffect(() => titleEffect(title), [title])
|
|
28
|
-
}
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
let originalTitle: string | undefined
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A React effect for prefixing the page title with the string passed as parameter followed by " - ".
|
|
7
|
+
*
|
|
8
|
+
* This returns a dispose function that changes the title back to its original value.
|
|
9
|
+
* @param title the prefix to add to the page title.
|
|
10
|
+
* @returns a dispose function.
|
|
11
|
+
*/
|
|
12
|
+
export function titleEffect(title: string | undefined | null) {
|
|
13
|
+
originalTitle ??= document.title
|
|
14
|
+
document.title = title ? `${title} - ${originalTitle}` : originalTitle
|
|
15
|
+
return () => {
|
|
16
|
+
document.title = originalTitle ?? ''
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Changes the page title to have the string passed as parameter as a prefix followed by " - " and the original title.
|
|
22
|
+
*
|
|
23
|
+
* Once disposed, this hook changes the title back to its original value.
|
|
24
|
+
* @param title the prefix to add to the page title.
|
|
25
|
+
*/
|
|
26
|
+
export function useTitleEffect(title: string | undefined | null) {
|
|
27
|
+
useEffect(() => titleEffect(title), [title])
|
|
28
|
+
}
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Code taken from https://blog.ag-grid.com/avoiding-react-18-double-mount/
|
|
5
|
-
*
|
|
6
|
-
* Attention: don't use this hook unless you really have to!
|
|
7
|
-
*
|
|
8
|
-
* This hook fixes the React 18 behavior of calling useEffect hooks twice in strict/development mode, which ruins some mounting/unmounting
|
|
9
|
-
* behaviors.
|
|
10
|
-
*
|
|
11
|
-
* @param effect refer to React's useEffect.
|
|
12
|
-
*/
|
|
13
|
-
export const useEffectOnce = (effect: () => void | (() => void)) => {
|
|
14
|
-
const effectFn = useRef<() => void | (() => void)>(effect)
|
|
15
|
-
const destroyFn = useRef<void | (() => void)>()
|
|
16
|
-
const effectCalled = useRef(false)
|
|
17
|
-
const rendered = useRef(false)
|
|
18
|
-
const [, setVal] = useState<number>(0)
|
|
19
|
-
|
|
20
|
-
if (effectCalled.current) {
|
|
21
|
-
rendered.current = true
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
// only execute the effect first time around
|
|
26
|
-
if (!effectCalled.current) {
|
|
27
|
-
destroyFn.current = effectFn.current()
|
|
28
|
-
effectCalled.current = true
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// this forces one render after the effect is run
|
|
32
|
-
setVal((val) => val + 1)
|
|
33
|
-
|
|
34
|
-
return () => {
|
|
35
|
-
// if the comp didn't render since the useEffect was called,
|
|
36
|
-
// we know it's the dummy React cycle
|
|
37
|
-
if (!rendered.current) return
|
|
38
|
-
|
|
39
|
-
// otherwise this is not a dummy destroy, so call the destroy func
|
|
40
|
-
if (destroyFn.current) destroyFn.current()
|
|
41
|
-
}
|
|
42
|
-
}, [])
|
|
43
|
-
}
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Code taken from https://blog.ag-grid.com/avoiding-react-18-double-mount/
|
|
5
|
+
*
|
|
6
|
+
* Attention: don't use this hook unless you really have to!
|
|
7
|
+
*
|
|
8
|
+
* This hook fixes the React 18 behavior of calling useEffect hooks twice in strict/development mode, which ruins some mounting/unmounting
|
|
9
|
+
* behaviors.
|
|
10
|
+
*
|
|
11
|
+
* @param effect refer to React's useEffect.
|
|
12
|
+
*/
|
|
13
|
+
export const useEffectOnce = (effect: () => void | (() => void)) => {
|
|
14
|
+
const effectFn = useRef<() => void | (() => void)>(effect)
|
|
15
|
+
const destroyFn = useRef<void | (() => void)>()
|
|
16
|
+
const effectCalled = useRef(false)
|
|
17
|
+
const rendered = useRef(false)
|
|
18
|
+
const [, setVal] = useState<number>(0)
|
|
19
|
+
|
|
20
|
+
if (effectCalled.current) {
|
|
21
|
+
rendered.current = true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
// only execute the effect first time around
|
|
26
|
+
if (!effectCalled.current) {
|
|
27
|
+
destroyFn.current = effectFn.current()
|
|
28
|
+
effectCalled.current = true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// this forces one render after the effect is run
|
|
32
|
+
setVal((val) => val + 1)
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
// if the comp didn't render since the useEffect was called,
|
|
36
|
+
// we know it's the dummy React cycle
|
|
37
|
+
if (!rendered.current) return
|
|
38
|
+
|
|
39
|
+
// otherwise this is not a dummy destroy, so call the destroy func
|
|
40
|
+
if (destroyFn.current) destroyFn.current()
|
|
41
|
+
}
|
|
42
|
+
}, [])
|
|
43
|
+
}
|