@stack-spot/portal-layout 2.35.1 → 2.36.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/dist/Layout.d.ts +5 -1
- package/dist/Layout.d.ts.map +1 -1
- package/dist/Layout.js +28 -9
- 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 +152 -47
- package/dist/LayoutOverlayManager.js.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 +10 -5
- 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/layout.css +30 -3
- package/package.json +6 -4
- package/src/Layout.tsx +67 -18
- package/src/LayoutOverlayManager.tsx +186 -48
- 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 +11 -5
- 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/layout.css +30 -3
|
@@ -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,8 +344,8 @@ class LayoutOverlayManager {
|
|
|
274
344
|
let dialogResult = false
|
|
275
345
|
return new Promise((resolve, reject) => {
|
|
276
346
|
try {
|
|
277
|
-
if (options.type === '
|
|
278
|
-
this.
|
|
347
|
+
if (options.type === 'panel') {
|
|
348
|
+
this.showCustomRightPanel(
|
|
279
349
|
<Dialog
|
|
280
350
|
{...options}
|
|
281
351
|
onCancel={() => this.closeModal()}
|
|
@@ -284,10 +354,10 @@ class LayoutOverlayManager {
|
|
|
284
354
|
this.closeModal()
|
|
285
355
|
}}
|
|
286
356
|
/>,
|
|
287
|
-
{ size, onClose: () => resolve(dialogResult)
|
|
357
|
+
{ size: size as RightPanelSize, onClose: () => resolve(dialogResult) },
|
|
288
358
|
)
|
|
289
359
|
} else {
|
|
290
|
-
this.
|
|
360
|
+
this.showCustomModal(
|
|
291
361
|
<Dialog
|
|
292
362
|
{...options}
|
|
293
363
|
onCancel={() => this.closeModal()}
|
|
@@ -296,7 +366,7 @@ class LayoutOverlayManager {
|
|
|
296
366
|
this.closeModal()
|
|
297
367
|
}}
|
|
298
368
|
/>,
|
|
299
|
-
{ size
|
|
369
|
+
{ size, onClose: () => resolve(dialogResult), ignoreFirstFocusOnCloseButton },
|
|
300
370
|
)
|
|
301
371
|
}
|
|
302
372
|
} catch (error) {
|
|
@@ -380,14 +450,17 @@ class LayoutOverlayManager {
|
|
|
380
450
|
* @param content a react element with the modal content.
|
|
381
451
|
* @param options the modal options {@link CustomModalOptions}.
|
|
382
452
|
*/
|
|
383
|
-
showCustomRightPanel(content: ReactElement, { size = 'medium', onClose
|
|
453
|
+
showCustomRightPanel(content: ReactElement, { size = 'medium', onClose, ignoreFirstFocusOnCloseButton = true, stack = false, id }:
|
|
454
|
+
CustomRightPanelOptions = {}) {
|
|
384
455
|
if (!this.elements?.rightPanel) throw new ElementNotFound('right panel overlay', elementIds.rightPanel)
|
|
385
456
|
if (!this.setContent.rightPanel) throw new LayoutError('unable to show right panel overlay, because it has not been setup yet.')
|
|
386
|
-
|
|
387
|
-
this.
|
|
388
|
-
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)
|
|
389
462
|
setTimeout(() => {
|
|
390
|
-
this.showOverlay(this.elements?.rightPanel, [], true, true)
|
|
463
|
+
this.showOverlay(this.elements?.rightPanel, [size], true, true, ignoreFirstFocusOnCloseButton)
|
|
391
464
|
})
|
|
392
465
|
}
|
|
393
466
|
|
|
@@ -399,10 +472,12 @@ class LayoutOverlayManager {
|
|
|
399
472
|
*
|
|
400
473
|
* @param options the modal options: {@link OverlayContentProps} & { size: {@link ModalSize} }.
|
|
401
474
|
*/
|
|
402
|
-
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)
|
|
403
478
|
this.showCustomRightPanel(
|
|
404
|
-
<OverlayContent {...props} onClose={() => this.closeRightPanel()} type="panel" />,
|
|
405
|
-
{ size, onClose: props.onClose },
|
|
479
|
+
<OverlayContent {...props} onGoBack={handleBack} onClose={() => this.closeRightPanel()} type="panel" />,
|
|
480
|
+
{ size, onClose: props.onClose, ignoreFirstFocusOnCloseButton, stack, id },
|
|
406
481
|
)
|
|
407
482
|
}
|
|
408
483
|
|
|
@@ -416,28 +491,31 @@ class LayoutOverlayManager {
|
|
|
416
491
|
}
|
|
417
492
|
|
|
418
493
|
/**
|
|
419
|
-
* Closes
|
|
494
|
+
* Closes all opened modals.
|
|
420
495
|
* @param runCloseListener whether or not to run the function `onClose` passed to `showModal` or `showCustomModal`. Defaults to true.
|
|
421
496
|
*/
|
|
422
497
|
closeModal(runCloseListener = true) {
|
|
423
498
|
this.elements?.modal?.classList.remove('visible')
|
|
424
|
-
this.
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
+
})
|
|
430
508
|
}
|
|
431
509
|
const animationMS = parseFloat(valueOfLayoutVar('--modal-animation-duration')) * 1000
|
|
432
510
|
setTimeout(
|
|
433
511
|
() => {
|
|
434
|
-
if (this.elements?.backdrop?.classList.contains('visible')) {
|
|
512
|
+
if (shouldHideBackdrop && this.elements?.backdrop?.classList.contains('visible')) {
|
|
435
513
|
// eslint-disable-next-line no-console
|
|
436
514
|
console.warn(multipleCallsWarning('modal', animationMS))
|
|
437
515
|
this.elements?.modal?.classList.remove('visible')
|
|
438
516
|
}
|
|
439
|
-
if (this.setContent.modal) this.setContent.modal(
|
|
440
|
-
this.hideOverlay(this.elements?.modal)
|
|
517
|
+
if (this.setContent.modal) this.setContent.modal([])
|
|
518
|
+
if (shouldHideBackdrop) this.hideOverlay(this.elements?.modal)
|
|
441
519
|
this.focusLastActiveElement()
|
|
442
520
|
},
|
|
443
521
|
animationMS,
|
|
@@ -445,35 +523,95 @@ class LayoutOverlayManager {
|
|
|
445
523
|
}
|
|
446
524
|
|
|
447
525
|
/**
|
|
448
|
-
* 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.
|
|
449
556
|
* @param runCloseListener whether or not to run the function `onClose` passed to `showRightPanel` or `showCustomRightPanel`. Defaults to
|
|
450
557
|
* true.
|
|
451
558
|
*/
|
|
452
559
|
closeRightPanel(runCloseListener = true) {
|
|
453
560
|
this.elements?.rightPanel?.classList.remove('visible')
|
|
454
|
-
this.
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
})
|
|
460
570
|
}
|
|
461
571
|
const animationMS = parseFloat(valueOfLayoutVar('--right-panel-animation-duration')) * 1000
|
|
462
572
|
setTimeout(
|
|
463
573
|
() => {
|
|
464
|
-
if (this.elements?.backdrop?.classList.contains('visible')) {
|
|
574
|
+
if (shouldHideBackdrop && this.elements?.backdrop?.classList.contains('visible')) {
|
|
465
575
|
// eslint-disable-next-line no-console
|
|
466
576
|
console.warn(multipleCallsWarning('rightPanel', animationMS))
|
|
467
577
|
this.elements?.rightPanel?.classList.remove('visible')
|
|
468
578
|
}
|
|
469
|
-
if (this.setContent.rightPanel) this.setContent.rightPanel(
|
|
470
|
-
this.hideOverlay(this.elements?.rightPanel)
|
|
579
|
+
if (this.setContent.rightPanel) this.setContent.rightPanel([])
|
|
580
|
+
if (shouldHideBackdrop) this.hideOverlay(this.elements?.rightPanel)
|
|
471
581
|
this.focusLastActiveElement()
|
|
472
582
|
},
|
|
473
583
|
animationMS,
|
|
474
584
|
)
|
|
475
585
|
}
|
|
476
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
|
+
|
|
477
615
|
/**
|
|
478
616
|
* Closes the bottom dialog if it's open.
|
|
479
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>
|
|
@@ -241,6 +241,7 @@ const OverlayRenderer = ({ content, customContent }: Pick<MenuSection, 'content'
|
|
|
241
241
|
*/
|
|
242
242
|
export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
243
243
|
const Link = useAnchorTag()
|
|
244
|
+
const [showSupport, setShowSupport] = useState(true)
|
|
244
245
|
const t = useTranslate(dictionary)
|
|
245
246
|
// this is a mock state only used to force an update on the component.
|
|
246
247
|
const [_, setUpdate] = useState(0)
|
|
@@ -250,8 +251,10 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
250
251
|
if (!layout) return
|
|
251
252
|
if (layout.classList.contains('menu-compact')) {
|
|
252
253
|
layout.classList.remove('menu-compact')
|
|
254
|
+
setShowSupport(true)
|
|
253
255
|
} else {
|
|
254
256
|
layout.classList.add('menu-compact')
|
|
257
|
+
setShowSupport(false)
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
layout.classList.add('menu-manual')
|
|
@@ -270,6 +273,9 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
270
273
|
const sectionItems = useMemo(
|
|
271
274
|
() => sections.reduce<JSX.Element[]>(
|
|
272
275
|
(result, s, i) => {
|
|
276
|
+
if (s.label === t.contactUs && !showSupport) {
|
|
277
|
+
return result
|
|
278
|
+
}
|
|
273
279
|
if (s.type) {
|
|
274
280
|
return (s.hidden ? result : [
|
|
275
281
|
...result, <Box key={`custom-element-${i}`}
|
|
@@ -298,7 +304,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
298
304
|
|
|
299
305
|
}, [],
|
|
300
306
|
),
|
|
301
|
-
[sections],
|
|
307
|
+
[sections, showSupport],
|
|
302
308
|
)
|
|
303
309
|
|
|
304
310
|
function onPressEscape() {
|
|
@@ -339,7 +345,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
339
345
|
<MenuSectionGroup className="open root no-indentation">{sectionItems}</MenuSectionGroup>
|
|
340
346
|
|
|
341
347
|
<Flex alignItems="center">
|
|
342
|
-
<RateAndContactUsItem {...props} />
|
|
348
|
+
<RateAndContactUsItem {...props} showSupport={showSupport} />
|
|
343
349
|
<ReactTooltip
|
|
344
350
|
text={t.toggle}
|
|
345
351
|
position="right"
|
|
@@ -388,7 +394,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
388
394
|
)
|
|
389
395
|
}
|
|
390
396
|
|
|
391
|
-
const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
|
|
397
|
+
const RateAndContactUsItem = ({ showSupport, ...props }: Omit<MenuProps, 'sections'> & { showSupport?: boolean }) => {
|
|
392
398
|
const t = useTranslate(dictionary)
|
|
393
399
|
const alreadyAnswered = localStorage.getItem('RATED_US_IN')
|
|
394
400
|
const hasAnsweredLess30Days = alreadyAnswered ? isLessThan30Days(new Date(+alreadyAnswered), new Date(Date.now())) : false
|
|
@@ -414,7 +420,7 @@ const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
|
|
|
414
420
|
className="collapse gradient grow-shrink" colorScheme="light.contrastText">{t.rateUs}</Text>
|
|
415
421
|
</button>
|
|
416
422
|
}
|
|
417
|
-
{(props.contactUs?.show) &&
|
|
423
|
+
{(props.contactUs?.show && showSupport) && (
|
|
418
424
|
<Link href={props.contactUs?.href} className="toggle sections-footer" onClick={props.contactUs?.onClick}
|
|
419
425
|
{...(props.contactUs.active ? { 'aria-current': 'page' } : undefined)} target={props.contactUs?.target}>
|
|
420
426
|
<IconBox aria-label={t.contactIcon}>
|
|
@@ -423,7 +429,7 @@ const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
|
|
|
423
429
|
<Text appearance="microtext1" ml={8} sx={{ marginTop: '3px' }}
|
|
424
430
|
className="collapse" colorScheme="light.contrastText">{t.contactUs}</Text>
|
|
425
431
|
</Link>
|
|
426
|
-
}
|
|
432
|
+
)}
|
|
427
433
|
</>
|
|
428
434
|
}
|
|
429
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/layout.css
CHANGED
|
@@ -98,7 +98,12 @@ body {
|
|
|
98
98
|
left: calc(var(--menu-sections-width) + var(--menu-content-width));
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
#layout.
|
|
101
|
+
#layout.menu-inner-content-visible #page {
|
|
102
|
+
left: calc(var(--menu-sections-width) + calc(var(--menu-content-width) * 2));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#layout.no-menu-sections:not(.menu-content-visible) #page,
|
|
106
|
+
#layout.no-menu-sections:not(.menu-inner-content-visible) #page {
|
|
102
107
|
border-top-left-radius: 0;
|
|
103
108
|
}
|
|
104
109
|
|
|
@@ -363,7 +368,8 @@ body {
|
|
|
363
368
|
pointer-events: auto;
|
|
364
369
|
}
|
|
365
370
|
|
|
366
|
-
#menuContent
|
|
371
|
+
#menuContent,
|
|
372
|
+
#menuInnerContent {
|
|
367
373
|
width: var(--menu-content-width);
|
|
368
374
|
transition: left ease-out var(--menu-animation-duration);
|
|
369
375
|
background-color: var(--light-400);
|
|
@@ -375,7 +381,13 @@ body {
|
|
|
375
381
|
border-top: 1px solid var(--light-300);
|
|
376
382
|
}
|
|
377
383
|
|
|
378
|
-
#
|
|
384
|
+
#menuInnerContent {
|
|
385
|
+
left: var(--menu-content-width);
|
|
386
|
+
border-left: 1px solid var(--light-300);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#menuContent .goBackLink,
|
|
390
|
+
#menuInnerContent .goBackLink {
|
|
379
391
|
display: flex;
|
|
380
392
|
flex-direction: row;
|
|
381
393
|
align-items: center;
|
|
@@ -482,6 +494,21 @@ body {
|
|
|
482
494
|
right: 0;
|
|
483
495
|
}
|
|
484
496
|
|
|
497
|
+
#modal .modal-instance,
|
|
498
|
+
#rightPanel .right-panel-instance {
|
|
499
|
+
flex-direction: column;
|
|
500
|
+
flex: 1;
|
|
501
|
+
overflow: hidden;
|
|
502
|
+
display: none;
|
|
503
|
+
&.active {
|
|
504
|
+
display: flex;
|
|
505
|
+
}
|
|
506
|
+
&.disabled {
|
|
507
|
+
pointer-events: none;
|
|
508
|
+
opacity: 0.7;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
485
512
|
#bottomPanel {
|
|
486
513
|
position: fixed;
|
|
487
514
|
display: flex;
|