@stack-spot/portal-layout 2.32.3 → 2.32.5-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.
- package/CHANGELOG.md +147 -0
- package/dist/Layout.d.ts +6 -7
- package/dist/Layout.d.ts.map +1 -1
- package/dist/Layout.js +30 -11
- package/dist/Layout.js.map +1 -1
- package/dist/LayoutOverlayManager.d.ts +77 -11
- package/dist/LayoutOverlayManager.d.ts.map +1 -1
- package/dist/LayoutOverlayManager.js +159 -46
- package/dist/LayoutOverlayManager.js.map +1 -1
- package/dist/components/Dialog.d.ts +1 -1
- package/dist/components/Dialog.d.ts.map +1 -1
- package/dist/components/NotificationCenter/NotificationsPanelFooter.js +1 -1
- package/dist/components/NotificationCenter/NotificationsPanelFooter.js.map +1 -1
- package/dist/components/NotificationCenter/dictionary.d.ts +1 -1
- package/dist/components/NotificationCenter/dictionary.d.ts.map +1 -1
- package/dist/components/NotificationCenter/dictionary.js +2 -2
- package/dist/components/NotificationCenter/dictionary.js.map +1 -1
- package/dist/components/OverlayContent.js +2 -2
- package/dist/components/error/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/error/SilentErrorBoundary.d.ts.map +1 -1
- package/dist/components/menu/MenuSections.d.ts.map +1 -1
- package/dist/components/menu/MenuSections.js +17 -7
- package/dist/components/menu/MenuSections.js.map +1 -1
- package/dist/components/menu/types.d.ts +7 -0
- package/dist/components/menu/types.d.ts.map +1 -1
- package/dist/components/tour/StepNavigation.d.ts.map +1 -1
- package/dist/components/tour/StepNavigation.js +2 -1
- package/dist/components/tour/StepNavigation.js.map +1 -1
- package/dist/dictionary.js +2 -2
- package/dist/dictionary.js.map +1 -1
- package/dist/elements.d.ts +1 -0
- package/dist/elements.d.ts.map +1 -1
- package/dist/elements.js +1 -0
- package/dist/elements.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/layout.css +33 -5
- package/package.json +6 -4
- package/readme.md +0 -1
- package/src/Layout.tsx +70 -22
- package/src/LayoutOverlayManager.tsx +206 -54
- package/src/components/Dialog.tsx +1 -1
- package/src/components/NotificationCenter/NotificationsPanelFooter.tsx +1 -1
- package/src/components/NotificationCenter/dictionary.ts +2 -2
- package/src/components/OverlayContent.tsx +2 -2
- package/src/components/menu/MenuSections.tsx +19 -8
- package/src/components/menu/types.ts +7 -0
- package/src/components/tour/StepNavigation.tsx +3 -1
- package/src/dictionary.ts +2 -2
- package/src/elements.ts +1 -0
- package/src/index.ts +1 -0
- package/src/layout.css +33 -5
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
2
|
|
|
3
3
|
import { Button } from '@citric/core'
|
|
4
|
+
import { ModalContent } from '@citric/ui'
|
|
4
5
|
import { focusAccessibleElement, focusFirstChild } from '@stack-spot/portal-components'
|
|
6
|
+
import { last } from 'lodash'
|
|
5
7
|
import { ReactElement, useLayoutEffect, useState } from 'react'
|
|
6
8
|
import { Dialog, DialogOptions } from './components/Dialog'
|
|
7
9
|
import { CLOSE_OVERLAY_ID, OverlayContent, OverlayContentProps } from './components/OverlayContent'
|
|
@@ -20,16 +22,28 @@ interface AlertOptions extends Omit<DialogOptions, 'cancel'> {
|
|
|
20
22
|
showButton?: boolean,
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
interface ModalContent {
|
|
26
|
+
id?: string,
|
|
27
|
+
element: React.ReactElement,
|
|
28
|
+
size: CustomModalSize | RightPanelSize,
|
|
29
|
+
onClose?: () => void,
|
|
30
|
+
stack: boolean,
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
type BottomDialogOptions = Omit<DialogOptions, 'title'>
|
|
24
|
-
type SetContentFn = (
|
|
34
|
+
type SetContentFn = (content: ModalContent[]) => void
|
|
25
35
|
|
|
26
36
|
interface OverlayContentSetter {
|
|
27
37
|
modal?: SetContentFn,
|
|
28
38
|
rightPanel?: SetContentFn,
|
|
29
|
-
bottomDialog?:
|
|
39
|
+
bottomDialog?: ((content: React.ReactElement | undefined) => void),
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
interface CustomModalOptions {
|
|
43
|
+
/**
|
|
44
|
+
* An optional, unique identifier for this modal.
|
|
45
|
+
*/
|
|
46
|
+
id?: string,
|
|
33
47
|
/**
|
|
34
48
|
* The size of the modal.
|
|
35
49
|
*/
|
|
@@ -43,9 +57,24 @@ interface CustomModalOptions {
|
|
|
43
57
|
* @default true
|
|
44
58
|
*/
|
|
45
59
|
ignoreFirstFocusOnCloseButton?: boolean,
|
|
60
|
+
/**
|
|
61
|
+
* If true, instead of replacing the previously opened modal (if any), it will open on top of it (stacked).
|
|
62
|
+
*
|
|
63
|
+
* When a modal is stacked on top of another:
|
|
64
|
+
* - Closing the modal, closes all opened modals.
|
|
65
|
+
* - Popping the modal, closes only the modal at the top of the stack.
|
|
66
|
+
* - Only the modal at the top of the stack can be interacted with.
|
|
67
|
+
*
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
stack?: boolean,
|
|
46
71
|
}
|
|
47
72
|
|
|
48
73
|
interface CustomRightPanelOptions {
|
|
74
|
+
/**
|
|
75
|
+
* An optional, unique identifier for this right panel.
|
|
76
|
+
*/
|
|
77
|
+
id?: string,
|
|
49
78
|
/**
|
|
50
79
|
* The size of the right panel.
|
|
51
80
|
*/
|
|
@@ -54,6 +83,22 @@ interface CustomRightPanelOptions {
|
|
|
54
83
|
* A function to call when the right panel closes.
|
|
55
84
|
*/
|
|
56
85
|
onClose?: () => void,
|
|
86
|
+
/**
|
|
87
|
+
* Property that defines whether the modal should ignore the initial focus on the close button.
|
|
88
|
+
* @default true
|
|
89
|
+
*/
|
|
90
|
+
ignoreFirstFocusOnCloseButton?: boolean,
|
|
91
|
+
/**
|
|
92
|
+
* If true, instead of replacing the previously opened right panel (if any), it will open on top of it (stacked).
|
|
93
|
+
*
|
|
94
|
+
* When a right panel is stacked on top of another:
|
|
95
|
+
* - Closing the panel, closes all opened panels.
|
|
96
|
+
* - Popping the panel, closes only the panel at the top of the stack.
|
|
97
|
+
* - Only the panel at the top of the stack can be interacted with.
|
|
98
|
+
*
|
|
99
|
+
* @default false
|
|
100
|
+
*/
|
|
101
|
+
stack?: boolean,
|
|
57
102
|
}
|
|
58
103
|
|
|
59
104
|
function multipleCallsWarning(type: 'modal' | 'rightPanel', timeMS: number) {
|
|
@@ -69,11 +114,12 @@ class LayoutOverlayManager {
|
|
|
69
114
|
static readonly instance?: LayoutOverlayManager
|
|
70
115
|
private setContent: OverlayContentSetter = {}
|
|
71
116
|
private elements?: LayoutElements
|
|
72
|
-
private onModalClose?: () => void
|
|
73
117
|
/**
|
|
74
118
|
* Last element with focus before an overlay is shown.
|
|
75
119
|
*/
|
|
76
120
|
private lastActiveElement: Element | null = null
|
|
121
|
+
private modals: ModalContent[] = []
|
|
122
|
+
private panels: ModalContent[] = []
|
|
77
123
|
|
|
78
124
|
private closeCustomBackdrops(elements: NodeListOf<Element>) {
|
|
79
125
|
// this is the easiest way to close each custom backdrop by calling their respective "onClose" callbacks. This is a hidden button
|
|
@@ -132,11 +178,17 @@ class LayoutOverlayManager {
|
|
|
132
178
|
useLayoutEffect(() => {
|
|
133
179
|
if (!this.elements) this.setupElements()
|
|
134
180
|
}, [])
|
|
135
|
-
const [modal, setModal] = useState<
|
|
136
|
-
const [rightPanel, setRightPanel] = useState<
|
|
181
|
+
const [modal, setModal] = useState<ModalContent[]>([])
|
|
182
|
+
const [rightPanel, setRightPanel] = useState<ModalContent[]>([])
|
|
137
183
|
const [bottomDialog, setBottomDialog] = useState<ReactElement | undefined>()
|
|
138
|
-
this.setContent.modal =
|
|
139
|
-
|
|
184
|
+
this.setContent.modal = (content) => {
|
|
185
|
+
this.modals = content
|
|
186
|
+
setModal(content)
|
|
187
|
+
}
|
|
188
|
+
this.setContent.rightPanel = (content) => {
|
|
189
|
+
this.panels = content
|
|
190
|
+
setRightPanel(content)
|
|
191
|
+
}
|
|
140
192
|
this.setContent.bottomDialog = setBottomDialog
|
|
141
193
|
return { modal, rightPanel, bottomDialog }
|
|
142
194
|
}
|
|
@@ -170,7 +222,6 @@ class LayoutOverlayManager {
|
|
|
170
222
|
ignoreFirstFocusOnCloseButton = true,
|
|
171
223
|
) {
|
|
172
224
|
this.lastActiveElement = document.activeElement
|
|
173
|
-
|
|
174
225
|
if (manageClasses) element?.classList.add('visible', ...extraClasses)
|
|
175
226
|
this.setInteractivity(element, true)
|
|
176
227
|
if (blockMainContent) this.setMainContentInteractivity(false)
|
|
@@ -220,6 +271,20 @@ class LayoutOverlayManager {
|
|
|
220
271
|
return this.elements?.modal?.classList.contains('visible') ?? false
|
|
221
272
|
}
|
|
222
273
|
|
|
274
|
+
/**
|
|
275
|
+
* @returns the number of modals currently tracked.
|
|
276
|
+
*/
|
|
277
|
+
getNumberOfOpenModals() {
|
|
278
|
+
return this.modals.length
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @returns the number of right panels currently tracked.
|
|
283
|
+
*/
|
|
284
|
+
getNumberOfOpenRightPanels() {
|
|
285
|
+
return this.panels.length
|
|
286
|
+
}
|
|
287
|
+
|
|
223
288
|
/**
|
|
224
289
|
* @returns true if the right panel is currently opened. False otherwise.
|
|
225
290
|
*/
|
|
@@ -244,11 +309,15 @@ class LayoutOverlayManager {
|
|
|
244
309
|
* @param options the modal options {@link CustomModalOptions}.
|
|
245
310
|
*/
|
|
246
311
|
showCustomModal(content: React.ReactElement,
|
|
247
|
-
{ size = 'medium', onClose, ignoreFirstFocusOnCloseButton = true }: CustomModalOptions = {}
|
|
312
|
+
{ size = 'medium', onClose, ignoreFirstFocusOnCloseButton = true, stack = false, id }: CustomModalOptions = {},
|
|
313
|
+
) {
|
|
248
314
|
if (!this.elements?.modal) throw new ElementNotFound('modal', elementIds.modal)
|
|
249
315
|
if (!this.setContent.modal) throw new LayoutError('unable to show modal, because it has not been setup yet.')
|
|
250
|
-
|
|
251
|
-
this.
|
|
316
|
+
const modal = { element: content, onClose, size, stack, id }
|
|
317
|
+
const currentModalSize = last(this.modals)?.size
|
|
318
|
+
this.setContent.modal(stack ? [...this.modals, modal] : [modal])
|
|
319
|
+
// we should remove the previous size, if any, before showing the modal
|
|
320
|
+
if (currentModalSize) this.elements.modal.classList.remove(currentModalSize)
|
|
252
321
|
this.showOverlay(this.elements.modal, [size], true, true, ignoreFirstFocusOnCloseButton)
|
|
253
322
|
}
|
|
254
323
|
|
|
@@ -261,11 +330,12 @@ class LayoutOverlayManager {
|
|
|
261
330
|
* @param options the modal options: {@link OverlayContentProps} & { size: {@link ModalSize} }.
|
|
262
331
|
*/
|
|
263
332
|
showModal({
|
|
264
|
-
size, ignoreFirstFocusOnCloseButton, ...props
|
|
265
|
-
}: OverlayContentProps & { size?: ModalSize, ignoreFirstFocusOnCloseButton
|
|
333
|
+
size, ignoreFirstFocusOnCloseButton, stack, onGoBack, id, ...props
|
|
334
|
+
}: OverlayContentProps & { size?: ModalSize } & Pick<CustomModalOptions, 'ignoreFirstFocusOnCloseButton' | 'stack' | 'id'>) {
|
|
335
|
+
const handleBack = onGoBack ?? ((stack && this.modals.length >= 1) ? () => this.popModal() : undefined)
|
|
266
336
|
this.showCustomModal(
|
|
267
|
-
<OverlayContent {...props} onClose={() => this.closeModal()} type="modal" />,
|
|
268
|
-
{ size, onClose: props.onClose, ignoreFirstFocusOnCloseButton },
|
|
337
|
+
<OverlayContent {...props} onGoBack={handleBack} onClose={() => this.closeModal()} type="modal" />,
|
|
338
|
+
{ size, onClose: props.onClose, ignoreFirstFocusOnCloseButton, stack, id },
|
|
269
339
|
)
|
|
270
340
|
}
|
|
271
341
|
|
|
@@ -274,17 +344,31 @@ class LayoutOverlayManager {
|
|
|
274
344
|
let dialogResult = false
|
|
275
345
|
return new Promise((resolve, reject) => {
|
|
276
346
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
347
|
+
if (options.type === 'panel') {
|
|
348
|
+
this.showCustomRightPanel(
|
|
349
|
+
<Dialog
|
|
350
|
+
{...options}
|
|
351
|
+
onCancel={() => this.closeModal()}
|
|
352
|
+
onConfirm={() => {
|
|
353
|
+
dialogResult = true
|
|
354
|
+
this.closeModal()
|
|
355
|
+
}}
|
|
356
|
+
/>,
|
|
357
|
+
{ size: size as RightPanelSize, onClose: () => resolve(dialogResult) },
|
|
358
|
+
)
|
|
359
|
+
} else {
|
|
360
|
+
this.showCustomModal(
|
|
361
|
+
<Dialog
|
|
362
|
+
{...options}
|
|
363
|
+
onCancel={() => this.closeModal()}
|
|
364
|
+
onConfirm={() => {
|
|
365
|
+
dialogResult = true
|
|
366
|
+
this.closeModal()
|
|
367
|
+
}}
|
|
368
|
+
/>,
|
|
369
|
+
{ size, onClose: () => resolve(dialogResult), ignoreFirstFocusOnCloseButton },
|
|
370
|
+
)
|
|
371
|
+
}
|
|
288
372
|
} catch (error) {
|
|
289
373
|
reject(error)
|
|
290
374
|
}
|
|
@@ -366,14 +450,17 @@ class LayoutOverlayManager {
|
|
|
366
450
|
* @param content a react element with the modal content.
|
|
367
451
|
* @param options the modal options {@link CustomModalOptions}.
|
|
368
452
|
*/
|
|
369
|
-
showCustomRightPanel(content: ReactElement, { size = 'medium', onClose
|
|
453
|
+
showCustomRightPanel(content: ReactElement, { size = 'medium', onClose, ignoreFirstFocusOnCloseButton = true, stack = false, id }:
|
|
454
|
+
CustomRightPanelOptions = {}) {
|
|
370
455
|
if (!this.elements?.rightPanel) throw new ElementNotFound('right panel overlay', elementIds.rightPanel)
|
|
371
456
|
if (!this.setContent.rightPanel) throw new LayoutError('unable to show right panel overlay, because it has not been setup yet.')
|
|
372
|
-
|
|
373
|
-
this.
|
|
374
|
-
this.
|
|
457
|
+
const panel = { element: content, onClose, size, stack, id }
|
|
458
|
+
const currentPanelSize = last(this.modals)?.size
|
|
459
|
+
this.setContent.rightPanel(stack ? [...this.panels, panel] : [panel])
|
|
460
|
+
// we should remove the previous size, if any, before showing the panel
|
|
461
|
+
if (currentPanelSize) this.elements.rightPanel.classList.remove(currentPanelSize)
|
|
375
462
|
setTimeout(() => {
|
|
376
|
-
this.showOverlay(this.elements?.rightPanel, [], true, true)
|
|
463
|
+
this.showOverlay(this.elements?.rightPanel, [size], true, true, ignoreFirstFocusOnCloseButton)
|
|
377
464
|
})
|
|
378
465
|
}
|
|
379
466
|
|
|
@@ -385,10 +472,12 @@ class LayoutOverlayManager {
|
|
|
385
472
|
*
|
|
386
473
|
* @param options the modal options: {@link OverlayContentProps} & { size: {@link ModalSize} }.
|
|
387
474
|
*/
|
|
388
|
-
showRightPanel({ size,
|
|
475
|
+
showRightPanel({ size, ignoreFirstFocusOnCloseButton, stack, onGoBack, id, ...props }:
|
|
476
|
+
OverlayContentProps & Pick<CustomRightPanelOptions, 'size' | 'ignoreFirstFocusOnCloseButton' | 'stack' | 'id'>) {
|
|
477
|
+
const handleBack = onGoBack ?? ((stack && this.panels.length >= 1) ? () => this.popRightPanel() : undefined)
|
|
389
478
|
this.showCustomRightPanel(
|
|
390
|
-
<OverlayContent {...props} onClose={() => this.closeRightPanel()} type="panel" />,
|
|
391
|
-
{ size, onClose: props.onClose },
|
|
479
|
+
<OverlayContent {...props} onGoBack={handleBack} onClose={() => this.closeRightPanel()} type="panel" />,
|
|
480
|
+
{ size, onClose: props.onClose, ignoreFirstFocusOnCloseButton, stack, id },
|
|
392
481
|
)
|
|
393
482
|
}
|
|
394
483
|
|
|
@@ -402,28 +491,31 @@ class LayoutOverlayManager {
|
|
|
402
491
|
}
|
|
403
492
|
|
|
404
493
|
/**
|
|
405
|
-
* Closes
|
|
494
|
+
* Closes all opened modals.
|
|
406
495
|
* @param runCloseListener whether or not to run the function `onClose` passed to `showModal` or `showCustomModal`. Defaults to true.
|
|
407
496
|
*/
|
|
408
497
|
closeModal(runCloseListener = true) {
|
|
409
498
|
this.elements?.modal?.classList.remove('visible')
|
|
410
|
-
this.
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
499
|
+
const shouldHideBackdrop = this.panels.length === 0
|
|
500
|
+
if (shouldHideBackdrop) this.elements?.backdrop?.setAttribute('class', '')
|
|
501
|
+
if (runCloseListener) {
|
|
502
|
+
this.modals.forEach((modal) => {
|
|
503
|
+
const onClose = modal.onClose
|
|
504
|
+
// setting it to undefined before running it prevents nested calls to closeModal from generating infinite loops.
|
|
505
|
+
modal.onClose = undefined
|
|
506
|
+
onClose?.()
|
|
507
|
+
})
|
|
416
508
|
}
|
|
417
509
|
const animationMS = parseFloat(valueOfLayoutVar('--modal-animation-duration')) * 1000
|
|
418
510
|
setTimeout(
|
|
419
511
|
() => {
|
|
420
|
-
if (this.elements?.backdrop?.classList.contains('visible')) {
|
|
512
|
+
if (shouldHideBackdrop && this.elements?.backdrop?.classList.contains('visible')) {
|
|
421
513
|
// eslint-disable-next-line no-console
|
|
422
514
|
console.warn(multipleCallsWarning('modal', animationMS))
|
|
423
515
|
this.elements?.modal?.classList.remove('visible')
|
|
424
516
|
}
|
|
425
|
-
if (this.setContent.modal) this.setContent.modal(
|
|
426
|
-
this.hideOverlay(this.elements?.modal)
|
|
517
|
+
if (this.setContent.modal) this.setContent.modal([])
|
|
518
|
+
if (shouldHideBackdrop) this.hideOverlay(this.elements?.modal)
|
|
427
519
|
this.focusLastActiveElement()
|
|
428
520
|
},
|
|
429
521
|
animationMS,
|
|
@@ -431,35 +523,95 @@ class LayoutOverlayManager {
|
|
|
431
523
|
}
|
|
432
524
|
|
|
433
525
|
/**
|
|
434
|
-
* Closes the
|
|
526
|
+
* Closes the top-most modal in the stack. Will behave like `closeModal` if only a single modal exists in the stack.
|
|
527
|
+
* @param amount number of modals to pop. Defaults to 1.
|
|
528
|
+
* @param runCloseListener whether or not to run the function `onClose` passed to `showModal` or `showCustomModal`. Defaults to true.
|
|
529
|
+
*/
|
|
530
|
+
popModal(amount = 1, runCloseListener = true) {
|
|
531
|
+
if (amount <= 0) return
|
|
532
|
+
if (this.modals.length <= amount) return this.closeModal(runCloseListener)
|
|
533
|
+
for (let i = 0; i < amount; i++) {
|
|
534
|
+
const modalToClose = this.modals.pop()! // "!": because of the second "if", the array can't have less than "amount + 1" elements
|
|
535
|
+
if (runCloseListener) modalToClose.onClose?.()
|
|
536
|
+
this.elements?.modal?.classList.remove(modalToClose.size)
|
|
537
|
+
}
|
|
538
|
+
const topModal = last(this.modals)! // "!": because of the second "if", the array can't have less than "amount + 1" elements
|
|
539
|
+
this.elements?.modal?.classList.add(topModal.size)
|
|
540
|
+
this.setContent.modal?.([...this.modals])
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Close all modals in the stack of modal until the modal with the given id is found. If no modal with the given id is found, nothing
|
|
545
|
+
* happens.
|
|
546
|
+
* @param id the id of the modal to pop to.
|
|
547
|
+
* @param inclusive when true, the modal with the given id is also popped. Defaults to false.
|
|
548
|
+
*/
|
|
549
|
+
popModalTo(id: string, inclusive = false) {
|
|
550
|
+
const index = this.modals.findIndex(m => m.id === id)
|
|
551
|
+
if (index >= 0) this.popModal(this.modals.length - index - (inclusive ? 0 : 1))
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Closes all opened right panels.
|
|
435
556
|
* @param runCloseListener whether or not to run the function `onClose` passed to `showRightPanel` or `showCustomRightPanel`. Defaults to
|
|
436
557
|
* true.
|
|
437
558
|
*/
|
|
438
559
|
closeRightPanel(runCloseListener = true) {
|
|
439
560
|
this.elements?.rightPanel?.classList.remove('visible')
|
|
440
|
-
this.
|
|
441
|
-
if (
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
561
|
+
const shouldHideBackdrop = this.modals.length === 0
|
|
562
|
+
if (shouldHideBackdrop) this.elements?.backdrop?.setAttribute('class', '')
|
|
563
|
+
if (runCloseListener) {
|
|
564
|
+
this.panels.forEach((panel) => {
|
|
565
|
+
const onClose = panel.onClose
|
|
566
|
+
// setting it to undefined before running it prevents nested calls to closeRightPanel from generating infinite loops.
|
|
567
|
+
panel.onClose = undefined
|
|
568
|
+
onClose?.()
|
|
569
|
+
})
|
|
446
570
|
}
|
|
447
571
|
const animationMS = parseFloat(valueOfLayoutVar('--right-panel-animation-duration')) * 1000
|
|
448
572
|
setTimeout(
|
|
449
573
|
() => {
|
|
450
|
-
if (this.elements?.backdrop?.classList.contains('visible')) {
|
|
574
|
+
if (shouldHideBackdrop && this.elements?.backdrop?.classList.contains('visible')) {
|
|
451
575
|
// eslint-disable-next-line no-console
|
|
452
576
|
console.warn(multipleCallsWarning('rightPanel', animationMS))
|
|
453
577
|
this.elements?.rightPanel?.classList.remove('visible')
|
|
454
578
|
}
|
|
455
|
-
if (this.setContent.rightPanel) this.setContent.rightPanel(
|
|
456
|
-
this.hideOverlay(this.elements?.rightPanel)
|
|
579
|
+
if (this.setContent.rightPanel) this.setContent.rightPanel([])
|
|
580
|
+
if (shouldHideBackdrop) this.hideOverlay(this.elements?.rightPanel)
|
|
457
581
|
this.focusLastActiveElement()
|
|
458
582
|
},
|
|
459
583
|
animationMS,
|
|
460
584
|
)
|
|
461
585
|
}
|
|
462
586
|
|
|
587
|
+
/**
|
|
588
|
+
* Closes the top-most modal in the stack. Will behave like `closeModal` if only a single modal exists in the stack.
|
|
589
|
+
* @param runCloseListener whether or not to run the function `onClose` passed to `showModal` or `showCustomModal`. Defaults to true.
|
|
590
|
+
*/
|
|
591
|
+
popRightPanel(amount = 1, runCloseListener = true) {
|
|
592
|
+
if (amount <= 0) return
|
|
593
|
+
if (this.panels.length <= amount) return this.closeRightPanel(runCloseListener)
|
|
594
|
+
for (let i = 0; i < amount; i++) {
|
|
595
|
+
const panelToClose = this.panels.pop()! // "!": because of the second "if", the array can't have less than "amount + 1" elements
|
|
596
|
+
if (runCloseListener) panelToClose.onClose?.()
|
|
597
|
+
this.elements?.rightPanel?.classList.remove(panelToClose.size)
|
|
598
|
+
}
|
|
599
|
+
const topPanel = last(this.panels)! // "!": because of the second "if", the array can't have less than "amount + 1" elements
|
|
600
|
+
this.elements?.rightPanel?.classList.add(topPanel.size)
|
|
601
|
+
this.setContent.rightPanel?.([...this.panels])
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Close all right panels in the stack of panels until the panel with the given id is found. If no panel with the given id is found,
|
|
606
|
+
* nothing happens.
|
|
607
|
+
* @param id the id of the right panel to pop to.
|
|
608
|
+
* @param inclusive when true, the right panel with the given id is also popped. Defaults to false.
|
|
609
|
+
*/
|
|
610
|
+
popRightPanelTo(id: string, inclusive = false) {
|
|
611
|
+
const index = this.panels.findIndex(m => m.id === id)
|
|
612
|
+
if (index >= 0) this.popRightPanel(this.panels.length - index - (inclusive ? 0 : 1))
|
|
613
|
+
}
|
|
614
|
+
|
|
463
615
|
/**
|
|
464
616
|
* Closes the bottom dialog if it's open.
|
|
465
617
|
*/
|
|
@@ -13,7 +13,7 @@ const dictionary = {
|
|
|
13
13
|
'HIGH.label': 'High',
|
|
14
14
|
'MEDIUM.label': 'Medium',
|
|
15
15
|
'LOW.label': 'Low',
|
|
16
|
-
|
|
16
|
+
viewAll: 'View all notifications',
|
|
17
17
|
openNotifications: 'View notifications',
|
|
18
18
|
hasUnread: 'Has Unread notifications',
|
|
19
19
|
close: 'Close',
|
|
@@ -32,7 +32,7 @@ const dictionary = {
|
|
|
32
32
|
'HIGH.label': 'Alto',
|
|
33
33
|
'MEDIUM.label': 'Médio',
|
|
34
34
|
'LOW.label': 'Baixo',
|
|
35
|
-
|
|
35
|
+
viewAll: 'Ver todas as notificações',
|
|
36
36
|
openNotifications: 'Visualizar notificações',
|
|
37
37
|
hasUnread: 'Existem notificações não lidas',
|
|
38
38
|
close: 'Fechar',
|
|
@@ -56,7 +56,7 @@ const ContentBox = styled.section`
|
|
|
56
56
|
flex-direction: column;
|
|
57
57
|
flex: 1;
|
|
58
58
|
}
|
|
59
|
-
header {
|
|
59
|
+
> header {
|
|
60
60
|
display: flex;
|
|
61
61
|
flex-direction: row;
|
|
62
62
|
margin-bottom: 1.25rem;
|
|
@@ -79,7 +79,7 @@ export const OverlayContent = ({ children, title, subtitle, className, style, on
|
|
|
79
79
|
<ArrowLeft />
|
|
80
80
|
</IconButton>
|
|
81
81
|
)}
|
|
82
|
-
<Text as="
|
|
82
|
+
<Text as="h1" appearance={type === 'modal' ? 'h3' : 'h4'}>{title}</Text>
|
|
83
83
|
</Flex>
|
|
84
84
|
{subtitle && <Text appearance="body2" colorScheme="light.700">{subtitle}</Text>}
|
|
85
85
|
</Flex>
|
|
@@ -149,6 +149,9 @@ const Section = ({
|
|
|
149
149
|
function click() {
|
|
150
150
|
if (onClick) onClick()
|
|
151
151
|
hideOverlayImmediately()
|
|
152
|
+
const classList = document.getElementById(elementIds.layout)?.classList
|
|
153
|
+
if (!classList) return
|
|
154
|
+
classList.remove('menu-manual')
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
const labelText = typeof label === 'string' ? label : label.id
|
|
@@ -238,6 +241,7 @@ const OverlayRenderer = ({ content, customContent }: Pick<MenuSection, 'content'
|
|
|
238
241
|
*/
|
|
239
242
|
export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
240
243
|
const Link = useAnchorTag()
|
|
244
|
+
const [showSupport, setShowSupport] = useState(true)
|
|
241
245
|
const t = useTranslate(dictionary)
|
|
242
246
|
// this is a mock state only used to force an update on the component.
|
|
243
247
|
const [_, setUpdate] = useState(0)
|
|
@@ -246,11 +250,15 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
246
250
|
const layout = document.getElementById('layout')
|
|
247
251
|
if (!layout) return
|
|
248
252
|
if (layout.classList.contains('menu-compact')) {
|
|
249
|
-
layout.classList.remove('menu-compact'
|
|
253
|
+
layout.classList.remove('menu-compact')
|
|
254
|
+
setShowSupport(true)
|
|
250
255
|
} else {
|
|
251
|
-
layout.classList.add('menu-compact'
|
|
256
|
+
layout.classList.add('menu-compact')
|
|
257
|
+
setShowSupport(false)
|
|
252
258
|
}
|
|
253
259
|
|
|
260
|
+
layout.classList.add('menu-manual')
|
|
261
|
+
|
|
254
262
|
setUpdate(current => current + 1)
|
|
255
263
|
}, [])
|
|
256
264
|
// the current overlay showing, when the user hovers the section icon. This is the index of the item in the sections array.
|
|
@@ -265,6 +273,9 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
265
273
|
const sectionItems = useMemo(
|
|
266
274
|
() => sections.reduce<JSX.Element[]>(
|
|
267
275
|
(result, s, i) => {
|
|
276
|
+
if (s.label === t.contactUs && !showSupport) {
|
|
277
|
+
return result
|
|
278
|
+
}
|
|
268
279
|
if (s.type) {
|
|
269
280
|
return (s.hidden ? result : [
|
|
270
281
|
...result, <Box key={`custom-element-${i}`}
|
|
@@ -293,7 +304,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
293
304
|
|
|
294
305
|
}, [],
|
|
295
306
|
),
|
|
296
|
-
[sections],
|
|
307
|
+
[sections, showSupport],
|
|
297
308
|
)
|
|
298
309
|
|
|
299
310
|
function onPressEscape() {
|
|
@@ -333,8 +344,8 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
333
344
|
<>
|
|
334
345
|
<MenuSectionGroup className="open root no-indentation">{sectionItems}</MenuSectionGroup>
|
|
335
346
|
|
|
336
|
-
<Flex
|
|
337
|
-
<RateAndContactUsItem {...props} />
|
|
347
|
+
<Flex alignItems="center">
|
|
348
|
+
<RateAndContactUsItem {...props} showSupport={showSupport} />
|
|
338
349
|
<ReactTooltip
|
|
339
350
|
text={t.toggle}
|
|
340
351
|
position="right"
|
|
@@ -383,7 +394,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
383
394
|
)
|
|
384
395
|
}
|
|
385
396
|
|
|
386
|
-
const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
|
|
397
|
+
const RateAndContactUsItem = ({ showSupport, ...props }: Omit<MenuProps, 'sections'> & { showSupport?: boolean }) => {
|
|
387
398
|
const t = useTranslate(dictionary)
|
|
388
399
|
const alreadyAnswered = localStorage.getItem('RATED_US_IN')
|
|
389
400
|
const hasAnsweredLess30Days = alreadyAnswered ? isLessThan30Days(new Date(+alreadyAnswered), new Date(Date.now())) : false
|
|
@@ -409,7 +420,7 @@ const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
|
|
|
409
420
|
className="collapse gradient grow-shrink" colorScheme="light.contrastText">{t.rateUs}</Text>
|
|
410
421
|
</button>
|
|
411
422
|
}
|
|
412
|
-
{(props.contactUs?.show) &&
|
|
423
|
+
{(props.contactUs?.show && showSupport) && (
|
|
413
424
|
<Link href={props.contactUs?.href} className="toggle sections-footer" onClick={props.contactUs?.onClick}
|
|
414
425
|
{...(props.contactUs.active ? { 'aria-current': 'page' } : undefined)} target={props.contactUs?.target}>
|
|
415
426
|
<IconBox aria-label={t.contactIcon}>
|
|
@@ -418,7 +429,7 @@ const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
|
|
|
418
429
|
<Text appearance="microtext1" ml={8} sx={{ marginTop: '3px' }}
|
|
419
430
|
className="collapse" colorScheme="light.contrastText">{t.contactUs}</Text>
|
|
420
431
|
</Link>
|
|
421
|
-
}
|
|
432
|
+
)}
|
|
422
433
|
</>
|
|
423
434
|
}
|
|
424
435
|
|
|
@@ -313,6 +313,13 @@ export interface MenuPropsWithDynamicContent extends BaseMenuProps {
|
|
|
313
313
|
* Identifies each content that might be rendered by the menu. This prevents React Hook errors when the content is a React Hook function.
|
|
314
314
|
*/
|
|
315
315
|
contentKey: React.Key,
|
|
316
|
+
/**
|
|
317
|
+
* The function that creates a config to render a third level nav menu content. It will be called only when the content is rendered,
|
|
318
|
+
* i.e. only when the content really needs to be rendered.
|
|
319
|
+
*
|
|
320
|
+
* Tip: this function can be a React Hook.
|
|
321
|
+
*/
|
|
322
|
+
innerContent?: MenuSectionContent | (() => MenuSectionContent),
|
|
316
323
|
}
|
|
317
324
|
|
|
318
325
|
export type MenuProps = MenuPropsWithStaticContent | MenuPropsWithDynamicContent
|
|
@@ -35,7 +35,9 @@ export const StepNavigation = ({ stepKey, nextButton, prevButton }: NavigationPr
|
|
|
35
35
|
const t = useTranslate(translations)
|
|
36
36
|
|
|
37
37
|
return <Flex w={12} px={5} py={2} mt="-1px" bg="inverse.500" justifyContent="space-between" alignItems="center">
|
|
38
|
-
|
|
38
|
+
{ steps.length > 1 &&
|
|
39
|
+
<Text appearance="microtext1" colorScheme="inverse.contrastText">{currentStep + 1} {t.of} {steps.length}</Text>
|
|
40
|
+
}
|
|
39
41
|
<Flex sx={{ gap: '8px' }}>
|
|
40
42
|
{currentStep >= 1 &&
|
|
41
43
|
<Button sx={{ paddingInline: '20px' }} onClick={() => {
|
package/src/dictionary.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Dictionary, getLanguage, useTranslate } from '@stack-spot/portal-translate'
|
|
1
|
+
import { Dictionary, getLanguage, ptEn, useTranslate } from '@stack-spot/portal-translate'
|
|
2
2
|
|
|
3
3
|
const dictionary = {
|
|
4
4
|
en: {
|
|
@@ -25,6 +25,6 @@ const dictionary = {
|
|
|
25
25
|
export const useDictionary = () => useTranslate(dictionary)
|
|
26
26
|
|
|
27
27
|
export function getDictionary() {
|
|
28
|
-
const language = getLanguage()
|
|
28
|
+
const language = getLanguage(ptEn)
|
|
29
29
|
return dictionary[language]
|
|
30
30
|
}
|
package/src/elements.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { RateComponent } from './components/Rate'
|
|
|
12
12
|
export { shouldShowNpsModal } from './components/Rate/hook'
|
|
13
13
|
export { ShowFeedbackModal, showRateUsModal } from './components/Rate/show-rate-us-modals'
|
|
14
14
|
export * from './components/tour'
|
|
15
|
+
export { TypeForm } from './components/TypeForm'
|
|
15
16
|
export { useTypeFormEffect } from './components/TypeForm/hook'
|
|
16
17
|
export { showTypeFormModal } from './components/TypeForm/show-typeform-modal'
|
|
17
18
|
export * from './elements'
|