@stack-spot/portal-layout 2.32.3 → 2.32.5-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/Layout.d.ts +6 -7
  3. package/dist/Layout.d.ts.map +1 -1
  4. package/dist/Layout.js +30 -11
  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 +159 -46
  9. package/dist/LayoutOverlayManager.js.map +1 -1
  10. package/dist/components/Dialog.d.ts +1 -1
  11. package/dist/components/Dialog.d.ts.map +1 -1
  12. package/dist/components/NotificationCenter/NotificationsPanelFooter.js +1 -1
  13. package/dist/components/NotificationCenter/NotificationsPanelFooter.js.map +1 -1
  14. package/dist/components/NotificationCenter/dictionary.d.ts +1 -1
  15. package/dist/components/NotificationCenter/dictionary.d.ts.map +1 -1
  16. package/dist/components/NotificationCenter/dictionary.js +2 -2
  17. package/dist/components/NotificationCenter/dictionary.js.map +1 -1
  18. package/dist/components/OverlayContent.js +2 -2
  19. package/dist/components/error/ErrorBoundary.d.ts.map +1 -1
  20. package/dist/components/error/SilentErrorBoundary.d.ts.map +1 -1
  21. package/dist/components/menu/MenuSections.d.ts.map +1 -1
  22. package/dist/components/menu/MenuSections.js +17 -7
  23. package/dist/components/menu/MenuSections.js.map +1 -1
  24. package/dist/components/menu/types.d.ts +7 -0
  25. package/dist/components/menu/types.d.ts.map +1 -1
  26. package/dist/components/tour/StepNavigation.d.ts.map +1 -1
  27. package/dist/components/tour/StepNavigation.js +2 -1
  28. package/dist/components/tour/StepNavigation.js.map +1 -1
  29. package/dist/dictionary.js +2 -2
  30. package/dist/dictionary.js.map +1 -1
  31. package/dist/elements.d.ts +1 -0
  32. package/dist/elements.d.ts.map +1 -1
  33. package/dist/elements.js +1 -0
  34. package/dist/elements.js.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/layout.css +33 -5
  40. package/package.json +6 -4
  41. package/readme.md +0 -1
  42. package/src/Layout.tsx +70 -22
  43. package/src/LayoutOverlayManager.tsx +206 -54
  44. package/src/components/Dialog.tsx +1 -1
  45. package/src/components/NotificationCenter/NotificationsPanelFooter.tsx +1 -1
  46. package/src/components/NotificationCenter/dictionary.ts +2 -2
  47. package/src/components/OverlayContent.tsx +2 -2
  48. package/src/components/menu/MenuSections.tsx +19 -8
  49. package/src/components/menu/types.ts +7 -0
  50. package/src/components/tour/StepNavigation.tsx +3 -1
  51. package/src/dictionary.ts +2 -2
  52. package/src/elements.ts +1 -0
  53. package/src/index.ts +1 -0
  54. package/src/layout.css +33 -5
@@ -1,7 +1,9 @@
1
1
  /* eslint-disable react-hooks/rules-of-hooks */
2
2
 
3
3
  import { Button } from '@citric/core'
4
+ import { ModalContent } from '@citric/ui'
4
5
  import { focusAccessibleElement, focusFirstChild } from '@stack-spot/portal-components'
6
+ import { last } from 'lodash'
5
7
  import { ReactElement, useLayoutEffect, useState } from 'react'
6
8
  import { Dialog, DialogOptions } from './components/Dialog'
7
9
  import { CLOSE_OVERLAY_ID, OverlayContent, OverlayContentProps } from './components/OverlayContent'
@@ -20,16 +22,28 @@ interface AlertOptions extends Omit<DialogOptions, 'cancel'> {
20
22
  showButton?: boolean,
21
23
  }
22
24
 
25
+ interface ModalContent {
26
+ id?: string,
27
+ element: React.ReactElement,
28
+ size: CustomModalSize | RightPanelSize,
29
+ onClose?: () => void,
30
+ stack: boolean,
31
+ }
32
+
23
33
  type BottomDialogOptions = Omit<DialogOptions, 'title'>
24
- type SetContentFn = ((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,17 +344,31 @@ class LayoutOverlayManager {
274
344
  let dialogResult = false
275
345
  return new Promise((resolve, reject) => {
276
346
  try {
277
- this.showCustomModal(
278
- <Dialog
279
- {...options}
280
- onCancel={() => this.closeModal()}
281
- onConfirm={() => {
282
- dialogResult = true
283
- this.closeModal()
284
- }}
285
- />,
286
- { size, onClose: () => resolve(dialogResult), ignoreFirstFocusOnCloseButton },
287
- )
347
+ if (options.type === 'panel') {
348
+ this.showCustomRightPanel(
349
+ <Dialog
350
+ {...options}
351
+ onCancel={() => this.closeModal()}
352
+ onConfirm={() => {
353
+ dialogResult = true
354
+ this.closeModal()
355
+ }}
356
+ />,
357
+ { size: size as RightPanelSize, onClose: () => resolve(dialogResult) },
358
+ )
359
+ } else {
360
+ this.showCustomModal(
361
+ <Dialog
362
+ {...options}
363
+ onCancel={() => this.closeModal()}
364
+ onConfirm={() => {
365
+ dialogResult = true
366
+ this.closeModal()
367
+ }}
368
+ />,
369
+ { size, onClose: () => resolve(dialogResult), ignoreFirstFocusOnCloseButton },
370
+ )
371
+ }
288
372
  } catch (error) {
289
373
  reject(error)
290
374
  }
@@ -366,14 +450,17 @@ class LayoutOverlayManager {
366
450
  * @param content a react element with the modal content.
367
451
  * @param options the modal options {@link CustomModalOptions}.
368
452
  */
369
- showCustomRightPanel(content: ReactElement, { size = 'medium', onClose }: CustomRightPanelOptions = {}) {
453
+ showCustomRightPanel(content: ReactElement, { size = 'medium', onClose, ignoreFirstFocusOnCloseButton = true, stack = false, id }:
454
+ CustomRightPanelOptions = {}) {
370
455
  if (!this.elements?.rightPanel) throw new ElementNotFound('right panel overlay', elementIds.rightPanel)
371
456
  if (!this.setContent.rightPanel) throw new LayoutError('unable to show right panel overlay, because it has not been setup yet.')
372
- this.onModalClose = onClose
373
- this.setContent.rightPanel(content)
374
- 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)
375
462
  setTimeout(() => {
376
- this.showOverlay(this.elements?.rightPanel, [], true, true)
463
+ this.showOverlay(this.elements?.rightPanel, [size], true, true, ignoreFirstFocusOnCloseButton)
377
464
  })
378
465
  }
379
466
 
@@ -385,10 +472,12 @@ class LayoutOverlayManager {
385
472
  *
386
473
  * @param options the modal options: {@link OverlayContentProps} & { size: {@link ModalSize} }.
387
474
  */
388
- showRightPanel({ size, ...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)
389
478
  this.showCustomRightPanel(
390
- <OverlayContent {...props} onClose={() => this.closeRightPanel()} type="panel" />,
391
- { size, onClose: props.onClose },
479
+ <OverlayContent {...props} onGoBack={handleBack} onClose={() => this.closeRightPanel()} type="panel" />,
480
+ { size, onClose: props.onClose, ignoreFirstFocusOnCloseButton, stack, id },
392
481
  )
393
482
  }
394
483
 
@@ -402,28 +491,31 @@ class LayoutOverlayManager {
402
491
  }
403
492
 
404
493
  /**
405
- * Closes the modal if it's open.
494
+ * Closes all opened modals.
406
495
  * @param runCloseListener whether or not to run the function `onClose` passed to `showModal` or `showCustomModal`. Defaults to true.
407
496
  */
408
497
  closeModal(runCloseListener = true) {
409
498
  this.elements?.modal?.classList.remove('visible')
410
- this.elements?.backdrop?.setAttribute('class', '')
411
- if (runCloseListener && this.onModalClose) {
412
- const onClose = this.onModalClose
413
- // setting it to undefined before running it prevents nested calls to closeModal from generating infinite loops.
414
- this.onModalClose = undefined
415
- 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
+ })
416
508
  }
417
509
  const animationMS = parseFloat(valueOfLayoutVar('--modal-animation-duration')) * 1000
418
510
  setTimeout(
419
511
  () => {
420
- if (this.elements?.backdrop?.classList.contains('visible')) {
512
+ if (shouldHideBackdrop && this.elements?.backdrop?.classList.contains('visible')) {
421
513
  // eslint-disable-next-line no-console
422
514
  console.warn(multipleCallsWarning('modal', animationMS))
423
515
  this.elements?.modal?.classList.remove('visible')
424
516
  }
425
- if (this.setContent.modal) this.setContent.modal(undefined)
426
- this.hideOverlay(this.elements?.modal)
517
+ if (this.setContent.modal) this.setContent.modal([])
518
+ if (shouldHideBackdrop) this.hideOverlay(this.elements?.modal)
427
519
  this.focusLastActiveElement()
428
520
  },
429
521
  animationMS,
@@ -431,35 +523,95 @@ class LayoutOverlayManager {
431
523
  }
432
524
 
433
525
  /**
434
- * Closes the 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.
435
556
  * @param runCloseListener whether or not to run the function `onClose` passed to `showRightPanel` or `showCustomRightPanel`. Defaults to
436
557
  * true.
437
558
  */
438
559
  closeRightPanel(runCloseListener = true) {
439
560
  this.elements?.rightPanel?.classList.remove('visible')
440
- this.elements?.backdrop?.setAttribute('class', '')
441
- if (runCloseListener && this.onModalClose) {
442
- const onClose = this.onModalClose
443
- // setting it to undefined before running it prevents nested calls to closeRightPanel from generating infinite loops.
444
- this.onModalClose = undefined
445
- 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
+ })
446
570
  }
447
571
  const animationMS = parseFloat(valueOfLayoutVar('--right-panel-animation-duration')) * 1000
448
572
  setTimeout(
449
573
  () => {
450
- if (this.elements?.backdrop?.classList.contains('visible')) {
574
+ if (shouldHideBackdrop && this.elements?.backdrop?.classList.contains('visible')) {
451
575
  // eslint-disable-next-line no-console
452
576
  console.warn(multipleCallsWarning('rightPanel', animationMS))
453
577
  this.elements?.rightPanel?.classList.remove('visible')
454
578
  }
455
- if (this.setContent.rightPanel) this.setContent.rightPanel(undefined)
456
- this.hideOverlay(this.elements?.rightPanel)
579
+ if (this.setContent.rightPanel) this.setContent.rightPanel([])
580
+ if (shouldHideBackdrop) this.hideOverlay(this.elements?.rightPanel)
457
581
  this.focusLastActiveElement()
458
582
  },
459
583
  animationMS,
460
584
  )
461
585
  }
462
586
 
587
+ /**
588
+ * Closes the top-most modal in the stack. Will behave like `closeModal` if only a single modal exists in the stack.
589
+ * @param runCloseListener whether or not to run the function `onClose` passed to `showModal` or `showCustomModal`. Defaults to true.
590
+ */
591
+ popRightPanel(amount = 1, runCloseListener = true) {
592
+ if (amount <= 0) return
593
+ if (this.panels.length <= amount) return this.closeRightPanel(runCloseListener)
594
+ for (let i = 0; i < amount; i++) {
595
+ const panelToClose = this.panels.pop()! // "!": because of the second "if", the array can't have less than "amount + 1" elements
596
+ if (runCloseListener) panelToClose.onClose?.()
597
+ this.elements?.rightPanel?.classList.remove(panelToClose.size)
598
+ }
599
+ const topPanel = last(this.panels)! // "!": because of the second "if", the array can't have less than "amount + 1" elements
600
+ this.elements?.rightPanel?.classList.add(topPanel.size)
601
+ this.setContent.rightPanel?.([...this.panels])
602
+ }
603
+
604
+ /**
605
+ * Close all right panels in the stack of panels until the panel with the given id is found. If no panel with the given id is found,
606
+ * nothing happens.
607
+ * @param id the id of the right panel to pop to.
608
+ * @param inclusive when true, the right panel with the given id is also popped. Defaults to false.
609
+ */
610
+ popRightPanelTo(id: string, inclusive = false) {
611
+ const index = this.panels.findIndex(m => m.id === id)
612
+ if (index >= 0) this.popRightPanel(this.panels.length - index - (inclusive ? 0 : 1))
613
+ }
614
+
463
615
  /**
464
616
  * Closes the bottom dialog if it's open.
465
617
  */
@@ -73,7 +73,7 @@ interface Props extends Omit<DialogOptions, 'message'> {
73
73
  /**
74
74
  * Function to run when the confirm button is clicked.
75
75
  */
76
- onConfirm: () => void,
76
+ onConfirm?: () => void,
77
77
  /**
78
78
  * Function to run when the cancel button is clicked.
79
79
  */
@@ -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>
@@ -149,6 +149,9 @@ const Section = ({
149
149
  function click() {
150
150
  if (onClick) onClick()
151
151
  hideOverlayImmediately()
152
+ const classList = document.getElementById(elementIds.layout)?.classList
153
+ if (!classList) return
154
+ classList.remove('menu-manual')
152
155
  }
153
156
 
154
157
  const labelText = typeof label === 'string' ? label : label.id
@@ -238,6 +241,7 @@ const OverlayRenderer = ({ content, customContent }: Pick<MenuSection, 'content'
238
241
  */
239
242
  export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
240
243
  const Link = useAnchorTag()
244
+ const [showSupport, setShowSupport] = useState(true)
241
245
  const t = useTranslate(dictionary)
242
246
  // this is a mock state only used to force an update on the component.
243
247
  const [_, setUpdate] = useState(0)
@@ -246,11 +250,15 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
246
250
  const layout = document.getElementById('layout')
247
251
  if (!layout) return
248
252
  if (layout.classList.contains('menu-compact')) {
249
- layout.classList.remove('menu-compact', 'menu-manual')
253
+ layout.classList.remove('menu-compact')
254
+ setShowSupport(true)
250
255
  } else {
251
- layout.classList.add('menu-compact', 'menu-manual')
256
+ layout.classList.add('menu-compact')
257
+ setShowSupport(false)
252
258
  }
253
259
 
260
+ layout.classList.add('menu-manual')
261
+
254
262
  setUpdate(current => current + 1)
255
263
  }, [])
256
264
  // the current overlay showing, when the user hovers the section icon. This is the index of the item in the sections array.
@@ -265,6 +273,9 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
265
273
  const sectionItems = useMemo(
266
274
  () => sections.reduce<JSX.Element[]>(
267
275
  (result, s, i) => {
276
+ if (s.label === t.contactUs && !showSupport) {
277
+ return result
278
+ }
268
279
  if (s.type) {
269
280
  return (s.hidden ? result : [
270
281
  ...result, <Box key={`custom-element-${i}`}
@@ -293,7 +304,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
293
304
 
294
305
  }, [],
295
306
  ),
296
- [sections],
307
+ [sections, showSupport],
297
308
  )
298
309
 
299
310
  function onPressEscape() {
@@ -333,8 +344,8 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
333
344
  <>
334
345
  <MenuSectionGroup className="open root no-indentation">{sectionItems}</MenuSectionGroup>
335
346
 
336
- <Flex mb={7} alignItems="center">
337
- <RateAndContactUsItem {...props} />
347
+ <Flex alignItems="center">
348
+ <RateAndContactUsItem {...props} showSupport={showSupport} />
338
349
  <ReactTooltip
339
350
  text={t.toggle}
340
351
  position="right"
@@ -383,7 +394,7 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
383
394
  )
384
395
  }
385
396
 
386
- const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
397
+ const RateAndContactUsItem = ({ showSupport, ...props }: Omit<MenuProps, 'sections'> & { showSupport?: boolean }) => {
387
398
  const t = useTranslate(dictionary)
388
399
  const alreadyAnswered = localStorage.getItem('RATED_US_IN')
389
400
  const hasAnsweredLess30Days = alreadyAnswered ? isLessThan30Days(new Date(+alreadyAnswered), new Date(Date.now())) : false
@@ -409,7 +420,7 @@ const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
409
420
  className="collapse gradient grow-shrink" colorScheme="light.contrastText">{t.rateUs}</Text>
410
421
  </button>
411
422
  }
412
- {(props.contactUs?.show) &&
423
+ {(props.contactUs?.show && showSupport) && (
413
424
  <Link href={props.contactUs?.href} className="toggle sections-footer" onClick={props.contactUs?.onClick}
414
425
  {...(props.contactUs.active ? { 'aria-current': 'page' } : undefined)} target={props.contactUs?.target}>
415
426
  <IconBox aria-label={t.contactIcon}>
@@ -418,7 +429,7 @@ const RateAndContactUsItem = ({ ...props }: Omit<MenuProps, 'sections'>) => {
418
429
  <Text appearance="microtext1" ml={8} sx={{ marginTop: '3px' }}
419
430
  className="collapse" colorScheme="light.contrastText">{t.contactUs}</Text>
420
431
  </Link>
421
- }
432
+ )}
422
433
  </>
423
434
  }
424
435
 
@@ -313,6 +313,13 @@ export interface MenuPropsWithDynamicContent extends BaseMenuProps {
313
313
  * Identifies each content that might be rendered by the menu. This prevents React Hook errors when the content is a React Hook function.
314
314
  */
315
315
  contentKey: React.Key,
316
+ /**
317
+ * The function that creates a config to render a third level nav menu content. It will be called only when the content is rendered,
318
+ * i.e. only when the content really needs to be rendered.
319
+ *
320
+ * Tip: this function can be a React Hook.
321
+ */
322
+ innerContent?: MenuSectionContent | (() => MenuSectionContent),
316
323
  }
317
324
 
318
325
  export type MenuProps = MenuPropsWithStaticContent | MenuPropsWithDynamicContent
@@ -35,7 +35,9 @@ export const StepNavigation = ({ stepKey, nextButton, prevButton }: NavigationPr
35
35
  const t = useTranslate(translations)
36
36
 
37
37
  return <Flex w={12} px={5} py={2} mt="-1px" bg="inverse.500" justifyContent="space-between" alignItems="center">
38
- <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/index.ts CHANGED
@@ -12,6 +12,7 @@ export { RateComponent } from './components/Rate'
12
12
  export { shouldShowNpsModal } from './components/Rate/hook'
13
13
  export { ShowFeedbackModal, showRateUsModal } from './components/Rate/show-rate-us-modals'
14
14
  export * from './components/tour'
15
+ export { TypeForm } from './components/TypeForm'
15
16
  export { useTypeFormEffect } from './components/TypeForm/hook'
16
17
  export { showTypeFormModal } from './components/TypeForm/show-typeform-modal'
17
18
  export * from './elements'