@stack-spot/portal-layout 0.0.48 → 0.0.50
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/dist/Layout.d.ts +2 -2
- package/dist/Layout.js +1 -1
- package/dist/LayoutOverlayManager.js +6 -6
- package/dist/LayoutOverlayManager.js.map +1 -1
- package/dist/components/Dialog.d.ts +1 -1
- package/dist/components/Dialog.js +1 -1
- package/dist/components/Header.d.ts +1 -1
- package/dist/components/Header.js +1 -1
- package/dist/components/OverlayContent.d.ts +1 -1
- package/dist/components/OverlayContent.js +20 -20
- package/dist/components/PortalSwitcher.d.ts +1 -1
- package/dist/components/PortalSwitcher.js +54 -54
- package/dist/components/SelectionList.d.ts +1 -1
- package/dist/components/SelectionList.js +54 -54
- package/dist/components/SelectionList.js.map +1 -1
- package/dist/components/Toaster.d.ts +1 -1
- package/dist/components/Toaster.js +1 -1
- package/dist/components/UserMenu.d.ts +1 -1
- package/dist/components/UserMenu.js +42 -42
- package/dist/components/UserMenu.js.map +1 -1
- package/dist/components/error/ErrorBoundary.d.ts +1 -1
- package/dist/components/error/ErrorBoundary.js +1 -1
- package/dist/components/error/ErrorFeedback.d.ts +1 -1
- package/dist/components/error/ErrorFeedback.js +1 -1
- package/dist/components/error/SilentErrorBoundary.d.ts +1 -1
- package/dist/components/error/SilentErrorBoundary.js +1 -1
- package/dist/components/menu/MenuContent.d.ts +12 -10
- package/dist/components/menu/MenuContent.d.ts.map +1 -1
- package/dist/components/menu/MenuContent.js +147 -147
- package/dist/components/menu/MenuContent.js.map +1 -1
- package/dist/components/menu/MenuSections.d.ts +1 -1
- package/dist/components/menu/MenuSections.js +1 -1
- package/dist/components/menu/MenuSections.js.map +1 -1
- package/dist/components/menu/PageSelector.d.ts +1 -1
- package/dist/components/menu/PageSelector.js +65 -65
- package/dist/components/menu/PageSelector.js.map +1 -1
- package/dist/components/menu/use-check-text-overflow.js.map +1 -1
- package/dist/layout.css +465 -465
- package/dist/svg/AI.d.ts +1 -1
- package/dist/svg/AI.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/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/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/dist/toaster.js +2 -2
- package/dist/toaster.js.map +1 -1
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/Layout.tsx +103 -103
- package/src/LayoutOverlayManager.tsx +273 -273
- package/src/components/Dialog.tsx +93 -93
- package/src/components/Header.tsx +29 -29
- package/src/components/OverlayContent.tsx +58 -58
- package/src/components/PortalSwitcher.tsx +147 -147
- package/src/components/SelectionList.tsx +268 -268
- package/src/components/Toaster.tsx +16 -16
- package/src/components/UserMenu.tsx +111 -111
- package/src/components/error/ErrorBoundary.tsx +38 -38
- package/src/components/error/ErrorFeedback.tsx +114 -114
- package/src/components/error/ErrorManager.ts +31 -31
- package/src/components/error/SilentErrorBoundary.tsx +54 -54
- package/src/components/menu/MenuContent.tsx +293 -293
- package/src/components/menu/MenuSections.tsx +268 -268
- package/src/components/menu/PageSelector.tsx +152 -152
- package/src/components/menu/constants.ts +2 -2
- package/src/components/menu/types.ts +112 -112
- package/src/components/menu/use-check-text-overflow.tsx +26 -26
- package/src/components/menu/use-keyboard-controls.tsx +70 -70
- package/src/components/types.ts +15 -15
- package/src/dictionary.ts +25 -25
- package/src/elements.ts +24 -24
- package/src/errors.ts +11 -11
- package/src/index.ts +17 -17
- package/src/layout.css +465 -465
- package/src/svg/AI.tsx +37 -37
- package/src/svg/EDP.tsx +35 -35
- package/src/svg/Forbidden.tsx +22 -22
- package/src/svg/HUB.tsx +35 -35
- package/src/svg/Logo.tsx +35 -35
- package/src/svg/NotFound.tsx +16 -16
- package/src/svg/ServerError.tsx +33 -33
- package/src/svg/Unauthenticated.tsx +16 -16
- package/src/toaster.tsx +76 -76
- package/src/utils.ts +114 -114
- package/tsconfig.json +8 -8
|
@@ -1,273 +1,273 @@
|
|
|
1
|
-
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
-
|
|
3
|
-
import { Button } from '@citric/core'
|
|
4
|
-
import { ReactElement, useLayoutEffect, useState } from 'react'
|
|
5
|
-
import { Dialog, DialogOptions } from './components/Dialog'
|
|
6
|
-
import { CLOSE_OVERLAY_ID, OverlayContent, OverlayContentProps } from './components/OverlayContent'
|
|
7
|
-
import { getDictionary } from './dictionary'
|
|
8
|
-
import { LayoutElements, elementIds, getLayoutElements } from './elements'
|
|
9
|
-
import { ElementNotFound, LayoutError } from './errors'
|
|
10
|
-
import { showToaster as showReactToaster } from './toaster'
|
|
11
|
-
import { focusFirstChild, valueOfLayoutVar } from './utils'
|
|
12
|
-
|
|
13
|
-
interface AlertOptions extends Omit<DialogOptions, 'cancel'> {
|
|
14
|
-
showButton?: boolean,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type BottomDialogOptions = Omit<DialogOptions, 'title'>
|
|
18
|
-
type OverlaySize = 'small' | 'medium' | 'large'
|
|
19
|
-
type ModalSize = 'fit-content' | OverlaySize
|
|
20
|
-
type SetContentFn = ((content: ReactElement | undefined) => void) | undefined
|
|
21
|
-
|
|
22
|
-
interface OverlayContentSetter {
|
|
23
|
-
modal?: SetContentFn,
|
|
24
|
-
rightPanel?: SetContentFn,
|
|
25
|
-
bottomDialog?: SetContentFn,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface CustomModalOptions {
|
|
29
|
-
size?: ModalSize,
|
|
30
|
-
onClose?: () => void,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface CustomRightPanelOptions {
|
|
34
|
-
size?: OverlaySize,
|
|
35
|
-
onClose?: () => void,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function multipleCallsWarning(type: 'modal' | 'rightPanel', timeMS: number) {
|
|
39
|
-
return `
|
|
40
|
-
Attempted to show a modal or rightPanel while a ${type} was still being closed. Closing a ${type} takes only ${timeMS}ms, so this action
|
|
41
|
-
is unlikely to have been triggered by the user and may point to errors in your code. Please check.\nTip: showModal and showRightPanel
|
|
42
|
-
are sideEffects and should never be called during the render of a component. Try to use "useEffect" or link it to an event, like
|
|
43
|
-
"onClick".
|
|
44
|
-
`.replace(/\s*\n\s+/g, ' ')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
class LayoutOverlayManager {
|
|
48
|
-
static readonly instance?: LayoutOverlayManager
|
|
49
|
-
private setContent: OverlayContentSetter = {}
|
|
50
|
-
private elements?: LayoutElements
|
|
51
|
-
private onModalClose?: () => void
|
|
52
|
-
|
|
53
|
-
private setupElements() {
|
|
54
|
-
this.elements = getLayoutElements()
|
|
55
|
-
this.elements.backdrop?.addEventListener('mousedown', (event) => {
|
|
56
|
-
if (this.isModalOpen()) !this.elements?.modal?.contains?.(event.target as Node) && this.closeModal()
|
|
57
|
-
else if (this.isRightPanelOpen()) !this.elements?.rightPanel?.contains?.(event.target as Node) && this.closeRightPanel()
|
|
58
|
-
else this.setMainContentInteractivity(true)
|
|
59
|
-
})
|
|
60
|
-
this.elements.backdrop?.addEventListener('keydown', (event) => {
|
|
61
|
-
if (event.key !== 'Escape') return
|
|
62
|
-
if (this.isModalOpen()) this.closeModal()
|
|
63
|
-
if (this.isRightPanelOpen()) this.closeRightPanel()
|
|
64
|
-
else this.setMainContentInteractivity(true)
|
|
65
|
-
event.preventDefault()
|
|
66
|
-
})
|
|
67
|
-
this.setInteractivity(this.elements?.modal, false)
|
|
68
|
-
this.setInteractivity(this.elements?.rightPanel, false)
|
|
69
|
-
this.setInteractivity(this.elements?.bottomDialog, false)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// this should actually be like Kotlin's "internal", i.e. private to any user of the lib, but public to the lib itself.
|
|
73
|
-
// @ts-ignore
|
|
74
|
-
private useOverlays() {
|
|
75
|
-
useLayoutEffect(() => {
|
|
76
|
-
if (!this.elements) this.setupElements()
|
|
77
|
-
}, [])
|
|
78
|
-
const [modal, setModal] = useState<ReactElement | undefined>()
|
|
79
|
-
const [rightPanel, setRightPanel] = useState<ReactElement | undefined>()
|
|
80
|
-
const [bottomDialog, setBottomDialog] = useState<ReactElement | undefined>()
|
|
81
|
-
this.setContent.modal = setModal
|
|
82
|
-
this.setContent.rightPanel = setRightPanel
|
|
83
|
-
this.setContent.bottomDialog = setBottomDialog
|
|
84
|
-
return { modal, rightPanel, bottomDialog }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private setInteractivity(element: HTMLElement | null | undefined, interactive: boolean) {
|
|
88
|
-
if (interactive) {
|
|
89
|
-
element?.removeAttribute('inert')
|
|
90
|
-
element?.removeAttribute('aria-hidden')
|
|
91
|
-
} else {
|
|
92
|
-
element?.setAttribute('aria-hidden', '')
|
|
93
|
-
element?.setAttribute('inert', '')
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private setMainContentInteractivity(interactive: boolean) {
|
|
98
|
-
this.setInteractivity(this.elements?.page, interactive)
|
|
99
|
-
this.setInteractivity(this.elements?.header, interactive)
|
|
100
|
-
this.setInteractivity(this.elements?.menu, interactive)
|
|
101
|
-
this.elements?.backdrop?.setAttribute('class', interactive ? '' : 'visible')
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private showOverlay(element: HTMLElement | null | undefined, extraClasses: string[] = [], blockMainContent = true) {
|
|
105
|
-
element?.classList.add('visible', ...extraClasses)
|
|
106
|
-
this.setInteractivity(element, true)
|
|
107
|
-
if (blockMainContent) this.setMainContentInteractivity(false)
|
|
108
|
-
setTimeout(() => focusFirstChild(
|
|
109
|
-
element,
|
|
110
|
-
{ priority: [['input', 'textarea', 'select', 'other', 'button']], ignore: `#${CLOSE_OVERLAY_ID}` },
|
|
111
|
-
), 50)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private hideOverlay(element: HTMLElement | null | undefined) {
|
|
115
|
-
element?.setAttribute('class', '')
|
|
116
|
-
this.setInteractivity(element, false)
|
|
117
|
-
this.setMainContentInteractivity(true)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
isModalOpen() {
|
|
121
|
-
return this.elements?.modal?.classList.contains('visible') ?? false
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
isRightPanelOpen() {
|
|
125
|
-
return this.elements?.rightPanel?.classList.contains('visible') ?? false
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
isBottomDialogOpen() {
|
|
129
|
-
return this.elements?.bottomDialog?.classList.contains('visible') ?? false
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
showCustomModal(content: React.ReactElement, { size = 'medium', onClose }: CustomModalOptions = {}) {
|
|
133
|
-
if (!this.elements?.modal) throw new ElementNotFound('modal', elementIds.modal)
|
|
134
|
-
if (!this.setContent.modal) throw new LayoutError('unable to show modal, because it has not been setup yet.')
|
|
135
|
-
this.onModalClose = onClose
|
|
136
|
-
this.setContent.modal(content)
|
|
137
|
-
this.showOverlay(this.elements.modal, [size])
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
showModal({ size, ...props }: OverlayContentProps & { size?: ModalSize }) {
|
|
141
|
-
this.showCustomModal(<OverlayContent {...props} onClose={() => this.closeModal()} type="modal" />, { size, onClose: props.onClose })
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private showDialog(options: DialogOptions): Promise<boolean> {
|
|
145
|
-
let dialogResult = false
|
|
146
|
-
return new Promise((resolve, reject) => {
|
|
147
|
-
try {
|
|
148
|
-
this.showCustomModal(
|
|
149
|
-
<Dialog
|
|
150
|
-
{...options}
|
|
151
|
-
onCancel={() => this.closeModal()}
|
|
152
|
-
onConfirm={() => {
|
|
153
|
-
dialogResult = true
|
|
154
|
-
this.closeModal()
|
|
155
|
-
}}
|
|
156
|
-
/>,
|
|
157
|
-
{ size: 'small', onClose: () => resolve(dialogResult) },
|
|
158
|
-
)
|
|
159
|
-
} catch (error) {
|
|
160
|
-
reject(error)
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
confirm({ confirm, cancel, ...options }: DialogOptions): Promise<boolean> {
|
|
166
|
-
const t = getDictionary()
|
|
167
|
-
return this.showDialog({ ...options, confirm: confirm || t.confirm, cancel: cancel || t.cancel })
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async alert({ confirm, showButton = true, ...options }: AlertOptions): Promise<void> {
|
|
171
|
-
const t = getDictionary()
|
|
172
|
-
await this.showDialog({ ...options, confirm: showButton ? (confirm || t.confirm) : undefined })
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
showBottomDialog({ message, cancel, confirm }: BottomDialogOptions): Promise<boolean> {
|
|
176
|
-
if (!this.elements?.bottomDialog) throw new ElementNotFound('bottom dialog', elementIds.bottomDialog)
|
|
177
|
-
if (!this.setContent.bottomDialog) throw new LayoutError('unable to show bottom dialog, because it has not been setup yet.')
|
|
178
|
-
return new Promise((resolve) => {
|
|
179
|
-
this.setContent.bottomDialog?.(
|
|
180
|
-
<>
|
|
181
|
-
{message}
|
|
182
|
-
<div className="btn-group">
|
|
183
|
-
{cancel && <Button onClick={() => resolve(false)} colorScheme="light" appearance="outlined">{cancel}</Button>}
|
|
184
|
-
{confirm && <Button onClick={() => resolve(true)} colorScheme="light">{confirm}</Button>}
|
|
185
|
-
</div>
|
|
186
|
-
</>,
|
|
187
|
-
)
|
|
188
|
-
this.showOverlay(this.elements?.bottomDialog, undefined, false)
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
showCustomRightPanel(content: ReactElement, { size = 'medium', onClose }: CustomRightPanelOptions = {}) {
|
|
193
|
-
if (!this.elements?.rightPanel) throw new ElementNotFound('right panel overlay', elementIds.rightPanel)
|
|
194
|
-
if (!this.setContent.rightPanel) throw new LayoutError('unable to show right panel overlay, because it has not been setup yet.')
|
|
195
|
-
this.onModalClose = onClose
|
|
196
|
-
this.setContent.rightPanel(content)
|
|
197
|
-
this.elements?.rightPanel.classList.add(size)
|
|
198
|
-
setTimeout(() => {
|
|
199
|
-
this.showOverlay(this.elements?.rightPanel)
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
showRightPanel({ size, ...props }: OverlayContentProps & { size?: OverlaySize }) {
|
|
204
|
-
this.showCustomRightPanel(
|
|
205
|
-
<OverlayContent {...props} onClose={() => this.closeRightPanel()} type="panel" />,
|
|
206
|
-
{ size, onClose: props.onClose },
|
|
207
|
-
)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
closeModal(runCloseListener = true) {
|
|
211
|
-
this.elements?.modal?.classList.remove('visible')
|
|
212
|
-
this.elements?.backdrop?.setAttribute('class', '')
|
|
213
|
-
if (runCloseListener && this.onModalClose) {
|
|
214
|
-
const onClose = this.onModalClose
|
|
215
|
-
// setting it to undefined before running it prevents nested calls to closeModal from generating infinite loops.
|
|
216
|
-
this.onModalClose = undefined
|
|
217
|
-
onClose()
|
|
218
|
-
}
|
|
219
|
-
const animationMS = parseFloat(valueOfLayoutVar('--modal-animation-duration')) * 1000
|
|
220
|
-
setTimeout(
|
|
221
|
-
() => {
|
|
222
|
-
if (this.elements?.backdrop?.classList.contains('visible')) {
|
|
223
|
-
// eslint-disable-next-line no-console
|
|
224
|
-
console.warn(multipleCallsWarning('modal', animationMS))
|
|
225
|
-
this.elements?.modal?.classList.remove('visible')
|
|
226
|
-
}
|
|
227
|
-
if (this.setContent.modal) this.setContent.modal(undefined)
|
|
228
|
-
this.hideOverlay(this.elements?.modal)
|
|
229
|
-
},
|
|
230
|
-
animationMS,
|
|
231
|
-
)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
closeRightPanel(runCloseListener = true) {
|
|
235
|
-
this.elements?.rightPanel?.classList.remove('visible')
|
|
236
|
-
this.elements?.backdrop?.setAttribute('class', '')
|
|
237
|
-
if (runCloseListener && this.onModalClose) {
|
|
238
|
-
const onClose = this.onModalClose
|
|
239
|
-
// setting it to undefined before running it prevents nested calls to closeRightPanel from generating infinite loops.
|
|
240
|
-
this.onModalClose = undefined
|
|
241
|
-
onClose()
|
|
242
|
-
}
|
|
243
|
-
const animationMS = parseFloat(valueOfLayoutVar('--right-panel-animation-duration')) * 1000
|
|
244
|
-
setTimeout(
|
|
245
|
-
() => {
|
|
246
|
-
if (this.elements?.backdrop?.classList.contains('visible')) {
|
|
247
|
-
// eslint-disable-next-line no-console
|
|
248
|
-
console.warn(multipleCallsWarning('rightPanel', animationMS))
|
|
249
|
-
this.elements?.rightPanel?.classList.remove('visible')
|
|
250
|
-
}
|
|
251
|
-
if (this.setContent.rightPanel) this.setContent.rightPanel(undefined)
|
|
252
|
-
this.hideOverlay(this.elements?.rightPanel)
|
|
253
|
-
},
|
|
254
|
-
animationMS,
|
|
255
|
-
)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
closeBottomDialog() {
|
|
259
|
-
this.hideOverlay(this.elements?.bottomDialog)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
isInsideModal(element: HTMLElement) {
|
|
263
|
-
return !!this.elements?.modal?.contains(element)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
isInsideRightPanel(element: HTMLElement) {
|
|
267
|
-
return !!this.elements?.rightPanel?.contains(element)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
showToaster = showReactToaster
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export const overlay = new LayoutOverlayManager()
|
|
1
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
+
|
|
3
|
+
import { Button } from '@citric/core'
|
|
4
|
+
import { ReactElement, useLayoutEffect, useState } from 'react'
|
|
5
|
+
import { Dialog, DialogOptions } from './components/Dialog'
|
|
6
|
+
import { CLOSE_OVERLAY_ID, OverlayContent, OverlayContentProps } from './components/OverlayContent'
|
|
7
|
+
import { getDictionary } from './dictionary'
|
|
8
|
+
import { LayoutElements, elementIds, getLayoutElements } from './elements'
|
|
9
|
+
import { ElementNotFound, LayoutError } from './errors'
|
|
10
|
+
import { showToaster as showReactToaster } from './toaster'
|
|
11
|
+
import { focusFirstChild, valueOfLayoutVar } from './utils'
|
|
12
|
+
|
|
13
|
+
interface AlertOptions extends Omit<DialogOptions, 'cancel'> {
|
|
14
|
+
showButton?: boolean,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type BottomDialogOptions = Omit<DialogOptions, 'title'>
|
|
18
|
+
type OverlaySize = 'small' | 'medium' | 'large'
|
|
19
|
+
type ModalSize = 'fit-content' | OverlaySize
|
|
20
|
+
type SetContentFn = ((content: ReactElement | undefined) => void) | undefined
|
|
21
|
+
|
|
22
|
+
interface OverlayContentSetter {
|
|
23
|
+
modal?: SetContentFn,
|
|
24
|
+
rightPanel?: SetContentFn,
|
|
25
|
+
bottomDialog?: SetContentFn,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CustomModalOptions {
|
|
29
|
+
size?: ModalSize,
|
|
30
|
+
onClose?: () => void,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface CustomRightPanelOptions {
|
|
34
|
+
size?: OverlaySize,
|
|
35
|
+
onClose?: () => void,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function multipleCallsWarning(type: 'modal' | 'rightPanel', timeMS: number) {
|
|
39
|
+
return `
|
|
40
|
+
Attempted to show a modal or rightPanel while a ${type} was still being closed. Closing a ${type} takes only ${timeMS}ms, so this action
|
|
41
|
+
is unlikely to have been triggered by the user and may point to errors in your code. Please check.\nTip: showModal and showRightPanel
|
|
42
|
+
are sideEffects and should never be called during the render of a component. Try to use "useEffect" or link it to an event, like
|
|
43
|
+
"onClick".
|
|
44
|
+
`.replace(/\s*\n\s+/g, ' ')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class LayoutOverlayManager {
|
|
48
|
+
static readonly instance?: LayoutOverlayManager
|
|
49
|
+
private setContent: OverlayContentSetter = {}
|
|
50
|
+
private elements?: LayoutElements
|
|
51
|
+
private onModalClose?: () => void
|
|
52
|
+
|
|
53
|
+
private setupElements() {
|
|
54
|
+
this.elements = getLayoutElements()
|
|
55
|
+
this.elements.backdrop?.addEventListener('mousedown', (event) => {
|
|
56
|
+
if (this.isModalOpen()) !this.elements?.modal?.contains?.(event.target as Node) && this.closeModal()
|
|
57
|
+
else if (this.isRightPanelOpen()) !this.elements?.rightPanel?.contains?.(event.target as Node) && this.closeRightPanel()
|
|
58
|
+
else this.setMainContentInteractivity(true)
|
|
59
|
+
})
|
|
60
|
+
this.elements.backdrop?.addEventListener('keydown', (event) => {
|
|
61
|
+
if (event.key !== 'Escape') return
|
|
62
|
+
if (this.isModalOpen()) this.closeModal()
|
|
63
|
+
if (this.isRightPanelOpen()) this.closeRightPanel()
|
|
64
|
+
else this.setMainContentInteractivity(true)
|
|
65
|
+
event.preventDefault()
|
|
66
|
+
})
|
|
67
|
+
this.setInteractivity(this.elements?.modal, false)
|
|
68
|
+
this.setInteractivity(this.elements?.rightPanel, false)
|
|
69
|
+
this.setInteractivity(this.elements?.bottomDialog, false)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// this should actually be like Kotlin's "internal", i.e. private to any user of the lib, but public to the lib itself.
|
|
73
|
+
// @ts-ignore
|
|
74
|
+
private useOverlays() {
|
|
75
|
+
useLayoutEffect(() => {
|
|
76
|
+
if (!this.elements) this.setupElements()
|
|
77
|
+
}, [])
|
|
78
|
+
const [modal, setModal] = useState<ReactElement | undefined>()
|
|
79
|
+
const [rightPanel, setRightPanel] = useState<ReactElement | undefined>()
|
|
80
|
+
const [bottomDialog, setBottomDialog] = useState<ReactElement | undefined>()
|
|
81
|
+
this.setContent.modal = setModal
|
|
82
|
+
this.setContent.rightPanel = setRightPanel
|
|
83
|
+
this.setContent.bottomDialog = setBottomDialog
|
|
84
|
+
return { modal, rightPanel, bottomDialog }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private setInteractivity(element: HTMLElement | null | undefined, interactive: boolean) {
|
|
88
|
+
if (interactive) {
|
|
89
|
+
element?.removeAttribute('inert')
|
|
90
|
+
element?.removeAttribute('aria-hidden')
|
|
91
|
+
} else {
|
|
92
|
+
element?.setAttribute('aria-hidden', '')
|
|
93
|
+
element?.setAttribute('inert', '')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private setMainContentInteractivity(interactive: boolean) {
|
|
98
|
+
this.setInteractivity(this.elements?.page, interactive)
|
|
99
|
+
this.setInteractivity(this.elements?.header, interactive)
|
|
100
|
+
this.setInteractivity(this.elements?.menu, interactive)
|
|
101
|
+
this.elements?.backdrop?.setAttribute('class', interactive ? '' : 'visible')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private showOverlay(element: HTMLElement | null | undefined, extraClasses: string[] = [], blockMainContent = true) {
|
|
105
|
+
element?.classList.add('visible', ...extraClasses)
|
|
106
|
+
this.setInteractivity(element, true)
|
|
107
|
+
if (blockMainContent) this.setMainContentInteractivity(false)
|
|
108
|
+
setTimeout(() => focusFirstChild(
|
|
109
|
+
element,
|
|
110
|
+
{ priority: [['input', 'textarea', 'select', 'other', 'button']], ignore: `#${CLOSE_OVERLAY_ID}` },
|
|
111
|
+
), 50)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private hideOverlay(element: HTMLElement | null | undefined) {
|
|
115
|
+
element?.setAttribute('class', '')
|
|
116
|
+
this.setInteractivity(element, false)
|
|
117
|
+
this.setMainContentInteractivity(true)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
isModalOpen() {
|
|
121
|
+
return this.elements?.modal?.classList.contains('visible') ?? false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
isRightPanelOpen() {
|
|
125
|
+
return this.elements?.rightPanel?.classList.contains('visible') ?? false
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
isBottomDialogOpen() {
|
|
129
|
+
return this.elements?.bottomDialog?.classList.contains('visible') ?? false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
showCustomModal(content: React.ReactElement, { size = 'medium', onClose }: CustomModalOptions = {}) {
|
|
133
|
+
if (!this.elements?.modal) throw new ElementNotFound('modal', elementIds.modal)
|
|
134
|
+
if (!this.setContent.modal) throw new LayoutError('unable to show modal, because it has not been setup yet.')
|
|
135
|
+
this.onModalClose = onClose
|
|
136
|
+
this.setContent.modal(content)
|
|
137
|
+
this.showOverlay(this.elements.modal, [size])
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
showModal({ size, ...props }: OverlayContentProps & { size?: ModalSize }) {
|
|
141
|
+
this.showCustomModal(<OverlayContent {...props} onClose={() => this.closeModal()} type="modal" />, { size, onClose: props.onClose })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private showDialog(options: DialogOptions): Promise<boolean> {
|
|
145
|
+
let dialogResult = false
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
try {
|
|
148
|
+
this.showCustomModal(
|
|
149
|
+
<Dialog
|
|
150
|
+
{...options}
|
|
151
|
+
onCancel={() => this.closeModal()}
|
|
152
|
+
onConfirm={() => {
|
|
153
|
+
dialogResult = true
|
|
154
|
+
this.closeModal()
|
|
155
|
+
}}
|
|
156
|
+
/>,
|
|
157
|
+
{ size: 'small', onClose: () => resolve(dialogResult) },
|
|
158
|
+
)
|
|
159
|
+
} catch (error) {
|
|
160
|
+
reject(error)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
confirm({ confirm, cancel, ...options }: DialogOptions): Promise<boolean> {
|
|
166
|
+
const t = getDictionary()
|
|
167
|
+
return this.showDialog({ ...options, confirm: confirm || t.confirm, cancel: cancel || t.cancel })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async alert({ confirm, showButton = true, ...options }: AlertOptions): Promise<void> {
|
|
171
|
+
const t = getDictionary()
|
|
172
|
+
await this.showDialog({ ...options, confirm: showButton ? (confirm || t.confirm) : undefined })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
showBottomDialog({ message, cancel, confirm }: BottomDialogOptions): Promise<boolean> {
|
|
176
|
+
if (!this.elements?.bottomDialog) throw new ElementNotFound('bottom dialog', elementIds.bottomDialog)
|
|
177
|
+
if (!this.setContent.bottomDialog) throw new LayoutError('unable to show bottom dialog, because it has not been setup yet.')
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
this.setContent.bottomDialog?.(
|
|
180
|
+
<>
|
|
181
|
+
{message}
|
|
182
|
+
<div className="btn-group">
|
|
183
|
+
{cancel && <Button onClick={() => resolve(false)} colorScheme="light" appearance="outlined">{cancel}</Button>}
|
|
184
|
+
{confirm && <Button onClick={() => resolve(true)} colorScheme="light">{confirm}</Button>}
|
|
185
|
+
</div>
|
|
186
|
+
</>,
|
|
187
|
+
)
|
|
188
|
+
this.showOverlay(this.elements?.bottomDialog, undefined, false)
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
showCustomRightPanel(content: ReactElement, { size = 'medium', onClose }: CustomRightPanelOptions = {}) {
|
|
193
|
+
if (!this.elements?.rightPanel) throw new ElementNotFound('right panel overlay', elementIds.rightPanel)
|
|
194
|
+
if (!this.setContent.rightPanel) throw new LayoutError('unable to show right panel overlay, because it has not been setup yet.')
|
|
195
|
+
this.onModalClose = onClose
|
|
196
|
+
this.setContent.rightPanel(content)
|
|
197
|
+
this.elements?.rightPanel.classList.add(size)
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
this.showOverlay(this.elements?.rightPanel)
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
showRightPanel({ size, ...props }: OverlayContentProps & { size?: OverlaySize }) {
|
|
204
|
+
this.showCustomRightPanel(
|
|
205
|
+
<OverlayContent {...props} onClose={() => this.closeRightPanel()} type="panel" />,
|
|
206
|
+
{ size, onClose: props.onClose },
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
closeModal(runCloseListener = true) {
|
|
211
|
+
this.elements?.modal?.classList.remove('visible')
|
|
212
|
+
this.elements?.backdrop?.setAttribute('class', '')
|
|
213
|
+
if (runCloseListener && this.onModalClose) {
|
|
214
|
+
const onClose = this.onModalClose
|
|
215
|
+
// setting it to undefined before running it prevents nested calls to closeModal from generating infinite loops.
|
|
216
|
+
this.onModalClose = undefined
|
|
217
|
+
onClose()
|
|
218
|
+
}
|
|
219
|
+
const animationMS = parseFloat(valueOfLayoutVar('--modal-animation-duration')) * 1000
|
|
220
|
+
setTimeout(
|
|
221
|
+
() => {
|
|
222
|
+
if (this.elements?.backdrop?.classList.contains('visible')) {
|
|
223
|
+
// eslint-disable-next-line no-console
|
|
224
|
+
console.warn(multipleCallsWarning('modal', animationMS))
|
|
225
|
+
this.elements?.modal?.classList.remove('visible')
|
|
226
|
+
}
|
|
227
|
+
if (this.setContent.modal) this.setContent.modal(undefined)
|
|
228
|
+
this.hideOverlay(this.elements?.modal)
|
|
229
|
+
},
|
|
230
|
+
animationMS,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
closeRightPanel(runCloseListener = true) {
|
|
235
|
+
this.elements?.rightPanel?.classList.remove('visible')
|
|
236
|
+
this.elements?.backdrop?.setAttribute('class', '')
|
|
237
|
+
if (runCloseListener && this.onModalClose) {
|
|
238
|
+
const onClose = this.onModalClose
|
|
239
|
+
// setting it to undefined before running it prevents nested calls to closeRightPanel from generating infinite loops.
|
|
240
|
+
this.onModalClose = undefined
|
|
241
|
+
onClose()
|
|
242
|
+
}
|
|
243
|
+
const animationMS = parseFloat(valueOfLayoutVar('--right-panel-animation-duration')) * 1000
|
|
244
|
+
setTimeout(
|
|
245
|
+
() => {
|
|
246
|
+
if (this.elements?.backdrop?.classList.contains('visible')) {
|
|
247
|
+
// eslint-disable-next-line no-console
|
|
248
|
+
console.warn(multipleCallsWarning('rightPanel', animationMS))
|
|
249
|
+
this.elements?.rightPanel?.classList.remove('visible')
|
|
250
|
+
}
|
|
251
|
+
if (this.setContent.rightPanel) this.setContent.rightPanel(undefined)
|
|
252
|
+
this.hideOverlay(this.elements?.rightPanel)
|
|
253
|
+
},
|
|
254
|
+
animationMS,
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
closeBottomDialog() {
|
|
259
|
+
this.hideOverlay(this.elements?.bottomDialog)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
isInsideModal(element: HTMLElement) {
|
|
263
|
+
return !!this.elements?.modal?.contains(element)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
isInsideRightPanel(element: HTMLElement) {
|
|
267
|
+
return !!this.elements?.rightPanel?.contains(element)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
showToaster = showReactToaster
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export const overlay = new LayoutOverlayManager()
|