@stack-spot/portal-layout 0.0.64 → 1.0.0

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 (209) hide show
  1. package/dist/Layout.d.ts +57 -5
  2. package/dist/Layout.d.ts.map +1 -1
  3. package/dist/Layout.js +12 -5
  4. package/dist/Layout.js.map +1 -1
  5. package/dist/LayoutOverlayManager.d.ts +173 -6
  6. package/dist/LayoutOverlayManager.d.ts.map +1 -1
  7. package/dist/LayoutOverlayManager.js +118 -9
  8. package/dist/LayoutOverlayManager.js.map +1 -1
  9. package/dist/components/Dialog.d.ts +48 -5
  10. package/dist/components/Dialog.d.ts.map +1 -1
  11. package/dist/components/Dialog.js +7 -2
  12. package/dist/components/Dialog.js.map +1 -1
  13. package/dist/components/Header.d.ts +29 -1
  14. package/dist/components/Header.d.ts.map +1 -1
  15. package/dist/components/Header.js +6 -2
  16. package/dist/components/Header.js.map +1 -1
  17. package/dist/components/OverlayContent.d.ts +22 -0
  18. package/dist/components/OverlayContent.d.ts.map +1 -1
  19. package/dist/components/OverlayContent.js +4 -0
  20. package/dist/components/OverlayContent.js.map +1 -1
  21. package/dist/components/PortalSwitcher.d.ts +14 -0
  22. package/dist/components/PortalSwitcher.d.ts.map +1 -1
  23. package/dist/components/PortalSwitcher.js +9 -6
  24. package/dist/components/PortalSwitcher.js.map +1 -1
  25. package/dist/components/Toaster.d.ts +4 -0
  26. package/dist/components/Toaster.d.ts.map +1 -1
  27. package/dist/components/Toaster.js +5 -1
  28. package/dist/components/Toaster.js.map +1 -1
  29. package/dist/components/UserMenu.d.ts +14 -1
  30. package/dist/components/UserMenu.d.ts.map +1 -1
  31. package/dist/components/UserMenu.js +5 -1
  32. package/dist/components/UserMenu.js.map +1 -1
  33. package/dist/components/error/ErrorBoundary.d.ts +10 -1
  34. package/dist/components/error/ErrorBoundary.d.ts.map +1 -1
  35. package/dist/components/error/ErrorBoundary.js +10 -1
  36. package/dist/components/error/ErrorBoundary.js.map +1 -1
  37. package/dist/components/error/ErrorManager.d.ts +22 -6
  38. package/dist/components/error/ErrorManager.d.ts.map +1 -1
  39. package/dist/components/error/ErrorManager.js +21 -1
  40. package/dist/components/error/ErrorManager.js.map +1 -1
  41. package/dist/components/error/SilentErrorBoundary.d.ts +11 -2
  42. package/dist/components/error/SilentErrorBoundary.d.ts.map +1 -1
  43. package/dist/components/error/SilentErrorBoundary.js +10 -0
  44. package/dist/components/error/SilentErrorBoundary.js.map +1 -1
  45. package/dist/components/menu/MenuContent.d.ts +19 -545
  46. package/dist/components/menu/MenuContent.d.ts.map +1 -1
  47. package/dist/components/menu/MenuContent.js +33 -35
  48. package/dist/components/menu/MenuContent.js.map +1 -1
  49. package/dist/components/menu/MenuSections.d.ts +10 -0
  50. package/dist/components/menu/MenuSections.d.ts.map +1 -1
  51. package/dist/components/menu/MenuSections.js +70 -16
  52. package/dist/components/menu/MenuSections.js.map +1 -1
  53. package/dist/components/menu/PageSelector.d.ts +5 -0
  54. package/dist/components/menu/PageSelector.d.ts.map +1 -1
  55. package/dist/components/menu/PageSelector.js +8 -3
  56. package/dist/components/menu/PageSelector.js.map +1 -1
  57. package/dist/components/menu/types.d.ts +100 -7
  58. package/dist/components/menu/types.d.ts.map +1 -1
  59. package/dist/components/tour/PortalSwitcherStep.d.ts +6 -1
  60. package/dist/components/tour/PortalSwitcherStep.d.ts.map +1 -1
  61. package/dist/components/tour/PortalSwitcherStep.js +6 -1
  62. package/dist/components/tour/PortalSwitcherStep.js.map +1 -1
  63. package/dist/components/types.d.ts +0 -13
  64. package/dist/components/types.d.ts.map +1 -1
  65. package/dist/dictionary.d.ts +3 -0
  66. package/dist/dictionary.d.ts.map +1 -1
  67. package/dist/dictionary.js +3 -0
  68. package/dist/dictionary.js.map +1 -1
  69. package/dist/elements.d.ts +6 -0
  70. package/dist/elements.d.ts.map +1 -1
  71. package/dist/elements.js +6 -0
  72. package/dist/elements.js.map +1 -1
  73. package/dist/index.d.ts +0 -3
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +0 -3
  76. package/dist/index.js.map +1 -1
  77. package/dist/layout.css +1 -5
  78. package/dist/toaster.d.ts +74 -9
  79. package/dist/toaster.d.ts.map +1 -1
  80. package/dist/toaster.js +32 -6
  81. package/dist/toaster.js.map +1 -1
  82. package/dist/utils.d.ts +6 -69
  83. package/dist/utils.d.ts.map +1 -1
  84. package/dist/utils.js +9 -130
  85. package/dist/utils.js.map +1 -1
  86. package/package.json +12 -11
  87. package/readme.md +146 -0
  88. package/src/Layout.tsx +78 -28
  89. package/src/LayoutOverlayManager.tsx +184 -9
  90. package/src/components/Dialog.tsx +49 -6
  91. package/src/components/Header.tsx +31 -3
  92. package/src/components/OverlayContent.tsx +22 -0
  93. package/src/components/PortalSwitcher.tsx +22 -8
  94. package/src/components/Toaster.tsx +10 -2
  95. package/src/components/UserMenu.tsx +14 -1
  96. package/src/components/error/ErrorBoundary.tsx +11 -2
  97. package/src/components/error/ErrorManager.ts +22 -6
  98. package/src/components/error/SilentErrorBoundary.tsx +12 -2
  99. package/src/components/menu/MenuContent.tsx +33 -52
  100. package/src/components/menu/MenuSections.tsx +99 -49
  101. package/src/components/menu/PageSelector.tsx +8 -3
  102. package/src/components/menu/types.ts +100 -8
  103. package/src/components/tour/PortalSwitcherStep.tsx +7 -4
  104. package/src/components/types.ts +0 -14
  105. package/src/dictionary.ts +3 -0
  106. package/src/elements.ts +6 -0
  107. package/src/index.ts +0 -3
  108. package/src/layout.css +1 -5
  109. package/src/toaster.tsx +125 -14
  110. package/src/utils.ts +9 -142
  111. package/dist/components/BottomNotification.d.ts +0 -1
  112. package/dist/components/BottomNotification.d.ts.map +0 -1
  113. package/dist/components/BottomNotification.js +0 -2
  114. package/dist/components/BottomNotification.js.map +0 -1
  115. package/dist/components/BottomPanel.d.ts +0 -1
  116. package/dist/components/BottomPanel.d.ts.map +0 -1
  117. package/dist/components/BottomPanel.js +0 -2
  118. package/dist/components/BottomPanel.js.map +0 -1
  119. package/dist/components/SelectionList.d.ts +0 -36
  120. package/dist/components/SelectionList.d.ts.map +0 -1
  121. package/dist/components/SelectionList.js +0 -140
  122. package/dist/components/SelectionList.js.map +0 -1
  123. package/dist/components/error/ErrorDescriptor.d.ts +0 -12
  124. package/dist/components/error/ErrorDescriptor.d.ts.map +0 -1
  125. package/dist/components/error/ErrorDescriptor.js +0 -17
  126. package/dist/components/error/ErrorDescriptor.js.map +0 -1
  127. package/dist/components/error/ErrorFeedback.d.ts +0 -3
  128. package/dist/components/error/ErrorFeedback.d.ts.map +0 -1
  129. package/dist/components/error/ErrorFeedback.js +0 -66
  130. package/dist/components/error/ErrorFeedback.js.map +0 -1
  131. package/dist/components/menu/use-check-text-overflow.d.ts +0 -6
  132. package/dist/components/menu/use-check-text-overflow.d.ts.map +0 -1
  133. package/dist/components/menu/use-check-text-overflow.js +0 -20
  134. package/dist/components/menu/use-check-text-overflow.js.map +0 -1
  135. package/dist/components/menu/use-keyboard-controls.d.ts +0 -23
  136. package/dist/components/menu/use-keyboard-controls.d.ts.map +0 -1
  137. package/dist/components/menu/use-keyboard-controls.js +0 -49
  138. package/dist/components/menu/use-keyboard-controls.js.map +0 -1
  139. package/dist/components/menu/useCheckTextOverflow.d.ts +0 -6
  140. package/dist/components/menu/useCheckTextOverflow.d.ts.map +0 -1
  141. package/dist/components/menu/useCheckTextOverflow.js +0 -20
  142. package/dist/components/menu/useCheckTextOverflow.js.map +0 -1
  143. package/dist/components/tour/Navigation.d.ts +0 -8
  144. package/dist/components/tour/Navigation.d.ts.map +0 -1
  145. package/dist/components/tour/Navigation.js +0 -15
  146. package/dist/components/tour/Navigation.js.map +0 -1
  147. package/dist/components/tour/config.d.ts +0 -3
  148. package/dist/components/tour/config.d.ts.map +0 -1
  149. package/dist/components/tour/config.js +0 -22
  150. package/dist/components/tour/config.js.map +0 -1
  151. package/dist/components/tour/steps/PortalSwitcherStep.d.ts +0 -3
  152. package/dist/components/tour/steps/PortalSwitcherStep.d.ts.map +0 -1
  153. package/dist/components/tour/steps/PortalSwitcherStep.js +0 -30
  154. package/dist/components/tour/steps/PortalSwitcherStep.js.map +0 -1
  155. package/dist/components/tour/utils.d.ts +0 -9
  156. package/dist/components/tour/utils.d.ts.map +0 -1
  157. package/dist/components/tour/utils.js +0 -48
  158. package/dist/components/tour/utils.js.map +0 -1
  159. package/dist/layout-context.d.ts +0 -10
  160. package/dist/layout-context.d.ts.map +0 -1
  161. package/dist/layout-context.js +0 -11
  162. package/dist/layout-context.js.map +0 -1
  163. package/dist/svg/AI.d.ts +0 -6
  164. package/dist/svg/AI.d.ts.map +0 -1
  165. package/dist/svg/AI.js +0 -9
  166. package/dist/svg/AI.js.map +0 -1
  167. package/dist/svg/EDP.d.ts +0 -6
  168. package/dist/svg/EDP.d.ts.map +0 -1
  169. package/dist/svg/EDP.js +0 -5
  170. package/dist/svg/EDP.js.map +0 -1
  171. package/dist/svg/Forbidden.d.ts +0 -6
  172. package/dist/svg/Forbidden.d.ts.map +0 -1
  173. package/dist/svg/Forbidden.js +0 -4
  174. package/dist/svg/Forbidden.js.map +0 -1
  175. package/dist/svg/HUB.d.ts +0 -6
  176. package/dist/svg/HUB.d.ts.map +0 -1
  177. package/dist/svg/HUB.js +0 -5
  178. package/dist/svg/HUB.js.map +0 -1
  179. package/dist/svg/Logo.d.ts +0 -2
  180. package/dist/svg/Logo.d.ts.map +0 -1
  181. package/dist/svg/Logo.js +0 -4
  182. package/dist/svg/Logo.js.map +0 -1
  183. package/dist/svg/NotFound.d.ts +0 -6
  184. package/dist/svg/NotFound.d.ts.map +0 -1
  185. package/dist/svg/NotFound.js +0 -4
  186. package/dist/svg/NotFound.js.map +0 -1
  187. package/dist/svg/ServerError.d.ts +0 -6
  188. package/dist/svg/ServerError.d.ts.map +0 -1
  189. package/dist/svg/ServerError.js +0 -4
  190. package/dist/svg/ServerError.js.map +0 -1
  191. package/dist/svg/Unauthenticated.d.ts +0 -6
  192. package/dist/svg/Unauthenticated.d.ts.map +0 -1
  193. package/dist/svg/Unauthenticated.js +0 -4
  194. package/dist/svg/Unauthenticated.js.map +0 -1
  195. package/src/components/BottomNotification.tsx +0 -0
  196. package/src/components/BottomPanel.tsx +0 -0
  197. package/src/components/SelectionList.tsx +0 -272
  198. package/src/components/error/ErrorFeedback.tsx +0 -114
  199. package/src/components/menu/use-check-text-overflow.tsx +0 -26
  200. package/src/components/menu/use-keyboard-controls.tsx +0 -70
  201. package/src/layout-context.tsx +0 -22
  202. package/src/svg/AI.tsx +0 -37
  203. package/src/svg/EDP.tsx +0 -35
  204. package/src/svg/Forbidden.tsx +0 -22
  205. package/src/svg/HUB.tsx +0 -35
  206. package/src/svg/Logo.tsx +0 -35
  207. package/src/svg/NotFound.tsx +0 -16
  208. package/src/svg/ServerError.tsx +0 -33
  209. package/src/svg/Unauthenticated.tsx +0 -16
@@ -1,31 +1,47 @@
1
- export interface ErrorDescription {
2
- code?: number,
3
- message?: string,
4
- debug?: boolean,
5
- }
1
+ import { ErrorDescription } from '@stack-spot/portal-components/ErrorFeedback'
6
2
 
7
3
  export type DescriptionFn = (error: any) => ErrorDescription
8
4
  export type ErrorHandler = (error: any) => void
9
5
 
6
+ /**
7
+ * Setup how the Error Boundaries deal with errors.
8
+ */
10
9
  export class ErrorManager {
11
10
  private static descriptionFunction: DescriptionFn = error => ({
12
11
  message: error.message || `${error}`,
13
12
  })
14
13
  private static errorHandler: ErrorHandler | undefined
15
14
 
15
+ /**
16
+ * Sets a custom logic for generating error messages and codes. Also setups the environment (debug or not).
17
+ * @param fn a function that, given an error, generates its description.
18
+ */
16
19
  static setDescriptionFunction(fn: DescriptionFn) {
17
20
  this.descriptionFunction = fn
18
21
  }
19
22
 
23
+ /**
24
+ * Sets a custom error handler, i.e. a function to run every time an error is catch by an ErrorBoundary.
25
+ * @param fn a function that receives the error.
26
+ */
20
27
  static setErrorHandler(handler: ErrorHandler) {
21
28
  this.errorHandler = handler
22
29
  }
23
30
 
31
+ /**
32
+ * Uses the error descriptor to describe an error.
33
+ * @param error the error
34
+ * @returns the error description
35
+ */
24
36
  static describe(error: any) {
25
37
  return this.descriptionFunction(error)
26
38
  }
27
39
 
40
+ /**
41
+ * Runs the error handler, if any has been setup.
42
+ * @param error the error
43
+ */
28
44
  static runErrorHandler(error: any) {
29
- return this.errorHandler?.(error)
45
+ this.errorHandler?.(error)
30
46
  }
31
47
  }
@@ -1,16 +1,26 @@
1
+ import { ErrorDescription } from '@stack-spot/portal-components/ErrorFeedback'
1
2
  import { theme } from '@stack-spot/portal-theme'
2
3
  import { Component } from 'react'
3
- import { ErrorDescription, ErrorManager } from './ErrorManager'
4
+ import { ErrorManager } from './ErrorManager'
4
5
 
5
6
  interface State extends ErrorDescription {
6
7
  hasError: boolean,
7
- message?: string,
8
8
  }
9
9
 
10
10
  interface Props {
11
11
  children: React.ReactNode,
12
12
  }
13
13
 
14
+ /**
15
+ * An Error Boundary that doesn't render anything if any of its children throws. If the environment is not production, a small error icon
16
+ * is rendered instead of nothing.
17
+ *
18
+ * To customize how the error is logged, setup an error descriptor for the ErrorManager class. If you're using the component `Layout` or
19
+ * `RawLayout`, you can use the property `errorDescriptor`.
20
+ *
21
+ * To run an error handler every time an error is catch by this boundary, setup an error handler for the ErrorManager class. If you're
22
+ * using the component `Layout` or `RawLayout`, you can use the property `onError`.
23
+ */
14
24
  export class SilentErrorBoundary extends Component<Props, State> {
15
25
  constructor(props: Props) {
16
26
  super(props)
@@ -1,17 +1,15 @@
1
- /* eslint-disable react-refresh/only-export-components */
2
- /* eslint-disable @typescript-eslint/no-unused-vars */
3
1
  import { Flex, IconBox, Text } from '@citric/core'
4
2
  import { ArrowLeft, ChevronDown } from '@citric/icons'
5
3
  import { LoadingCircular } from '@citric/ui'
4
+ import { useCheckTextOverflow } from '@stack-spot/portal-components'
5
+ import { useAnchorTag } from '@stack-spot/portal-components/anchor'
6
6
  import { listToClass, theme } from '@stack-spot/portal-theme'
7
7
  import { useMemo, useState } from 'react'
8
8
  import { styled } from 'styled-components'
9
- import { useAnchorTag } from '../../layout-context'
10
9
  import { hideOverlayImmediately } from './MenuSections'
11
10
  import { PageSelector } from './PageSelector'
12
11
  import { MENU_CONTENT_ITEM_PADDING as ITEM_PADDING, MENU_CONTENT_PADDING as PADDING } from './constants'
13
12
  import { ItemGroup, MenuAction, MenuItem, MenuSectionContent } from './types'
14
- import { useCheckTextOverflow } from './use-check-text-overflow'
15
13
 
16
14
  const BackLink = styled.a`
17
15
  display: flex;
@@ -22,6 +20,9 @@ const BackLink = styled.a`
22
20
  gap: 6px;
23
21
  `
24
22
 
23
+ /**
24
+ * The list (<ul>) used for grouping items in a menu.
25
+ */
25
26
  export const MenuGroup = styled.ul`
26
27
  padding: 0 0 0 16px;
27
28
  display: flex;
@@ -33,30 +34,18 @@ export const MenuGroup = styled.ul`
33
34
  padding: 0;
34
35
  }
35
36
 
36
- .item-row-group > a {
37
- padding: 0 16px;
38
- margin: 0;
39
- border-radius: 0;
40
- }
41
-
42
- .item-row-group > a::before {
43
- content: '';
44
- position: absolute;
45
- top: 0;
46
- left: 0;
47
- right: 0;
48
- bottom: 0;
49
- background-color: var(--light-300);
50
- opacity: 0.24;
51
- border-radius: inherit;
52
- }
53
-
54
37
  .item-row {
55
38
  display: flex;
56
39
  flex-direction: row;
57
40
  gap: 8px;
58
41
  align-items: center;
59
42
 
43
+ &.root {
44
+ padding: 0 16px;
45
+ margin-top: 16px;
46
+ border-radius: 0;
47
+ }
48
+
60
49
  .label {
61
50
  flex: 1;
62
51
  &.hidden, &.ellipsis {
@@ -68,10 +57,6 @@ export const MenuGroup = styled.ul`
68
57
  }
69
58
  }
70
59
  }
71
-
72
- .item-row-group {
73
- margin-top: 16px;
74
- }
75
60
 
76
61
  li a {
77
62
  position: relative;
@@ -129,7 +114,7 @@ export const MenuGroup = styled.ul`
129
114
  &.open {
130
115
  visibility: visible;
131
116
  transition: unset;
132
- & > li > a, & > li > .item-row-group > a {
117
+ & > li > a {
133
118
  height: 40px;
134
119
  }
135
120
  }
@@ -158,12 +143,19 @@ export const MenuGroup = styled.ul`
158
143
  }
159
144
  `
160
145
 
146
+ /**
147
+ * The header (<header>) for a group of items in a menu. Contains the title of the group.
148
+ */
161
149
  export const Title = styled.header`
162
150
  display: flex;
163
151
  flex-direction: column;
164
152
  margin: ${PADDING}px 0 24px ${PADDING}px;
165
153
  `
166
154
 
155
+ /**
156
+ * A menu item that performs an action.
157
+ * @param props the props for the component {@link MenuAction}.
158
+ */
167
159
  export const ActionItem = ({ label, onClick, href, active, icon, badge, overflow = 'wrap' }: MenuAction) => {
168
160
  const Link = useAnchorTag()
169
161
  const { ref, overflow: textOverflow } = useCheckTextOverflow()
@@ -189,6 +181,11 @@ export const ActionItem = ({ label, onClick, href, active, icon, badge, overflow
189
181
  )
190
182
  }
191
183
 
184
+ /**
185
+ * A menu item that is actually a subgroup and can be collapsed/expanded.
186
+ * @param props the props for the component {@link ItemGroup} & { root: boolean }. Pass root=true to style this group as a root group.
187
+ * Root groups have slightly different visuals.
188
+ */
192
189
  const CollapsibleGroupItem = ({ label, open: initiallyOpened, children, icon, badge, root, overflow = 'wrap' }:
193
190
  ItemGroup & { root?: boolean }) => {
194
191
  const [open, setOpen] = useState(initiallyOpened ?? children?.some(c => 'active' in c && c.active) ?? false)
@@ -200,7 +197,7 @@ const CollapsibleGroupItem = ({ label, open: initiallyOpened, children, icon, ba
200
197
  <a
201
198
  onClick={() => setOpen(!open)}
202
199
  onKeyDown={e => e.key === 'Enter' && setOpen(!open)}
203
- className="item-row"
200
+ className={listToClass(['item-row', root && 'root'])}
204
201
  tabIndex={0}
205
202
  aria-controls={id}
206
203
  aria-expanded={open}
@@ -223,38 +220,22 @@ const CollapsibleGroupItem = ({ label, open: initiallyOpened, children, icon, ba
223
220
  )
224
221
  }
225
222
 
226
- const RootGroupItem = (props: ItemGroup) => {
227
- const items = useMemo(() => props.children?.filter(i => !i.hidden).map(renderOption), [props.children])
228
-
229
- return <>
230
- {items.length ? <div className="item-row-group">
231
- <CollapsibleGroupItem {...props} open={true} root={true} />
232
- </div> :
233
- <>
234
- <div className="item-row">
235
- {props.icon}
236
- <Text appearance="overheader2" colorScheme="light.700" className={`group-title label ${props.overflow}`}>{props.label}</Text>
237
- {props.badge}
238
- </div>
239
- <MenuGroup className="open no-indentation">{items}</MenuGroup>
240
- </>
241
- }
242
- </>
243
- }
244
-
245
- const GroupItem = ({ root, ...item }: ItemGroup & { root?: boolean }) => (
246
- root ? <RootGroupItem {...item} /> : <CollapsibleGroupItem {...item} />
247
- )
248
-
249
223
  function renderOption({ root, ...option }: MenuItem & { root?: boolean }) {
250
224
  const labelText = typeof option.label === 'string' ? option.label : option.label.id
251
225
  return (
252
226
  <li key={labelText} role="menuitem" aria-selected={'children' in option ? undefined : option.active}>
253
- {'children' in option ? <GroupItem root={root} {...option} /> : <ActionItem {...option} />}
227
+ {'children' in option ? <CollapsibleGroupItem {...option} root={root} open={option.open ?? root} /> : <ActionItem {...option} />}
254
228
  </li >
255
229
  )
256
230
  }
257
231
 
232
+ /**
233
+ * Renders a menu-content interface.
234
+ *
235
+ * Considering the Stackspot UI, this is the "menu content", not the "menu sections", i.e. it's the second menu from left to right, the
236
+ * one that changes according to section selected.
237
+ * @param props the props for the component {@link MenuSectionContent}.
238
+ */
258
239
  export const MenuContent = ({ pageSelector, goBack, title, subtitle, afterTitle, options = [], loading, error }: MenuSectionContent) => {
259
240
  const items = useMemo(() => options.filter(o => !o.hidden).map(o => renderOption({ ...o, root: true })), [options])
260
241
 
@@ -1,32 +1,54 @@
1
1
  import { Flex, IconBox, Text } from '@citric/core'
2
2
  import { ChevronRight, Cog, Collapse, Expand } from '@citric/icons'
3
+ import { useKeyboardControls } from '@stack-spot/portal-components'
4
+ import { useAnchorTag } from '@stack-spot/portal-components/anchor'
3
5
  import { Dictionary, interpolate, useTranslate } from '@stack-spot/portal-translate'
4
6
  import { useCallback, useMemo, useState } from 'react'
5
7
  import { elementIds, getLayoutElements } from '../../elements'
6
- import { useAnchorTag } from '../../layout-context'
7
8
  import { MenuContent } from './MenuContent'
8
9
  import { MenuProps, MenuSection } from './types'
9
- import { useKeyboardControls } from './use-keyboard-controls'
10
10
 
11
+ /**
12
+ * Amount of time to wait before hiding the menu overlay once the mouse leaves its area.
13
+ */
11
14
  const HIDE_OVERLAY_DELAY_MS = 400
12
15
  const MENU_OVERLAY_ID = 'menuContentOverlay'
13
16
 
17
+ /**
18
+ * Pointer to the latest "hideOverlay" task. This allows the operation to be cancelled.
19
+ */
14
20
  let hideOverlayTask: number | undefined
15
21
 
16
- // fixme: this should definitely not be handled like this...
22
+ /**
23
+ * Accessibility. Makes the menu overlay accessible through the keyboard.
24
+ */
17
25
  let attachKeyboardListenersForOverlay: () => void
26
+ /**
27
+ * Accessibility. Makes the menu overlay invisible to the keyboard.
28
+ */
18
29
  let detachKeyboardListenersForOverlay: () => void
19
30
 
31
+ /**
32
+ * Hides the menu overlay after HIDE_OVERLAY_DELAY_MS. This operation may be canceled.
33
+ *
34
+ * This gives the user some time to move the mouse outside the overlay for while before it disappears. If the user moves the mouse out of
35
+ * the overlay and, before HIDE_OVERLAY_DELAY_MS, moves the mouse back, we want the overlay to keep showing.
36
+ */
20
37
  function hideOverlay() {
21
38
  if (hideOverlayTask !== undefined) return
22
39
  hideOverlayTask = window.setTimeout(hideOverlayImmediately, HIDE_OVERLAY_DELAY_MS)
23
40
  }
24
41
 
42
+ /**
43
+ * Accessibility. Returns the accessibility button of the section with the preview (overlay) currently active.
44
+ */
25
45
  function getAccessibilityButtonOfSectionWithActiveOverlay(): HTMLElement | null | undefined {
26
46
  return document.getElementById(elementIds.menuSections)?.querySelector('button[aria-expanded="true"]')
27
47
  }
28
48
 
29
- // eslint-disable-next-line react-refresh/only-export-components
49
+ /**
50
+ * Hides the menu overlay without waiting for anything. This is not cancellable.
51
+ */
30
52
  export function hideOverlayImmediately() {
31
53
  detachKeyboardListenersForOverlay?.()
32
54
  const overlay = document.getElementById(MENU_OVERLAY_ID)
@@ -36,12 +58,18 @@ export function hideOverlayImmediately() {
36
58
  getAccessibilityButtonOfSectionWithActiveOverlay()?.setAttribute('aria-expanded', 'false')
37
59
  }
38
60
 
61
+ /**
62
+ * If `hideOverlay` was called and not fulfilled yet, this cancels the task, preventing the overlay from closing.
63
+ */
39
64
  function cancelHideOverlayTask() {
40
65
  if (hideOverlayTask === undefined) return
41
66
  clearTimeout(hideOverlayTask)
42
67
  hideOverlayTask = undefined
43
68
  }
44
69
 
70
+ /**
71
+ * Shows the menu overlay.
72
+ */
45
73
  function showOverlay() {
46
74
  cancelHideOverlayTask()
47
75
  const overlay = document.getElementById(MENU_OVERLAY_ID)
@@ -51,10 +79,23 @@ function showOverlay() {
51
79
  attachKeyboardListenersForOverlay?.()
52
80
  }
53
81
 
82
+ /**
83
+ * Checks if the menu content (2nd menu list from left to right) is visible or not.
84
+ * The menu content is visible if:
85
+ * 1. The menu is not compact (the button at the end of the section list compacts/expands the menu);
86
+ * 2. If the current section has any menu content.
87
+ */
54
88
  function isMenuContentVisible() {
55
89
  return !!document.getElementById('layout')?.classList?.contains('menu-content-visible')
56
90
  }
57
91
 
92
+ /**
93
+ * A section in the the menu-sections.
94
+ *
95
+ * A section in the menu is responsible for rendering the section icon, label, accessibility button and controlling the menu overlay.
96
+ * @param props React props for the component {@link MenuSection} & { id, setCurrentOverlay }. Id identifies the current section and
97
+ * setCurrentOverlay controls the overlay (preview) content.
98
+ */
58
99
  const Section = ({
59
100
  icon,
60
101
  label,
@@ -64,7 +105,6 @@ const Section = ({
64
105
  active,
65
106
  content,
66
107
  customContent,
67
- onOpen,
68
108
  setCurrentOverlay,
69
109
  id,
70
110
  hasContent,
@@ -88,7 +128,6 @@ const Section = ({
88
128
 
89
129
  function prepareShowOverlay(event: React.MouseEvent<HTMLAnchorElement, MouseEvent> | React.KeyboardEvent<any>) {
90
130
  if (!shouldShowOverlay()) return
91
- onOpen?.()
92
131
  const anchorElement = event.target as HTMLElement
93
132
  const accessibilityButton = anchorElement?.parentElement?.querySelector('button') as HTMLElement
94
133
  accessibilityButton?.setAttribute('aria-expanded', 'true')
@@ -104,58 +143,69 @@ const Section = ({
104
143
  const labelText = typeof label === 'string' ? label : label.id
105
144
 
106
145
  return (
107
- <>
108
- <li
109
- role="menuitem"
110
- key={labelText}
146
+ <li
147
+ role="menuitem"
148
+ key={labelText}
149
+ title={labelText}
150
+ className={`section-submenu ${className || ''} ${active ? 'active' : ''}`}
151
+ aria-selected={active}>
152
+ <Link
153
+ href={href}
154
+ target={target}
155
+ onClick={click}
156
+ onMouseEnter={prepareShowOverlay}
157
+ onMouseLeave={() => shouldShowOverlay() && hideOverlay()}
111
158
  title={labelText}
112
- className={`section-submenu ${className || ''} ${active ? 'active' : ''}`}
113
- aria-selected={active}>
114
- <Link
115
- href={href}
116
- target={target}
117
- onClick={click}
118
- onMouseEnter={prepareShowOverlay}
119
- onMouseLeave={() => shouldShowOverlay() && hideOverlay()}
120
- title={labelText}
121
- aria-label={labelText}
122
- onKeyDown={onClick ? e => e.key === 'Enter' && onClick() : undefined}
123
- {...(active ? { 'aria-current': 'page' } : undefined)}
124
- {...(!href ? { 'tabIndex': 0 } : undefined)}
125
- >
126
- <Flex alignItems="center" justifyContent="center" px={5}>
127
- <IconBox>{icon}</IconBox>
128
- {typeof label === 'string' ? <Text appearance="microtext1" className="section-label" ml={3}>{label}</Text> : label.element}
129
- </Flex>
130
- </Link>
131
- {shouldShowOverlay() &&
132
- <IconBox size="sm" className="section-submenu-icon"
133
- as="button"
134
- aria-label={interpolate(t.menuOptions, label)}
135
- aria-controls={MENU_OVERLAY_ID}
136
- aria-expanded={false}
137
- onKeyDown={(event) => {
138
- if (event.key === 'Enter') {
139
- prepareShowOverlay(event)
140
- }
141
- }}>
142
- <ChevronRight />
143
- </IconBox>
144
- }
145
- </li>
146
- </>
159
+ aria-label={labelText}
160
+ onKeyDown={onClick ? e => e.key === 'Enter' && onClick() : undefined}
161
+ {...(active ? { 'aria-current': 'page' } : undefined)}
162
+ {...(!href ? { 'tabIndex': 0 } : undefined)}
163
+ >
164
+ <Flex alignItems="center" justifyContent="center" px={5}>
165
+ <IconBox>{icon}</IconBox>
166
+ {typeof label === 'string' ? <Text appearance="microtext1" className="section-label" ml={3}>{label}</Text> : label.element}
167
+ </Flex>
168
+ </Link>
169
+ {shouldShowOverlay() &&
170
+ <IconBox size="sm" className="section-submenu-icon"
171
+ as="button"
172
+ aria-label={interpolate(t.menuOptions, label)}
173
+ aria-controls={MENU_OVERLAY_ID}
174
+ aria-expanded={false}
175
+ onKeyDown={(event) => {
176
+ if (event.key === 'Enter') {
177
+ prepareShowOverlay(event)
178
+ }
179
+ }}>
180
+ <ChevronRight />
181
+ </IconBox>
182
+ }
183
+ </li>
147
184
  )
148
185
  }
149
186
 
187
+ /**
188
+ * Renders the overlay content.
189
+ * @param props the content of the overlay, can be either customized (react component), a config object or a function that creates the
190
+ * config object.
191
+ * @returns the content
192
+ */
150
193
  const OverlayRenderer = ({ content, customContent }: Pick<MenuSection, 'content' | 'customContent'>) => {
151
194
  if (customContent) {
152
- return <div id="custom-selectable-item"> {customContent} </div>
195
+ return <div id="custom-selectable-item">{customContent}</div>
153
196
  }
154
197
 
155
198
  const data = typeof content === 'function' ? content() : content
156
199
  return <MenuContent {...data} />
157
200
  }
158
201
 
202
+ /**
203
+ * Renders a menu-sections interface.
204
+ *
205
+ * Considering the Stackspot UI, this is the "menu sections", not the "menu content", i.e. it's the first menu from left to right, the
206
+ * one with the icons and section names: the main menu.
207
+ * @param props the props for the component {@link MenuProps}.
208
+ */
159
209
  export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
160
210
  const Link = useAnchorTag()
161
211
  const t = useTranslate(dictionary)
@@ -199,7 +249,8 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
199
249
  querySelectors: 'li a.action, #custom-selectable-item button, #custom-selectable-item input',
200
250
  })
201
251
 
202
- // fixme: this should definitely not be handled like this...
252
+ // this only works because we have a single section menu in the site. This workaround was created since the keyboard controls were
253
+ // transformed into a hook. This is not ideal and it would be a good idea to rewrite this code without the need for this.
203
254
  attachKeyboardListenersForOverlay = attachKeyboardListeners
204
255
  detachKeyboardListenersForOverlay = detachKeyboardListeners
205
256
 
@@ -246,7 +297,6 @@ export const MenuSections = ({ sections = [], ...props }: MenuProps) => {
246
297
 
247
298
  <div id={MENU_OVERLAY_ID} onMouseEnter={showOverlay} onMouseLeave={hideOverlay} ref={overlayRef}>
248
299
  {renderMenuOverlay()}
249
- <div className="arrow"></div>
250
300
  </div>
251
301
  </>
252
302
  )
@@ -264,7 +314,7 @@ const dictionary = {
264
314
  toggle: 'Visualizar ou esconder o menu',
265
315
  menuOptions: 'Visualizar opções do menu $0',
266
316
  settings: 'Configurações',
267
- settingsIcon: 'Icone de configurações',
317
+ settingsIcon: 'Ícone de configurações',
268
318
  hide: 'Esconder',
269
319
  },
270
320
  } satisfies Dictionary
@@ -1,15 +1,15 @@
1
1
  import { IconBox, Text } from '@citric/core'
2
2
  import { ArrowRight, Select } from '@citric/icons'
3
3
  import { LoadingCircular } from '@citric/ui'
4
+ import { useCheckTextOverflow } from '@stack-spot/portal-components'
5
+ import { ListAction, SelectionList } from '@stack-spot/portal-components/SelectionList'
6
+ import { useAnchorTag } from '@stack-spot/portal-components/anchor'
4
7
  import { theme } from '@stack-spot/portal-theme'
5
8
  import { Dictionary, interpolate, useTranslate } from '@stack-spot/portal-translate'
6
9
  import { KeyboardEvent, useMemo, useRef, useState } from 'react'
7
10
  import { styled } from 'styled-components'
8
- import { useAnchorTag } from '../../layout-context'
9
- import { ListAction, SelectionList } from '../SelectionList'
10
11
  import { MENU_CONTENT_PADDING as PADDING } from './constants'
11
12
  import { Selector } from './types'
12
- import { useCheckTextOverflow } from './use-check-text-overflow'
13
13
 
14
14
  const SelectorBox = styled.div`
15
15
  position: relative;
@@ -81,6 +81,11 @@ const SelectorBox = styled.div`
81
81
  }
82
82
  `
83
83
 
84
+ /**
85
+ * A selector component to render inside a menu-content. Allows the user to select another page.
86
+ *
87
+ * @param props the React props for the component {@link Selector}.
88
+ */
84
89
  export const PageSelector = ({ options, value, button, loading, title }: Selector) => {
85
90
  const Link = useAnchorTag()
86
91
  const t = useTranslate(dictionary)