@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/Layout.d.ts +5 -1
  3. package/dist/Layout.d.ts.map +1 -1
  4. package/dist/Layout.js +28 -9
  5. package/dist/Layout.js.map +1 -1
  6. package/dist/LayoutOverlayManager.d.ts +77 -11
  7. package/dist/LayoutOverlayManager.d.ts.map +1 -1
  8. package/dist/LayoutOverlayManager.js +152 -47
  9. package/dist/LayoutOverlayManager.js.map +1 -1
  10. package/dist/components/NotificationCenter/NotificationsPanelFooter.js +1 -1
  11. package/dist/components/NotificationCenter/NotificationsPanelFooter.js.map +1 -1
  12. package/dist/components/NotificationCenter/dictionary.d.ts +1 -1
  13. package/dist/components/NotificationCenter/dictionary.d.ts.map +1 -1
  14. package/dist/components/NotificationCenter/dictionary.js +2 -2
  15. package/dist/components/NotificationCenter/dictionary.js.map +1 -1
  16. package/dist/components/OverlayContent.js +2 -2
  17. package/dist/components/error/ErrorBoundary.d.ts.map +1 -1
  18. package/dist/components/error/SilentErrorBoundary.d.ts.map +1 -1
  19. package/dist/components/menu/MenuSections.d.ts.map +1 -1
  20. package/dist/components/menu/MenuSections.js +10 -5
  21. package/dist/components/menu/MenuSections.js.map +1 -1
  22. package/dist/components/menu/types.d.ts +7 -0
  23. package/dist/components/menu/types.d.ts.map +1 -1
  24. package/dist/components/tour/StepNavigation.d.ts.map +1 -1
  25. package/dist/components/tour/StepNavigation.js +2 -1
  26. package/dist/components/tour/StepNavigation.js.map +1 -1
  27. package/dist/dictionary.js +2 -2
  28. package/dist/dictionary.js.map +1 -1
  29. package/dist/elements.d.ts +1 -0
  30. package/dist/elements.d.ts.map +1 -1
  31. package/dist/elements.js +1 -0
  32. package/dist/elements.js.map +1 -1
  33. package/dist/layout.css +30 -3
  34. package/package.json +6 -4
  35. package/src/Layout.tsx +67 -18
  36. package/src/LayoutOverlayManager.tsx +186 -48
  37. package/src/components/NotificationCenter/NotificationsPanelFooter.tsx +1 -1
  38. package/src/components/NotificationCenter/dictionary.ts +2 -2
  39. package/src/components/OverlayContent.tsx +2 -2
  40. package/src/components/menu/MenuSections.tsx +11 -5
  41. package/src/components/menu/types.ts +7 -0
  42. package/src/components/tour/StepNavigation.tsx +3 -1
  43. package/src/dictionary.ts +2 -2
  44. package/src/elements.ts +1 -0
  45. 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 = ((content: ReactElement | undefined) => void) | undefined
34
+ type SetContentFn = (content: ModalContent[]) => void
25
35
 
26
36
  interface OverlayContentSetter {
27
37
  modal?: SetContentFn,
28
38
  rightPanel?: SetContentFn,
29
- bottomDialog?: SetContentFn,
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<ReactElement | undefined>()
136
- const [rightPanel, setRightPanel] = useState<ReactElement | undefined>()
181
+ const [modal, setModal] = useState<ModalContent[]>([])
182
+ const [rightPanel, setRightPanel] = useState<ModalContent[]>([])
137
183
  const [bottomDialog, setBottomDialog] = useState<ReactElement | undefined>()
138
- this.setContent.modal = setModal
139
- this.setContent.rightPanel = setRightPanel
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
- this.onModalClose = onClose
251
- this.setContent.modal(content)
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?: boolean }) {
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 === 'modal') {
278
- this.showCustomModal(
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), ignoreFirstFocusOnCloseButton },
357
+ { size: size as RightPanelSize, onClose: () => resolve(dialogResult) },
288
358
  )
289
359
  } else {
290
- this.showCustomRightPanel(
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: size as RightPanelSize, onClose: () => resolve(dialogResult) },
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 }: CustomRightPanelOptions = {}) {
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
- this.onModalClose = onClose
387
- this.setContent.rightPanel(content)
388
- this.elements?.rightPanel.classList.add(size)
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, ...props }: OverlayContentProps & { size?: RightPanelSize }) {
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 the modal if it's open.
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.elements?.backdrop?.setAttribute('class', '')
425
- if (runCloseListener && this.onModalClose) {
426
- const onClose = this.onModalClose
427
- // setting it to undefined before running it prevents nested calls to closeModal from generating infinite loops.
428
- this.onModalClose = undefined
429
- onClose()
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(undefined)
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 right panel if it's open.
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.elements?.backdrop?.setAttribute('class', '')
455
- if (runCloseListener && this.onModalClose) {
456
- const onClose = this.onModalClose
457
- // setting it to undefined before running it prevents nested calls to closeRightPanel from generating infinite loops.
458
- this.onModalClose = undefined
459
- onClose()
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(undefined)
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
  */
@@ -18,7 +18,7 @@ export const NotificationPanelFooter = ({ onClose }: { onClose: () => void }) =>
18
18
  href={controller.config.notificationsPath}
19
19
  >
20
20
  <Text appearance="microtext1">
21
- {t.seeAll}
21
+ {t.viewAll}
22
22
  </Text>
23
23
  </Button>
24
24
  )
@@ -13,7 +13,7 @@ const dictionary = {
13
13
  'HIGH.label': 'High',
14
14
  'MEDIUM.label': 'Medium',
15
15
  'LOW.label': 'Low',
16
- seeAll: 'See all notifications',
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
- seeAll: 'Ver todas as notificações',
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="h2" appearance={type === 'modal' ? 'h3' : 'h4'}>{title}</Text>
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
- <Text appearance="microtext1" colorScheme="inverse.contrastText">{currentStep + 1} {t.of} {steps.length}</Text>
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
@@ -12,6 +12,7 @@ export const elementIds = {
12
12
  header: 'header',
13
13
  menu: 'menu',
14
14
  menuContent: 'menuContent',
15
+ menuInnerContent: 'menuInnerContent',
15
16
  menuSections: 'menuSections',
16
17
  accessibilityAnnouncer: 'accessibilityAnnouncer',
17
18
  } as const
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.no-menu-sections:not(.menu-content-visible) #page {
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
- #menuContent .goBackLink {
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;