@stack-spot/portal-components 2.27.0 → 2.27.2

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 (250) hide show
  1. package/CHANGELOG.md +635 -621
  2. package/dist/components/AnimatedHeight.d.ts +1 -1
  3. package/dist/components/AnimatedHeight.js +26 -26
  4. package/dist/components/AsyncContent.d.ts +1 -1
  5. package/dist/components/AsyncContent.js +1 -1
  6. package/dist/components/BannerWarning.d.ts +1 -1
  7. package/dist/components/BannerWarning.js +1 -1
  8. package/dist/components/Breadcrumb/index.d.ts +2 -2
  9. package/dist/components/Breadcrumb/index.js +1 -1
  10. package/dist/components/Breadcrumb/styled.js +31 -31
  11. package/dist/components/ButtonLoading.d.ts +1 -1
  12. package/dist/components/ButtonLoading.js +1 -1
  13. package/dist/components/ChatBot.d.ts +1 -1
  14. package/dist/components/ChatBot.js +1 -1
  15. package/dist/components/ContentValidateFilter.d.ts +1 -1
  16. package/dist/components/ContentValidateFilter.js +1 -1
  17. package/dist/components/FadingOverflow.d.ts +1 -1
  18. package/dist/components/FadingOverflow.js +69 -69
  19. package/dist/components/FileTreeView/More.d.ts +1 -1
  20. package/dist/components/FileTreeView/More.js +1 -1
  21. package/dist/components/FileTreeView/index.d.ts +1 -1
  22. package/dist/components/FileTreeView/index.js +1 -1
  23. package/dist/components/InfiniteScroll.d.ts +1 -1
  24. package/dist/components/InfiniteScroll.js +1 -1
  25. package/dist/components/InfoMaintenanceBanner.d.ts +1 -1
  26. package/dist/components/InfoMaintenanceBanner.js +2 -2
  27. package/dist/components/LazyMarkdown/BlockquoteMd.d.ts +1 -1
  28. package/dist/components/LazyMarkdown/BlockquoteMd.js +1 -1
  29. package/dist/components/LazyMarkdown/CodeViewer.d.ts +1 -1
  30. package/dist/components/LazyMarkdown/CodeViewer.js +76 -76
  31. package/dist/components/LazyMarkdown/Markdown.d.ts +1 -1
  32. package/dist/components/LazyMarkdown/Markdown.js +1 -1
  33. package/dist/components/LazyMarkdown/MarkdownButton.d.ts +1 -1
  34. package/dist/components/LazyMarkdown/MarkdownButton.js +1 -1
  35. package/dist/components/LazyMarkdown/Video.d.ts +1 -1
  36. package/dist/components/LazyMarkdown/Video.js +1 -1
  37. package/dist/components/LazyMarkdown/index.d.ts +1 -1
  38. package/dist/components/LazyMarkdown/index.js +1 -1
  39. package/dist/components/Placeholder.d.ts +7 -3
  40. package/dist/components/Placeholder.d.ts.map +1 -1
  41. package/dist/components/Placeholder.js +3 -3
  42. package/dist/components/Placeholder.js.map +1 -1
  43. package/dist/components/ScrollView.js +16 -16
  44. package/dist/components/Select/BadgeItem.d.ts +1 -1
  45. package/dist/components/Select/BadgeItem.js +1 -1
  46. package/dist/components/Select/ClearInput.d.ts +1 -1
  47. package/dist/components/Select/ClearInput.js +1 -1
  48. package/dist/components/Select/CloseItem.d.ts +1 -1
  49. package/dist/components/Select/CloseItem.js +1 -1
  50. package/dist/components/Select/CreatableSelect.js +1 -1
  51. package/dist/components/Select/CustomMenu.d.ts +1 -1
  52. package/dist/components/Select/CustomMenu.js +1 -1
  53. package/dist/components/Select/LabelItem.d.ts +1 -1
  54. package/dist/components/Select/LabelItem.js +1 -1
  55. package/dist/components/Select/MultiValue.d.ts +1 -1
  56. package/dist/components/Select/MultiValue.js +1 -1
  57. package/dist/components/Select/SelectInfiniteScroll.d.ts +1 -1
  58. package/dist/components/Select/SelectInfiniteScroll.js +1 -1
  59. package/dist/components/Select/SelectSearch.d.ts +1 -1
  60. package/dist/components/Select/SelectSearch.js +1 -1
  61. package/dist/components/SelectionList.d.ts +1 -1
  62. package/dist/components/SelectionList.js +61 -61
  63. package/dist/components/StatusCircle.d.ts +1 -1
  64. package/dist/components/StatusCircle.js +6 -6
  65. package/dist/components/Stepper/Navigation.js +4 -4
  66. package/dist/components/Stepper/Step.js +3 -3
  67. package/dist/components/Stepper/Stepper.js +6 -6
  68. package/dist/components/Stepper/headers.js +22 -22
  69. package/dist/components/Table/HeaderItem.js +1 -1
  70. package/dist/components/Table/SettingsVerticalMenu.d.ts +1 -1
  71. package/dist/components/Table/SettingsVerticalMenu.js +1 -1
  72. package/dist/components/Table/StyledLinkTable.d.ts +1 -1
  73. package/dist/components/Table/StyledLinkTable.js +5 -5
  74. package/dist/components/Table/TableData.d.ts +1 -1
  75. package/dist/components/Table/TableData.js +25 -25
  76. package/dist/components/TimelineSection.d.ts +1 -1
  77. package/dist/components/TimelineSection.js +14 -14
  78. package/dist/components/error/ErrorFeedback.d.ts +1 -1
  79. package/dist/components/error/ErrorFeedback.js +35 -35
  80. package/dist/components/error/NotFound.d.ts +1 -1
  81. package/dist/components/error/NotFound.js +1 -1
  82. package/dist/components/error/UnderMaintenance.d.ts +1 -1
  83. package/dist/components/error/UnderMaintenance.js +1 -1
  84. package/dist/components/form/Form/Form.d.ts +1 -1
  85. package/dist/components/form/Form/Form.js +1 -1
  86. package/dist/components/form/Form/FormGroup.d.ts +2 -2
  87. package/dist/components/form/Form/FormGroup.js +1 -1
  88. package/dist/components/form/SearchInput.d.ts +1 -1
  89. package/dist/components/form/SearchInput.js +1 -1
  90. package/dist/components/form/Select/CustomSelect.d.ts +1 -1
  91. package/dist/components/form/Select/CustomSelect.js +1 -1
  92. package/dist/components/form/Select/DetailedSelect.d.ts +1 -1
  93. package/dist/components/form/Select/DetailedSelect.js +1 -1
  94. package/dist/components/form/Select/Select.d.ts +1 -1
  95. package/dist/components/form/Select/Select.js +1 -1
  96. package/dist/components/form/Select/styled.js +161 -161
  97. package/dist/components/form/Select/utils.js +1 -1
  98. package/dist/components/notification/NotificationComponent.d.ts +1 -1
  99. package/dist/components/notification/NotificationComponent.js +54 -54
  100. package/dist/components/notification/NotificationItem.d.ts +1 -1
  101. package/dist/components/notification/NotificationItem.d.ts.map +1 -1
  102. package/dist/components/notification/NotificationItem.js +11 -5
  103. package/dist/components/notification/NotificationItem.js.map +1 -1
  104. package/dist/components/notification/NotificationList.d.ts +1 -1
  105. package/dist/components/notification/NotificationList.d.ts.map +1 -1
  106. package/dist/components/notification/NotificationList.js +44 -44
  107. package/dist/components/notification/NotificationList.js.map +1 -1
  108. package/dist/components/notification/NotificationPlaceholder.d.ts +1 -1
  109. package/dist/components/notification/NotificationPlaceholder.d.ts.map +1 -1
  110. package/dist/components/notification/NotificationPlaceholder.js +2 -2
  111. package/dist/components/notification/NotificationPlaceholder.js.map +1 -1
  112. package/dist/containers/NotificationsPage.d.ts +1 -1
  113. package/dist/containers/NotificationsPage.d.ts.map +1 -1
  114. package/dist/containers/NotificationsPage.js +24 -11
  115. package/dist/containers/NotificationsPage.js.map +1 -1
  116. package/dist/context/anchor.d.ts +1 -1
  117. package/dist/context/anchor.js +1 -1
  118. package/dist/context/loading.d.ts +1 -1
  119. package/dist/context/loading.js +1 -1
  120. package/dist/context/notification/context.d.ts +1 -1
  121. package/dist/context/notification/context.js +1 -1
  122. package/dist/context/notification/types.d.ts +1 -0
  123. package/dist/context/notification/types.d.ts.map +1 -1
  124. package/dist/hooks/date.js +1 -1
  125. package/dist/hooks/service-now.js +28 -28
  126. package/dist/svg/AI.d.ts +1 -1
  127. package/dist/svg/AI.js +1 -1
  128. package/dist/svg/CS.d.ts +1 -1
  129. package/dist/svg/CS.js +1 -1
  130. package/dist/svg/EDP.d.ts +1 -1
  131. package/dist/svg/EDP.js +1 -1
  132. package/dist/svg/Forbidden.d.ts +1 -1
  133. package/dist/svg/Forbidden.js +1 -1
  134. package/dist/svg/GenericPlaceholder.d.ts +4 -2
  135. package/dist/svg/GenericPlaceholder.d.ts.map +1 -1
  136. package/dist/svg/GenericPlaceholder.js +2 -2
  137. package/dist/svg/GenericPlaceholder.js.map +1 -1
  138. package/dist/svg/HUB.d.ts +1 -1
  139. package/dist/svg/HUB.js +1 -1
  140. package/dist/svg/Logo.d.ts +1 -1
  141. package/dist/svg/Logo.js +1 -1
  142. package/dist/svg/MiniLogo.d.ts +1 -1
  143. package/dist/svg/MiniLogo.js +1 -1
  144. package/dist/svg/NotFound.d.ts +1 -1
  145. package/dist/svg/NotFound.js +1 -1
  146. package/dist/svg/ServerError.d.ts +1 -1
  147. package/dist/svg/ServerError.js +1 -1
  148. package/dist/svg/Unauthenticated.d.ts +1 -1
  149. package/dist/svg/Unauthenticated.js +1 -1
  150. package/package.json +6 -6
  151. package/readme.md +66 -66
  152. package/src/components/AnimatedHeight.tsx +174 -174
  153. package/src/components/AsyncContent.tsx +78 -78
  154. package/src/components/BannerWarning.tsx +91 -91
  155. package/src/components/Breadcrumb/index.tsx +76 -76
  156. package/src/components/Breadcrumb/styled.ts +37 -37
  157. package/src/components/ButtonLoading.tsx +29 -29
  158. package/src/components/ChatBot.tsx +82 -82
  159. package/src/components/ContentValidateFilter.tsx +15 -15
  160. package/src/components/FadingOverflow.tsx +265 -265
  161. package/src/components/FileTreeView/More.tsx +114 -114
  162. package/src/components/FileTreeView/index.tsx +186 -186
  163. package/src/components/InfiniteScroll.tsx +24 -24
  164. package/src/components/InfoMaintenanceBanner.tsx +29 -29
  165. package/src/components/LazyMarkdown/BlockquoteMd.tsx +107 -107
  166. package/src/components/LazyMarkdown/CodeViewer.tsx +161 -161
  167. package/src/components/LazyMarkdown/Markdown.tsx +122 -122
  168. package/src/components/LazyMarkdown/MarkdownButton.tsx +24 -24
  169. package/src/components/LazyMarkdown/Video.tsx +13 -13
  170. package/src/components/LazyMarkdown/index.tsx +21 -21
  171. package/src/components/Placeholder.tsx +123 -118
  172. package/src/components/ScrollView.tsx +57 -57
  173. package/src/components/Select/BadgeItem.tsx +58 -58
  174. package/src/components/Select/ClearInput.tsx +24 -24
  175. package/src/components/Select/CloseItem.tsx +38 -38
  176. package/src/components/Select/CreatableSelect.tsx +155 -155
  177. package/src/components/Select/CustomMenu.tsx +16 -16
  178. package/src/components/Select/LabelItem.tsx +8 -8
  179. package/src/components/Select/MultiValue.tsx +49 -49
  180. package/src/components/Select/SelectInfiniteScroll.tsx +82 -82
  181. package/src/components/Select/SelectSearch.tsx +195 -195
  182. package/src/components/Select/index.tsx +7 -7
  183. package/src/components/Select/types.ts +8 -8
  184. package/src/components/SelectionList.tsx +427 -427
  185. package/src/components/StatusCircle.tsx +67 -67
  186. package/src/components/Stepper/Navigation.tsx +97 -97
  187. package/src/components/Stepper/Step.tsx +30 -30
  188. package/src/components/Stepper/Stepper.tsx +113 -113
  189. package/src/components/Stepper/headers.tsx +64 -64
  190. package/src/components/Stepper/index.ts +3 -3
  191. package/src/components/Table/HeaderItem.tsx +52 -52
  192. package/src/components/Table/SettingsVerticalMenu.tsx +50 -50
  193. package/src/components/Table/StyledLinkTable.tsx +22 -22
  194. package/src/components/Table/TableData.tsx +251 -251
  195. package/src/components/Table/index.tsx +2 -2
  196. package/src/components/TimelineSection.tsx +66 -66
  197. package/src/components/error/ErrorFeedback.tsx +217 -217
  198. package/src/components/error/NotFound.tsx +24 -24
  199. package/src/components/error/UnderMaintenance.tsx +30 -30
  200. package/src/components/error/index.ts +4 -4
  201. package/src/components/form/Form/Form.tsx +101 -101
  202. package/src/components/form/Form/FormGroup.tsx +221 -221
  203. package/src/components/form/Form/index.ts +2 -2
  204. package/src/components/form/SearchInput.tsx +69 -69
  205. package/src/components/form/Select/CustomSelect.tsx +232 -232
  206. package/src/components/form/Select/DetailedSelect.tsx +85 -85
  207. package/src/components/form/Select/Select.tsx +67 -67
  208. package/src/components/form/Select/index.ts +4 -4
  209. package/src/components/form/Select/styled.ts +165 -165
  210. package/src/components/form/Select/types.ts +112 -112
  211. package/src/components/form/Select/utils.tsx +28 -28
  212. package/src/components/notification/NotificationComponent.tsx +340 -340
  213. package/src/components/notification/NotificationItem.tsx +345 -336
  214. package/src/components/notification/NotificationList.tsx +179 -178
  215. package/src/components/notification/NotificationPlaceholder.tsx +44 -43
  216. package/src/components/notification/types.ts +72 -72
  217. package/src/containers/NotificationsPage.tsx +119 -98
  218. package/src/context/anchor.tsx +37 -37
  219. package/src/context/loading.tsx +36 -36
  220. package/src/context/notification/LazyNotificationList.ts +103 -103
  221. package/src/context/notification/NotificationController.ts +104 -104
  222. package/src/context/notification/context.tsx +23 -23
  223. package/src/context/notification/hooks.ts +98 -98
  224. package/src/context/notification/types.ts +66 -65
  225. package/src/hooks/date.ts +31 -31
  226. package/src/hooks/keyboard.tsx +128 -128
  227. package/src/hooks/manual-render.tsx +10 -10
  228. package/src/hooks/service-now.tsx +233 -233
  229. package/src/hooks/text.tsx +30 -30
  230. package/src/hooks/title.tsx +28 -28
  231. package/src/hooks/use-effect-once.tsx +43 -43
  232. package/src/index.ts +19 -19
  233. package/src/notifications.ts +11 -11
  234. package/src/svg/AI.tsx +41 -41
  235. package/src/svg/CS.tsx +48 -48
  236. package/src/svg/EDP.tsx +31 -31
  237. package/src/svg/Forbidden.tsx +22 -22
  238. package/src/svg/GenericPlaceholder.tsx +20 -20
  239. package/src/svg/HUB.tsx +48 -48
  240. package/src/svg/Logo.tsx +16 -16
  241. package/src/svg/MiniLogo.tsx +12 -12
  242. package/src/svg/NotFound.tsx +16 -16
  243. package/src/svg/ServerError.tsx +33 -33
  244. package/src/svg/Unauthenticated.tsx +16 -16
  245. package/src/svg/index.ts +11 -11
  246. package/src/utils/accessibility.ts +135 -135
  247. package/src/utils/cookie.ts +73 -73
  248. package/src/utils/promise.ts +5 -5
  249. package/src/utils/read-file.ts +16 -16
  250. package/tsconfig.json +10 -10
@@ -1,427 +1,427 @@
1
- import { Flex, IconBox, Text } from '@citric/core'
2
- import { ArrowLeft, Check, ChevronRight } from '@citric/icons'
3
- import { IconButton } from '@citric/ui'
4
- import { WithStyle, listToClass, theme } from '@stack-spot/portal-theme'
5
- import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
6
- import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
7
- import { styled } from 'styled-components'
8
- import { AnchorComponent, useAnchorTag } from '../context/anchor'
9
- import { useKeyboardControls } from '../hooks/keyboard'
10
-
11
- interface CustomLabel {
12
- /**
13
- * A unique identifier for this label. This is also used to read this label to screen readers.
14
- */
15
- id: string,
16
- /**
17
- * A custom label that can be made up of any React component.
18
- */
19
- element: React.ReactNode,
20
- }
21
-
22
- export interface Action {
23
- /**
24
- * The label of the action.
25
- */
26
- label: string | CustomLabel,
27
- /**
28
- * Function to run on a click.
29
- */
30
- onClick?: (event?: React.MouseEvent) => void,
31
- /**
32
- * URL to open on a click.
33
- */
34
- href?: string,
35
- /**
36
- * Target of the URL to open.
37
- */
38
- target?: React.AnchorHTMLAttributes<HTMLAnchorElement>['target'],
39
- /**
40
- * Language of the linked document.
41
- */
42
- lang?: React.AnchorHTMLAttributes<HTMLAnchorElement>['lang'],
43
- /**
44
- * Language of the destination URL.
45
- */
46
- hrefLang?: React.AnchorHTMLAttributes<HTMLAnchorElement>['hrefLang'],
47
- /**
48
- * The title of the action.
49
- */
50
- title?: string,
51
- }
52
-
53
- interface ItemWithIcon {
54
- /**
55
- * An Icon to appear at the left of the item.
56
- */
57
- icon?: React.ReactElement,
58
- /**
59
- * An Icon to appear at the right of the item.
60
- */
61
- iconRight?: React.ReactElement,
62
- }
63
-
64
- export interface ListAction extends ItemWithIcon, Action {
65
- /**
66
- * Whether or not this option is currently active.
67
- */
68
- active?: boolean,
69
- /**
70
- * Icon to render when this option is active.
71
- */
72
- iconActive?: React.ReactElement,
73
- /**
74
- * A class to be added to this item.
75
- */
76
- className?: string,
77
- /**
78
- * Defines the ARIA role of the element for accessibility purposes. Example: "button", "dialog", etc.
79
- */
80
- role?: string,
81
- }
82
-
83
- interface ListGroup {
84
- /**
85
- * If this group is rendered as a section with its items right below it or a collapsible, which requires a click to open a submenu.
86
- */
87
- type?: 'section' | 'collapsible',
88
- /**
89
- * The items of this group.
90
- */
91
- children: ListItem[],
92
- /**
93
- * A class to be added to this item.
94
- */
95
- className?: string,
96
- }
97
-
98
- interface ListSection extends ListGroup {
99
- type: 'section',
100
- /**
101
- * The section's title.
102
- */
103
- label?: string,
104
- }
105
-
106
- interface ListCollapsible extends ListGroup, ItemWithIcon {
107
- type?: 'collapsible',
108
- /**
109
- * The title of the collapsible menu.
110
- */
111
- label: string,
112
- }
113
-
114
- type ListItem = ListSection | ListCollapsible | ListAction
115
-
116
- interface CurrentItemList {
117
- items: ListItem[],
118
- label?: string,
119
- parent?: CurrentItemList,
120
- }
121
-
122
- const ANIMATION_DURATION_MS = 300
123
- const MAX_HEIGHT_TRANSITION = `max-height ease-in ${ANIMATION_DURATION_MS / 1000}s`
124
-
125
- export interface SelectionListProps extends WithStyle {
126
- /**
127
- * The id of this selection list. This is important for accessibility. Be sure to link it to the aria-controls tag of the element who
128
- * controls the visibility of this selection list.
129
- */
130
- id: string,
131
- /**
132
- * Whether or not the selection list is visible.
133
- */
134
- visible?: boolean,
135
- /**
136
- * The options in the selection list.
137
- */
138
- items: ListItem[],
139
- /**
140
- * Function to run when the selection list is hidden/closed.
141
- */
142
- onHide?: () => void,
143
- /**
144
- * The maximum height for the selection list.
145
- * @default "300px"
146
- */
147
- maxHeight?: string,
148
- /**
149
- * A React element to render right before the items.
150
- */
151
- before?: ReactElement,
152
- /**
153
- * A React element to render right after the items.
154
- */
155
- after?: ReactElement,
156
- /**
157
- * Whether or not this list should be scrollable.
158
- */
159
- scroll?: boolean,
160
- /**
161
- * Show list as card
162
- */
163
- showListAsCard?: boolean,
164
- }
165
-
166
- interface RenderOptions {
167
- setCurrent: (current: CurrentItemList) => void,
168
- controllerId?: string,
169
- onClose?: () => void,
170
- Link: AnchorComponent,
171
- }
172
-
173
- const SelectionBox = styled.div<{ $maxHeight: string, $scroll?: boolean, $showListAsCard?: boolean }>`
174
- max-height: 0;
175
- overflow-y: ${({ $scroll }) => $scroll ? 'auto' : 'hidden'};
176
- overflow-x: hidden;
177
- transition: ${MAX_HEIGHT_TRANSITION}, visibility 0s ${ANIMATION_DURATION_MS / 1000}s;
178
- z-index: 1;
179
- box-shadow: 4px 4px 48px #000;
180
- border-radius: 0.5rem;
181
- visibility: hidden;
182
-
183
- .selection-list-content {
184
- display: flex;
185
- flex-direction: column;
186
- border-radius: 0.5rem;
187
- background-color: ${theme.color.light['300']};
188
- padding-inline: ${({ $showListAsCard }) => $showListAsCard ? '8px' : '0'};
189
- }
190
-
191
- .section-title, li > a {
192
- height: 40px;
193
- padding: 0 8px;
194
- display: flex;
195
- flex-direction: row;
196
- align-items: center;
197
- }
198
-
199
- li > a {
200
- gap: 4px;
201
- transition: background-color 0.2s;
202
- &:hover, &:focus {
203
- background: ${theme.color.light['400']};
204
- }
205
- .label {
206
- flex: 1;
207
- white-space: nowrap;
208
- overflow: hidden;
209
- text-overflow: ellipsis;
210
- }
211
- }
212
-
213
- li.action {
214
- background-color: ${({ $showListAsCard }) => $showListAsCard ? theme.color.light['400'] : ''};
215
- border: ${({ $showListAsCard }) => '1px solid' + $showListAsCard ? theme.color.light['500'] : 'transparent'};
216
- border-radius: ${({ $showListAsCard }) => $showListAsCard ? '4px' : '0'};
217
- margin-bottom: ${({ $showListAsCard }) => $showListAsCard ? '8px' : '0'};
218
- }
219
-
220
- li.section {
221
- border-bottom: 2px solid;
222
- border-bottom-color: ${({ $showListAsCard }) => $showListAsCard ? 'transparent' : theme.color.light['600']};
223
- &:last-child {
224
- border-bottom: none;
225
- }
226
- }
227
-
228
- &.visible {
229
- max-height: ${({ $maxHeight }) => $maxHeight};
230
- visibility: visible;
231
- transition: ${MAX_HEIGHT_TRANSITION};
232
- }
233
- `
234
-
235
- function renderAction({
236
- label, href, onClick, icon, iconRight, active, target, iconActive = <Check />,
237
- className = '', lang, hrefLang, title, role = 'link',
238
- }: ListAction, { onClose, Link }: RenderOptions) {
239
- function handleClick(event: React.MouseEvent) {
240
- onClick?.(event)
241
- onClose?.()
242
- }
243
-
244
- const isTextLabel = typeof label === 'string'
245
-
246
- return (
247
- <li key={isTextLabel ? label : label.id} className="action" title={title}>
248
- <Link href={href} onClick={handleClick} target={target} tabIndex={0} aria-current={active && 'page'}
249
- className={className} lang={lang} hrefLang={hrefLang} role={role}>
250
- {icon && <IconBox>{icon}</IconBox>}
251
- {isTextLabel ? <Text appearance="body2" className="label">{label}</Text> : label.element}
252
- {iconRight && <IconBox>{iconRight}</IconBox>}
253
- {active && <IconBox>{iconActive}</IconBox>}
254
- </Link>
255
- </li>
256
- )
257
- }
258
-
259
- function renderCollapsible({ label, icon, iconRight, className = '', children }:
260
- ListCollapsible, { setCurrent, controllerId, Link }: RenderOptions) {
261
- function handleClick(ev: React.MouseEvent) {
262
- //accessibility: this will tell the screen reader the section was expanded before this link is removed from the DOM.
263
- (ev.target as HTMLElement)?.setAttribute?.('aria-expanded', 'true')
264
- setCurrent({ items: children, label })
265
- }
266
- return (
267
- <li key={label} className="collapsible">
268
- <Link onClick={handleClick} tabIndex={0} className={className} aria-expanded={false} aria-controls={controllerId}>
269
- {icon && <IconBox>{icon}</IconBox>}
270
- <Text appearance="body2" className="label">{label}</Text>
271
- {iconRight && <IconBox>{iconRight}</IconBox>}
272
- <IconBox><ChevronRight /></IconBox>
273
- </Link>
274
- </li>
275
- )
276
- }
277
-
278
- function renderSection({ label, className = '', children }: ListSection, options: RenderOptions) {
279
- return (
280
- <li key={label ?? children.map(c => c.label).join('-')} className={`${className} section`}>
281
- {label && <Text appearance="overheader2" colorScheme="light.700" className="section-title">{label}</Text>}
282
- <ul>{children.map(i => renderItem(i, options))}</ul>
283
- </li>
284
- )
285
- }
286
-
287
- function renderItem(item: ListItem, options: RenderOptions) {
288
- if ('children' in item) {
289
- return item.type === 'section' ? renderSection(item, options) : renderCollapsible(item, options)
290
- }
291
- return renderAction(item, options)
292
- }
293
-
294
- /**
295
- * Renders a component that allows the selection of one item. The list is show/hidden according to the prop `visible`.
296
- *
297
- * The items in this list can be grouped into multiple sections. Sections may be displayed on a column (section) or might replace the
298
- * current list of options with another list of options (collapsible).
299
- *
300
- * This component implements keyboard controls and accessibility features.
301
- * @param props the component's props {@link SelectionListProps}.
302
- */
303
- export const SelectionList = ({
304
- id, items, className, style, visible = true, maxHeight = '300px', onHide, before, after, scroll, showListAsCard,
305
- }: SelectionListProps) => {
306
- const Link = useAnchorTag()
307
- const t = useTranslate(dictionary)
308
- const [current, setCurrent] = useState<CurrentItemList>({ items })
309
- const { keyboardControlledElement: wrapper, attachKeyboardListeners, detachKeyboardListeners } = useKeyboardControls(
310
- { onPressEscape: onHide, querySelectors: 'li.action a, li.collapsible a, button' },
311
- )
312
-
313
- const listItems = useMemo(
314
- () => current.items.map(i => renderItem(
315
- i,
316
- {
317
- setCurrent: (next: CurrentItemList) => setCurrent({ ...next, parent: current }),
318
- onClose: onHide,
319
- controllerId: id,
320
- Link,
321
- },
322
- )),
323
- [current],
324
- )
325
-
326
- useEffect(() => {
327
- const selectionBox = wrapper.current
328
- if (!selectionBox) return
329
-
330
- const handleTransitionEnd = () => {
331
- selectionBox.style.overflow = 'auto'
332
- }
333
-
334
- selectionBox.addEventListener('transitionend', handleTransitionEnd)
335
-
336
- return () => {
337
- selectionBox.removeEventListener('transitionend', handleTransitionEnd)
338
- }
339
- }, [])
340
-
341
- const handleExpand = () => {
342
- const selectionBox = wrapper.current
343
- if (selectionBox) {
344
- selectionBox.style.overflow = 'hidden'
345
- }
346
- }
347
-
348
- const hide = useCallback((event: Event) => {
349
- const target = (event.target as HTMLElement | null)
350
- // if the element is not in the DOM anymore, we'll consider the click was inside the selection list
351
- const isClickInsideSelectionList = !target?.isConnected || wrapper.current?.contains(target)
352
- const isAction = target?.classList?.contains('action') || !!target?.closest('.action')
353
- const isInsideTour = !!target?.closest('#___reactour')
354
- if (!isInsideTour && (!isClickInsideSelectionList || isAction)) onHide?.()
355
- }, [])
356
-
357
- useEffect(() => {
358
- handleExpand()
359
- function removeListeners() {
360
- detachKeyboardListeners()
361
- document.removeEventListener('click', hide)
362
- }
363
- if (visible) {
364
- setCurrent({ items })
365
- attachKeyboardListeners()
366
- if (onHide) setTimeout(() => document.addEventListener('click', hide), 50)
367
- } else {
368
- removeListeners()
369
- }
370
- return () => removeListeners()
371
- }, [visible])
372
-
373
- return (
374
- <SelectionBox
375
- id={id}
376
- ref={wrapper}
377
- $maxHeight={maxHeight}
378
- style={style}
379
- className={listToClass(['selection-list', visible ? 'visible' : undefined, className])}
380
- $scroll={scroll}
381
- aria-hidden={!visible}
382
- $showListAsCard={showListAsCard}
383
- >
384
- <div className="selection-list-content">
385
- {before}
386
- {current.parent
387
- ? (
388
- <Flex mt={5} mb={1} alignItems="center">
389
- <IconButton
390
- onClick={(ev) => {
391
- // accessibility: this will tell the screen reader the section was collapsed before this button is removed from the DOM.
392
- (ev.target as HTMLElement)?.setAttribute?.('aria-expanded', 'false')
393
- setCurrent(current.parent ?? { items })
394
- }}
395
- sx={{ mr: 3 }}
396
- title={t.back}
397
- aria-controls={id}
398
- aria-expanded={true}
399
- >
400
- <ArrowLeft />
401
- </IconButton>
402
- <Text appearance="microtext1">{current.label}</Text>
403
- </Flex>
404
- )
405
- : undefined
406
- }
407
- <ul>
408
- {listItems}
409
- {after &&
410
- <li className="action">
411
- {after}
412
- </li>
413
- }
414
- </ul>
415
- </div>
416
- </SelectionBox>
417
- )
418
- }
419
-
420
- const dictionary = {
421
- en: {
422
- back: 'Go back',
423
- },
424
- pt: {
425
- back: 'Voltar',
426
- },
427
- } satisfies Dictionary
1
+ import { Flex, IconBox, Text } from '@citric/core'
2
+ import { ArrowLeft, Check, ChevronRight } from '@citric/icons'
3
+ import { IconButton } from '@citric/ui'
4
+ import { WithStyle, listToClass, theme } from '@stack-spot/portal-theme'
5
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
6
+ import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
7
+ import { styled } from 'styled-components'
8
+ import { AnchorComponent, useAnchorTag } from '../context/anchor'
9
+ import { useKeyboardControls } from '../hooks/keyboard'
10
+
11
+ interface CustomLabel {
12
+ /**
13
+ * A unique identifier for this label. This is also used to read this label to screen readers.
14
+ */
15
+ id: string,
16
+ /**
17
+ * A custom label that can be made up of any React component.
18
+ */
19
+ element: React.ReactNode,
20
+ }
21
+
22
+ export interface Action {
23
+ /**
24
+ * The label of the action.
25
+ */
26
+ label: string | CustomLabel,
27
+ /**
28
+ * Function to run on a click.
29
+ */
30
+ onClick?: (event?: React.MouseEvent) => void,
31
+ /**
32
+ * URL to open on a click.
33
+ */
34
+ href?: string,
35
+ /**
36
+ * Target of the URL to open.
37
+ */
38
+ target?: React.AnchorHTMLAttributes<HTMLAnchorElement>['target'],
39
+ /**
40
+ * Language of the linked document.
41
+ */
42
+ lang?: React.AnchorHTMLAttributes<HTMLAnchorElement>['lang'],
43
+ /**
44
+ * Language of the destination URL.
45
+ */
46
+ hrefLang?: React.AnchorHTMLAttributes<HTMLAnchorElement>['hrefLang'],
47
+ /**
48
+ * The title of the action.
49
+ */
50
+ title?: string,
51
+ }
52
+
53
+ interface ItemWithIcon {
54
+ /**
55
+ * An Icon to appear at the left of the item.
56
+ */
57
+ icon?: React.ReactElement,
58
+ /**
59
+ * An Icon to appear at the right of the item.
60
+ */
61
+ iconRight?: React.ReactElement,
62
+ }
63
+
64
+ export interface ListAction extends ItemWithIcon, Action {
65
+ /**
66
+ * Whether or not this option is currently active.
67
+ */
68
+ active?: boolean,
69
+ /**
70
+ * Icon to render when this option is active.
71
+ */
72
+ iconActive?: React.ReactElement,
73
+ /**
74
+ * A class to be added to this item.
75
+ */
76
+ className?: string,
77
+ /**
78
+ * Defines the ARIA role of the element for accessibility purposes. Example: "button", "dialog", etc.
79
+ */
80
+ role?: string,
81
+ }
82
+
83
+ interface ListGroup {
84
+ /**
85
+ * If this group is rendered as a section with its items right below it or a collapsible, which requires a click to open a submenu.
86
+ */
87
+ type?: 'section' | 'collapsible',
88
+ /**
89
+ * The items of this group.
90
+ */
91
+ children: ListItem[],
92
+ /**
93
+ * A class to be added to this item.
94
+ */
95
+ className?: string,
96
+ }
97
+
98
+ interface ListSection extends ListGroup {
99
+ type: 'section',
100
+ /**
101
+ * The section's title.
102
+ */
103
+ label?: string,
104
+ }
105
+
106
+ interface ListCollapsible extends ListGroup, ItemWithIcon {
107
+ type?: 'collapsible',
108
+ /**
109
+ * The title of the collapsible menu.
110
+ */
111
+ label: string,
112
+ }
113
+
114
+ type ListItem = ListSection | ListCollapsible | ListAction
115
+
116
+ interface CurrentItemList {
117
+ items: ListItem[],
118
+ label?: string,
119
+ parent?: CurrentItemList,
120
+ }
121
+
122
+ const ANIMATION_DURATION_MS = 300
123
+ const MAX_HEIGHT_TRANSITION = `max-height ease-in ${ANIMATION_DURATION_MS / 1000}s`
124
+
125
+ export interface SelectionListProps extends WithStyle {
126
+ /**
127
+ * The id of this selection list. This is important for accessibility. Be sure to link it to the aria-controls tag of the element who
128
+ * controls the visibility of this selection list.
129
+ */
130
+ id: string,
131
+ /**
132
+ * Whether or not the selection list is visible.
133
+ */
134
+ visible?: boolean,
135
+ /**
136
+ * The options in the selection list.
137
+ */
138
+ items: ListItem[],
139
+ /**
140
+ * Function to run when the selection list is hidden/closed.
141
+ */
142
+ onHide?: () => void,
143
+ /**
144
+ * The maximum height for the selection list.
145
+ * @default "300px"
146
+ */
147
+ maxHeight?: string,
148
+ /**
149
+ * A React element to render right before the items.
150
+ */
151
+ before?: ReactElement,
152
+ /**
153
+ * A React element to render right after the items.
154
+ */
155
+ after?: ReactElement,
156
+ /**
157
+ * Whether or not this list should be scrollable.
158
+ */
159
+ scroll?: boolean,
160
+ /**
161
+ * Show list as card
162
+ */
163
+ showListAsCard?: boolean,
164
+ }
165
+
166
+ interface RenderOptions {
167
+ setCurrent: (current: CurrentItemList) => void,
168
+ controllerId?: string,
169
+ onClose?: () => void,
170
+ Link: AnchorComponent,
171
+ }
172
+
173
+ const SelectionBox = styled.div<{ $maxHeight: string, $scroll?: boolean, $showListAsCard?: boolean }>`
174
+ max-height: 0;
175
+ overflow-y: ${({ $scroll }) => $scroll ? 'auto' : 'hidden'};
176
+ overflow-x: hidden;
177
+ transition: ${MAX_HEIGHT_TRANSITION}, visibility 0s ${ANIMATION_DURATION_MS / 1000}s;
178
+ z-index: 1;
179
+ box-shadow: 4px 4px 48px #000;
180
+ border-radius: 0.5rem;
181
+ visibility: hidden;
182
+
183
+ .selection-list-content {
184
+ display: flex;
185
+ flex-direction: column;
186
+ border-radius: 0.5rem;
187
+ background-color: ${theme.color.light['300']};
188
+ padding-inline: ${({ $showListAsCard }) => $showListAsCard ? '8px' : '0'};
189
+ }
190
+
191
+ .section-title, li > a {
192
+ height: 40px;
193
+ padding: 0 8px;
194
+ display: flex;
195
+ flex-direction: row;
196
+ align-items: center;
197
+ }
198
+
199
+ li > a {
200
+ gap: 4px;
201
+ transition: background-color 0.2s;
202
+ &:hover, &:focus {
203
+ background: ${theme.color.light['400']};
204
+ }
205
+ .label {
206
+ flex: 1;
207
+ white-space: nowrap;
208
+ overflow: hidden;
209
+ text-overflow: ellipsis;
210
+ }
211
+ }
212
+
213
+ li.action {
214
+ background-color: ${({ $showListAsCard }) => $showListAsCard ? theme.color.light['400'] : ''};
215
+ border: ${({ $showListAsCard }) => '1px solid' + $showListAsCard ? theme.color.light['500'] : 'transparent'};
216
+ border-radius: ${({ $showListAsCard }) => $showListAsCard ? '4px' : '0'};
217
+ margin-bottom: ${({ $showListAsCard }) => $showListAsCard ? '8px' : '0'};
218
+ }
219
+
220
+ li.section {
221
+ border-bottom: 2px solid;
222
+ border-bottom-color: ${({ $showListAsCard }) => $showListAsCard ? 'transparent' : theme.color.light['600']};
223
+ &:last-child {
224
+ border-bottom: none;
225
+ }
226
+ }
227
+
228
+ &.visible {
229
+ max-height: ${({ $maxHeight }) => $maxHeight};
230
+ visibility: visible;
231
+ transition: ${MAX_HEIGHT_TRANSITION};
232
+ }
233
+ `
234
+
235
+ function renderAction({
236
+ label, href, onClick, icon, iconRight, active, target, iconActive = <Check />,
237
+ className = '', lang, hrefLang, title, role = 'link',
238
+ }: ListAction, { onClose, Link }: RenderOptions) {
239
+ function handleClick(event: React.MouseEvent) {
240
+ onClick?.(event)
241
+ onClose?.()
242
+ }
243
+
244
+ const isTextLabel = typeof label === 'string'
245
+
246
+ return (
247
+ <li key={isTextLabel ? label : label.id} className="action" title={title}>
248
+ <Link href={href} onClick={handleClick} target={target} tabIndex={0} aria-current={active && 'page'}
249
+ className={className} lang={lang} hrefLang={hrefLang} role={role}>
250
+ {icon && <IconBox>{icon}</IconBox>}
251
+ {isTextLabel ? <Text appearance="body2" className="label">{label}</Text> : label.element}
252
+ {iconRight && <IconBox>{iconRight}</IconBox>}
253
+ {active && <IconBox>{iconActive}</IconBox>}
254
+ </Link>
255
+ </li>
256
+ )
257
+ }
258
+
259
+ function renderCollapsible({ label, icon, iconRight, className = '', children }:
260
+ ListCollapsible, { setCurrent, controllerId, Link }: RenderOptions) {
261
+ function handleClick(ev: React.MouseEvent) {
262
+ //accessibility: this will tell the screen reader the section was expanded before this link is removed from the DOM.
263
+ (ev.target as HTMLElement)?.setAttribute?.('aria-expanded', 'true')
264
+ setCurrent({ items: children, label })
265
+ }
266
+ return (
267
+ <li key={label} className="collapsible">
268
+ <Link onClick={handleClick} tabIndex={0} className={className} aria-expanded={false} aria-controls={controllerId}>
269
+ {icon && <IconBox>{icon}</IconBox>}
270
+ <Text appearance="body2" className="label">{label}</Text>
271
+ {iconRight && <IconBox>{iconRight}</IconBox>}
272
+ <IconBox><ChevronRight /></IconBox>
273
+ </Link>
274
+ </li>
275
+ )
276
+ }
277
+
278
+ function renderSection({ label, className = '', children }: ListSection, options: RenderOptions) {
279
+ return (
280
+ <li key={label ?? children.map(c => c.label).join('-')} className={`${className} section`}>
281
+ {label && <Text appearance="overheader2" colorScheme="light.700" className="section-title">{label}</Text>}
282
+ <ul>{children.map(i => renderItem(i, options))}</ul>
283
+ </li>
284
+ )
285
+ }
286
+
287
+ function renderItem(item: ListItem, options: RenderOptions) {
288
+ if ('children' in item) {
289
+ return item.type === 'section' ? renderSection(item, options) : renderCollapsible(item, options)
290
+ }
291
+ return renderAction(item, options)
292
+ }
293
+
294
+ /**
295
+ * Renders a component that allows the selection of one item. The list is show/hidden according to the prop `visible`.
296
+ *
297
+ * The items in this list can be grouped into multiple sections. Sections may be displayed on a column (section) or might replace the
298
+ * current list of options with another list of options (collapsible).
299
+ *
300
+ * This component implements keyboard controls and accessibility features.
301
+ * @param props the component's props {@link SelectionListProps}.
302
+ */
303
+ export const SelectionList = ({
304
+ id, items, className, style, visible = true, maxHeight = '300px', onHide, before, after, scroll, showListAsCard,
305
+ }: SelectionListProps) => {
306
+ const Link = useAnchorTag()
307
+ const t = useTranslate(dictionary)
308
+ const [current, setCurrent] = useState<CurrentItemList>({ items })
309
+ const { keyboardControlledElement: wrapper, attachKeyboardListeners, detachKeyboardListeners } = useKeyboardControls(
310
+ { onPressEscape: onHide, querySelectors: 'li.action a, li.collapsible a, button' },
311
+ )
312
+
313
+ const listItems = useMemo(
314
+ () => current.items.map(i => renderItem(
315
+ i,
316
+ {
317
+ setCurrent: (next: CurrentItemList) => setCurrent({ ...next, parent: current }),
318
+ onClose: onHide,
319
+ controllerId: id,
320
+ Link,
321
+ },
322
+ )),
323
+ [current],
324
+ )
325
+
326
+ useEffect(() => {
327
+ const selectionBox = wrapper.current
328
+ if (!selectionBox) return
329
+
330
+ const handleTransitionEnd = () => {
331
+ selectionBox.style.overflow = 'auto'
332
+ }
333
+
334
+ selectionBox.addEventListener('transitionend', handleTransitionEnd)
335
+
336
+ return () => {
337
+ selectionBox.removeEventListener('transitionend', handleTransitionEnd)
338
+ }
339
+ }, [])
340
+
341
+ const handleExpand = () => {
342
+ const selectionBox = wrapper.current
343
+ if (selectionBox) {
344
+ selectionBox.style.overflow = 'hidden'
345
+ }
346
+ }
347
+
348
+ const hide = useCallback((event: Event) => {
349
+ const target = (event.target as HTMLElement | null)
350
+ // if the element is not in the DOM anymore, we'll consider the click was inside the selection list
351
+ const isClickInsideSelectionList = !target?.isConnected || wrapper.current?.contains(target)
352
+ const isAction = target?.classList?.contains('action') || !!target?.closest('.action')
353
+ const isInsideTour = !!target?.closest('#___reactour')
354
+ if (!isInsideTour && (!isClickInsideSelectionList || isAction)) onHide?.()
355
+ }, [])
356
+
357
+ useEffect(() => {
358
+ handleExpand()
359
+ function removeListeners() {
360
+ detachKeyboardListeners()
361
+ document.removeEventListener('click', hide)
362
+ }
363
+ if (visible) {
364
+ setCurrent({ items })
365
+ attachKeyboardListeners()
366
+ if (onHide) setTimeout(() => document.addEventListener('click', hide), 50)
367
+ } else {
368
+ removeListeners()
369
+ }
370
+ return () => removeListeners()
371
+ }, [visible])
372
+
373
+ return (
374
+ <SelectionBox
375
+ id={id}
376
+ ref={wrapper}
377
+ $maxHeight={maxHeight}
378
+ style={style}
379
+ className={listToClass(['selection-list', visible ? 'visible' : undefined, className])}
380
+ $scroll={scroll}
381
+ aria-hidden={!visible}
382
+ $showListAsCard={showListAsCard}
383
+ >
384
+ <div className="selection-list-content">
385
+ {before}
386
+ {current.parent
387
+ ? (
388
+ <Flex mt={5} mb={1} alignItems="center">
389
+ <IconButton
390
+ onClick={(ev) => {
391
+ // accessibility: this will tell the screen reader the section was collapsed before this button is removed from the DOM.
392
+ (ev.target as HTMLElement)?.setAttribute?.('aria-expanded', 'false')
393
+ setCurrent(current.parent ?? { items })
394
+ }}
395
+ sx={{ mr: 3 }}
396
+ title={t.back}
397
+ aria-controls={id}
398
+ aria-expanded={true}
399
+ >
400
+ <ArrowLeft />
401
+ </IconButton>
402
+ <Text appearance="microtext1">{current.label}</Text>
403
+ </Flex>
404
+ )
405
+ : undefined
406
+ }
407
+ <ul>
408
+ {listItems}
409
+ {after &&
410
+ <li className="action">
411
+ {after}
412
+ </li>
413
+ }
414
+ </ul>
415
+ </div>
416
+ </SelectionBox>
417
+ )
418
+ }
419
+
420
+ const dictionary = {
421
+ en: {
422
+ back: 'Go back',
423
+ },
424
+ pt: {
425
+ back: 'Voltar',
426
+ },
427
+ } satisfies Dictionary