@stack-spot/portal-layout 2.35.0 → 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 +92 -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 +14 -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/readme.md +0 -1
- package/src/Layout.tsx +68 -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 +14 -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>
|
|
@@ -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)
|
|
@@ -247,8 +251,10 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
247
251
|
if (!layout) return
|
|
248
252
|
if (layout.classList.contains('menu-compact')) {
|
|
249
253
|
layout.classList.remove('menu-compact')
|
|
254
|
+
setShowSupport(true)
|
|
250
255
|
} else {
|
|
251
256
|
layout.classList.add('menu-compact')
|
|
257
|
+
setShowSupport(false)
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
layout.classList.add('menu-manual')
|
|
@@ -267,6 +273,9 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
267
273
|
const sectionItems = useMemo(
|
|
268
274
|
() => sections.reduce<JSX.Element[]>(
|
|
269
275
|
(result, s, i) => {
|
|
276
|
+
if (s.label === t.contactUs && !showSupport) {
|
|
277
|
+
return result
|
|
278
|
+
}
|
|
270
279
|
if (s.type) {
|
|
271
280
|
return (s.hidden ? result : [
|
|
272
281
|
...result, <Box key={`custom-element-${i}`}
|
|
@@ -295,7 +304,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
295
304
|
|
|
296
305
|
}, [],
|
|
297
306
|
),
|
|
298
|
-
[sections],
|
|
307
|
+
[sections, showSupport],
|
|
299
308
|
)
|
|
300
309
|
|
|
301
310
|
function onPressEscape() {
|
|
@@ -336,7 +345,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
336
345
|
<MenuSectionGroup className="open root no-indentation">{sectionItems}</MenuSectionGroup>
|
|
337
346
|
|
|
338
347
|
<Flex alignItems="center">
|
|
339
|
-
<RateAndContactUsItem {...props} />
|
|
348
|
+
<RateAndContactUsItem {...props} showSupport={showSupport} />
|
|
340
349
|
<ReactTooltip
|
|
341
350
|
text={t.toggle}
|
|
342
351
|
position="right"
|
|
@@ -385,7 +394,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
|
|
|
385
394
|
)
|
|
386
395
|
}
|
|
387
396
|
|
|
388
|
-
const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
|
|
397
|
+
const RateAndContactUsItem = ({ showSupport, ...props }: Omit<MenuProps, 'sections'> & { showSupport?: boolean }) => {
|
|
389
398
|
const t = useTranslate(dictionary)
|
|
390
399
|
const alreadyAnswered = localStorage.getItem('RATED_US_IN')
|
|
391
400
|
const hasAnsweredLess30Days = alreadyAnswered ? isLessThan30Days(new Date(+alreadyAnswered), new Date(Date.now())) : false
|
|
@@ -411,7 +420,7 @@ const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
|
|
|
411
420
|
className="collapse gradient grow-shrink" colorScheme="light.contrastText">{t.rateUs}</Text>
|
|
412
421
|
</button>
|
|
413
422
|
}
|
|
414
|
-
{(props.contactUs?.show) &&
|
|
423
|
+
{(props.contactUs?.show && showSupport) && (
|
|
415
424
|
<Link href={props.contactUs?.href} className="toggle sections-footer" onClick={props.contactUs?.onClick}
|
|
416
425
|
{...(props.contactUs.active ? { 'aria-current': 'page' } : undefined)} target={props.contactUs?.target}>
|
|
417
426
|
<IconBox aria-label={t.contactIcon}>
|
|
@@ -420,7 +429,7 @@ const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
|
|
|
420
429
|
<Text appearance="microtext1" ml={8} sx={{ marginTop: '3px' }}
|
|
421
430
|
className="collapse" colorScheme="light.contrastText">{t.contactUs}</Text>
|
|
422
431
|
</Link>
|
|
423
|
-
}
|
|
432
|
+
)}
|
|
424
433
|
</>
|
|
425
434
|
}
|
|
426
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;
|