@stack-spot/portal-components 2.26.0 → 2.27.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 (237) hide show
  1. package/CHANGELOG.md +621 -614
  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 +3 -3
  40. package/dist/components/Placeholder.js +1 -1
  41. package/dist/components/ScrollView.js +16 -16
  42. package/dist/components/Select/BadgeItem.d.ts +1 -1
  43. package/dist/components/Select/BadgeItem.js +1 -1
  44. package/dist/components/Select/ClearInput.d.ts +1 -1
  45. package/dist/components/Select/ClearInput.js +1 -1
  46. package/dist/components/Select/CloseItem.d.ts +1 -1
  47. package/dist/components/Select/CloseItem.js +1 -1
  48. package/dist/components/Select/CreatableSelect.js +1 -1
  49. package/dist/components/Select/CustomMenu.d.ts +1 -1
  50. package/dist/components/Select/CustomMenu.js +1 -1
  51. package/dist/components/Select/LabelItem.d.ts +1 -1
  52. package/dist/components/Select/LabelItem.js +1 -1
  53. package/dist/components/Select/MultiValue.d.ts +1 -1
  54. package/dist/components/Select/MultiValue.js +1 -1
  55. package/dist/components/Select/SelectInfiniteScroll.d.ts +1 -1
  56. package/dist/components/Select/SelectInfiniteScroll.js +1 -1
  57. package/dist/components/Select/SelectSearch.d.ts +1 -1
  58. package/dist/components/Select/SelectSearch.js +1 -1
  59. package/dist/components/SelectionList.d.ts +1 -1
  60. package/dist/components/SelectionList.js +61 -61
  61. package/dist/components/StatusCircle.d.ts +1 -1
  62. package/dist/components/StatusCircle.js +6 -6
  63. package/dist/components/Stepper/Navigation.js +4 -4
  64. package/dist/components/Stepper/Step.js +3 -3
  65. package/dist/components/Stepper/Stepper.js +6 -6
  66. package/dist/components/Stepper/headers.js +22 -22
  67. package/dist/components/Table/HeaderItem.js +1 -1
  68. package/dist/components/Table/SettingsVerticalMenu.d.ts +1 -1
  69. package/dist/components/Table/SettingsVerticalMenu.js +1 -1
  70. package/dist/components/Table/StyledLinkTable.d.ts +1 -1
  71. package/dist/components/Table/StyledLinkTable.js +5 -5
  72. package/dist/components/Table/TableData.d.ts +1 -1
  73. package/dist/components/Table/TableData.js +25 -25
  74. package/dist/components/TimelineSection.d.ts +1 -1
  75. package/dist/components/TimelineSection.js +14 -14
  76. package/dist/components/error/ErrorFeedback.d.ts +1 -1
  77. package/dist/components/error/ErrorFeedback.js +35 -35
  78. package/dist/components/error/NotFound.d.ts +1 -1
  79. package/dist/components/error/NotFound.js +1 -1
  80. package/dist/components/error/UnderMaintenance.d.ts +1 -1
  81. package/dist/components/error/UnderMaintenance.js +1 -1
  82. package/dist/components/form/Form/Form.d.ts +1 -1
  83. package/dist/components/form/Form/Form.js +1 -1
  84. package/dist/components/form/Form/FormGroup.d.ts +2 -2
  85. package/dist/components/form/Form/FormGroup.js +1 -1
  86. package/dist/components/form/SearchInput.d.ts +1 -1
  87. package/dist/components/form/SearchInput.js +1 -1
  88. package/dist/components/form/Select/CustomSelect.d.ts +1 -1
  89. package/dist/components/form/Select/CustomSelect.js +1 -1
  90. package/dist/components/form/Select/DetailedSelect.d.ts +1 -1
  91. package/dist/components/form/Select/DetailedSelect.js +1 -1
  92. package/dist/components/form/Select/Select.d.ts +1 -1
  93. package/dist/components/form/Select/Select.js +1 -1
  94. package/dist/components/form/Select/styled.js +161 -161
  95. package/dist/components/form/Select/utils.js +1 -1
  96. package/dist/components/notification/NotificationComponent.d.ts +1 -1
  97. package/dist/components/notification/NotificationComponent.js +54 -54
  98. package/dist/components/notification/NotificationItem.d.ts +1 -1
  99. package/dist/components/notification/NotificationItem.js +1 -1
  100. package/dist/components/notification/NotificationList.d.ts +1 -1
  101. package/dist/components/notification/NotificationList.js +43 -43
  102. package/dist/components/notification/NotificationPlaceholder.d.ts +1 -1
  103. package/dist/components/notification/NotificationPlaceholder.js +9 -9
  104. package/dist/components/notification/NotificationPlaceholder.js.map +1 -1
  105. package/dist/containers/NotificationsPage.d.ts +1 -1
  106. package/dist/containers/NotificationsPage.js +10 -10
  107. package/dist/context/anchor.d.ts +1 -1
  108. package/dist/context/anchor.js +1 -1
  109. package/dist/context/loading.d.ts +1 -1
  110. package/dist/context/loading.js +1 -1
  111. package/dist/context/notification/context.d.ts +1 -1
  112. package/dist/context/notification/context.js +1 -1
  113. package/dist/hooks/date.js +1 -1
  114. package/dist/hooks/service-now.js +28 -28
  115. package/dist/svg/AI.d.ts +1 -1
  116. package/dist/svg/AI.js +1 -1
  117. package/dist/svg/CS.d.ts +1 -1
  118. package/dist/svg/CS.js +1 -1
  119. package/dist/svg/EDP.d.ts +1 -1
  120. package/dist/svg/EDP.js +1 -1
  121. package/dist/svg/Forbidden.d.ts +1 -1
  122. package/dist/svg/Forbidden.js +1 -1
  123. package/dist/svg/GenericPlaceholder.d.ts +1 -1
  124. package/dist/svg/GenericPlaceholder.js +1 -1
  125. package/dist/svg/HUB.d.ts +1 -1
  126. package/dist/svg/HUB.js +1 -1
  127. package/dist/svg/Logo.d.ts +1 -1
  128. package/dist/svg/Logo.js +1 -1
  129. package/dist/svg/MiniLogo.d.ts +1 -1
  130. package/dist/svg/MiniLogo.js +1 -1
  131. package/dist/svg/NotFound.d.ts +1 -1
  132. package/dist/svg/NotFound.js +1 -1
  133. package/dist/svg/ServerError.d.ts +1 -1
  134. package/dist/svg/ServerError.js +1 -1
  135. package/dist/svg/Unauthenticated.d.ts +1 -1
  136. package/dist/svg/Unauthenticated.js +1 -1
  137. package/package.json +6 -6
  138. package/readme.md +66 -66
  139. package/src/components/AnimatedHeight.tsx +174 -174
  140. package/src/components/AsyncContent.tsx +78 -78
  141. package/src/components/BannerWarning.tsx +91 -91
  142. package/src/components/Breadcrumb/index.tsx +76 -76
  143. package/src/components/Breadcrumb/styled.ts +37 -37
  144. package/src/components/ButtonLoading.tsx +29 -29
  145. package/src/components/ChatBot.tsx +82 -82
  146. package/src/components/ContentValidateFilter.tsx +15 -15
  147. package/src/components/FadingOverflow.tsx +265 -265
  148. package/src/components/FileTreeView/More.tsx +114 -114
  149. package/src/components/FileTreeView/index.tsx +186 -186
  150. package/src/components/InfiniteScroll.tsx +24 -24
  151. package/src/components/InfoMaintenanceBanner.tsx +29 -29
  152. package/src/components/LazyMarkdown/BlockquoteMd.tsx +107 -107
  153. package/src/components/LazyMarkdown/CodeViewer.tsx +161 -161
  154. package/src/components/LazyMarkdown/Markdown.tsx +122 -122
  155. package/src/components/LazyMarkdown/MarkdownButton.tsx +24 -24
  156. package/src/components/LazyMarkdown/Video.tsx +13 -13
  157. package/src/components/LazyMarkdown/index.tsx +21 -21
  158. package/src/components/Placeholder.tsx +118 -118
  159. package/src/components/ScrollView.tsx +57 -57
  160. package/src/components/Select/BadgeItem.tsx +58 -58
  161. package/src/components/Select/ClearInput.tsx +24 -24
  162. package/src/components/Select/CloseItem.tsx +38 -38
  163. package/src/components/Select/CreatableSelect.tsx +155 -155
  164. package/src/components/Select/CustomMenu.tsx +16 -16
  165. package/src/components/Select/LabelItem.tsx +8 -8
  166. package/src/components/Select/MultiValue.tsx +49 -49
  167. package/src/components/Select/SelectInfiniteScroll.tsx +82 -82
  168. package/src/components/Select/SelectSearch.tsx +195 -195
  169. package/src/components/Select/index.tsx +7 -7
  170. package/src/components/Select/types.ts +8 -8
  171. package/src/components/SelectionList.tsx +427 -427
  172. package/src/components/StatusCircle.tsx +67 -67
  173. package/src/components/Stepper/Navigation.tsx +97 -97
  174. package/src/components/Stepper/Step.tsx +30 -30
  175. package/src/components/Stepper/Stepper.tsx +113 -113
  176. package/src/components/Stepper/headers.tsx +64 -64
  177. package/src/components/Stepper/index.ts +3 -3
  178. package/src/components/Table/HeaderItem.tsx +52 -52
  179. package/src/components/Table/SettingsVerticalMenu.tsx +50 -50
  180. package/src/components/Table/StyledLinkTable.tsx +22 -22
  181. package/src/components/Table/TableData.tsx +251 -251
  182. package/src/components/Table/index.tsx +2 -2
  183. package/src/components/TimelineSection.tsx +66 -66
  184. package/src/components/error/ErrorFeedback.tsx +217 -217
  185. package/src/components/error/NotFound.tsx +24 -24
  186. package/src/components/error/UnderMaintenance.tsx +30 -30
  187. package/src/components/error/index.ts +4 -4
  188. package/src/components/form/Form/Form.tsx +101 -101
  189. package/src/components/form/Form/FormGroup.tsx +221 -221
  190. package/src/components/form/Form/index.ts +2 -2
  191. package/src/components/form/SearchInput.tsx +69 -69
  192. package/src/components/form/Select/CustomSelect.tsx +232 -232
  193. package/src/components/form/Select/DetailedSelect.tsx +85 -85
  194. package/src/components/form/Select/Select.tsx +67 -67
  195. package/src/components/form/Select/index.ts +4 -4
  196. package/src/components/form/Select/styled.ts +165 -165
  197. package/src/components/form/Select/types.ts +112 -112
  198. package/src/components/form/Select/utils.tsx +28 -28
  199. package/src/components/notification/NotificationComponent.tsx +340 -340
  200. package/src/components/notification/NotificationItem.tsx +336 -336
  201. package/src/components/notification/NotificationList.tsx +178 -178
  202. package/src/components/notification/NotificationPlaceholder.tsx +43 -43
  203. package/src/components/notification/types.ts +72 -72
  204. package/src/containers/NotificationsPage.tsx +98 -98
  205. package/src/context/anchor.tsx +37 -37
  206. package/src/context/loading.tsx +36 -36
  207. package/src/context/notification/LazyNotificationList.ts +103 -103
  208. package/src/context/notification/NotificationController.ts +104 -104
  209. package/src/context/notification/context.tsx +23 -23
  210. package/src/context/notification/hooks.ts +98 -98
  211. package/src/context/notification/types.ts +65 -65
  212. package/src/hooks/date.ts +31 -31
  213. package/src/hooks/keyboard.tsx +128 -128
  214. package/src/hooks/manual-render.tsx +10 -10
  215. package/src/hooks/service-now.tsx +233 -233
  216. package/src/hooks/text.tsx +30 -30
  217. package/src/hooks/title.tsx +28 -28
  218. package/src/hooks/use-effect-once.tsx +43 -43
  219. package/src/index.ts +19 -19
  220. package/src/notifications.ts +11 -11
  221. package/src/svg/AI.tsx +41 -41
  222. package/src/svg/CS.tsx +48 -48
  223. package/src/svg/EDP.tsx +31 -31
  224. package/src/svg/Forbidden.tsx +22 -22
  225. package/src/svg/GenericPlaceholder.tsx +20 -20
  226. package/src/svg/HUB.tsx +48 -48
  227. package/src/svg/Logo.tsx +16 -16
  228. package/src/svg/MiniLogo.tsx +12 -12
  229. package/src/svg/NotFound.tsx +16 -16
  230. package/src/svg/ServerError.tsx +33 -33
  231. package/src/svg/Unauthenticated.tsx +16 -16
  232. package/src/svg/index.ts +11 -11
  233. package/src/utils/accessibility.ts +135 -135
  234. package/src/utils/cookie.ts +73 -73
  235. package/src/utils/promise.ts +5 -5
  236. package/src/utils/read-file.ts +16 -16
  237. package/tsconfig.json +10 -10
@@ -1,265 +1,265 @@
1
- import { IconBox } from '@citric/core'
2
- import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from '@citric/icons'
3
- import { listToClass, theme, WithStyle } from '@stack-spot/portal-theme'
4
- import { debounce } from 'lodash'
5
- import { useEffect, useRef } from 'react'
6
- import { styled } from 'styled-components'
7
-
8
- type Side = 'top' | 'right' | 'left' | 'bottom'
9
- type ScrollType = 'none' | 'wheel' | 'bar' | 'arrows'
10
-
11
- interface Props extends WithStyle {
12
- /**
13
- * How to scroll the content.
14
- * - none: overflow is hidden. No scrolling.
15
- * - wheel: the content is solely scrollable through the mouse wheel and keyboard arrows, no scroll bars are rendered.
16
- * - bar: this is the normal browser scroll. The content will be scrollable through the mouse wheel, keyboard and scrollbar.
17
- * - arrows: arrows will be placed in the far edges of the sides that must be scrolled. The content is scrolled through the mouse wheel,
18
- * keyboard and these arrows. Hovering an arrow slowly scrolls the content, clicking the arrow fully scrolls the content in its direction.
19
- * There are no scrollbars in this scenario.
20
- * @default 'none'
21
- */
22
- scroll?: ScrollType,
23
- /**
24
- * If true, a horizontal scroll can be performed by using the vertical mouse wheel.
25
- * @default false
26
- */
27
- enableHorizontalScrollWithVerticalWheel?: boolean,
28
- /**
29
- * Which sides to fade when the content overflows.
30
- *
31
- * Important: it seems the current solution doesn't support mixing vertical and horizontal scrolls. I'm not sure if this is possible, I
32
- * believed combining linear gradients would work, but it doesn't.
33
- */
34
- sides?: Side[],
35
- children?: React.ReactNode,
36
- }
37
-
38
- // Scrolling can be very fast, this sets a lower limit for us to run listeners without impacting performance.
39
- const MIN_CHECK_INTERVAL_MS = 20
40
- // The interval which to scroll the content when a scroll arrow is hovered.
41
- const SCROLL_INTERVAL_MS = 20
42
- // This sets the speed of the scroll when the user hovers a scroll arrow.
43
- const SCROLL_PX = 4
44
- // Fade masks
45
- const masks = {
46
- right: 'linear-gradient(to left, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
47
- left: 'linear-gradient(to right, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
48
- top: 'linear-gradient(to bottom, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
49
- bottom: 'linear-gradient(to top, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
50
- horizontal: 'linear-gradient(to left, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgb(0, 0, 0) max(70%, calc(100% - 100px)), rgba(0, 0, 0, 0) 100%)',
51
- vertical: 'linear-gradient(to top, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgb(0, 0, 0) max(70%, calc(100% - 100px)), rgba(0, 0, 0, 0) 100%)',
52
- }
53
-
54
- const OverflowBox = styled.div`
55
- &.hidden-scroll-bars::-webkit-scrollbar, &.scroll-arrows ::-webkit-scrollbar {
56
- width: 0;
57
- height: 0;
58
- }
59
-
60
- &.scroll-arrows {
61
- position: relative;
62
-
63
- > .content {
64
- width: 100%;
65
- }
66
- }
67
-
68
- .scroll-to-left, .scroll-to-right, .scroll-to-top, .scroll-to-bottom {
69
- position: absolute;
70
- top: 0;
71
- bottom: 0;
72
- left: 0;
73
- right: 0;
74
- width: 30px;
75
- height: 30px;
76
- display: flex;
77
- align-items: center;
78
- justify-content: center;
79
- opacity: 0;
80
- pointer-events: none;
81
- transition: opacity 0.3s;
82
-
83
- ${IconBox} {
84
- background-color: ${theme.color.light[300]};
85
- border-radius: 50%;
86
- width: 16px;
87
- height: 16px;
88
- display: flex;
89
- align-items: center;
90
- justify-content: center;
91
- }
92
-
93
- &.visible {
94
- opacity: 0.6;
95
- pointer-events: auto;
96
-
97
- &:hover {
98
- opacity: 1;
99
- }
100
- }
101
- }
102
-
103
- .scroll-to-left {
104
- right: unset;
105
- height: unset;
106
- }
107
-
108
- .scroll-to-right {
109
- left: unset;
110
- height: unset;
111
- }
112
-
113
- .scroll-to-top {
114
- bottom: unset;
115
- width: unset;
116
- }
117
-
118
- .scroll-to-bottom {
119
- top: unset;
120
- width: unset;
121
- }
122
- `
123
-
124
- /**
125
- * This component applies a fading graphical effect to its content if it overflows in any of the sides specified by the props.
126
- *
127
- * This also controls how this overflow is scrolled, introducing a new scrolling technique, which applies arrows to the overflowing side.
128
- */
129
- export const FadingOverflow = (
130
- { children, scroll = 'none', sides, enableHorizontalScrollWithVerticalWheel, className, ...props }: Props,
131
- ) => {
132
- const ref = useRef<HTMLDivElement>(null)
133
- const scrollIntervalId = useRef<number>()
134
- // Add a margin to compensate for rounding inaccuracies
135
- const SCROLL_MARGIN = 2
136
-
137
- useEffect(() => {
138
- if (!ref.current) return
139
- const element = ref.current
140
- const fadeTop = !sides || sides.includes('top')
141
- const fadeRight = !sides || sides.includes('right')
142
- const fadeBottom = !sides || sides.includes('bottom')
143
- const fadeLeft = !sides || sides.includes('left')
144
- const overflow = scroll === 'none' ? 'clip' : 'auto'
145
- element.style.overflowX = (fadeLeft || fadeRight) ? overflow : ''
146
- element.style.overflowY = (fadeTop || fadeBottom) ? overflow : ''
147
- if (scroll === 'arrows' || scroll === 'wheel') element.classList.add('hidden-scroll-bars')
148
-
149
- function stopScrolling() {
150
- clearInterval(scrollIntervalId.current)
151
- }
152
-
153
- function checkOverflow() {
154
- // masking (fading)
155
- const overflowsRight = element.clientWidth < element.scrollWidth && element.scrollLeft < (element.scrollWidth - element.clientWidth)
156
- const overflowsLeft = element.scrollLeft > 0
157
- const overflowsBottom = element.clientHeight < element.scrollHeight
158
- && element.scrollTop < (element.scrollHeight - element.clientHeight - SCROLL_MARGIN)
159
- const overflowsTop = element.scrollTop > 0
160
- const masksToApply: string[] = []
161
- if (overflowsLeft && fadeLeft && overflowsRight && fadeRight) masksToApply.push(masks.horizontal)
162
- else {
163
- if (overflowsRight && fadeRight) masksToApply.push(masks.right)
164
- if (overflowsLeft && fadeLeft) masksToApply.push(masks.left)
165
- }
166
- if (overflowsTop && fadeTop && overflowsBottom && fadeBottom) masksToApply.push(masks.vertical)
167
- else {
168
- if (overflowsTop && fadeTop) masksToApply.push(masks.top)
169
- if (overflowsBottom && fadeBottom) masksToApply.push(masks.bottom)
170
- }
171
- element.style.maskImage = masksToApply.join(', ')
172
-
173
- // arrow buttons
174
- if (scroll !== 'arrows') return
175
-
176
- function startScrolling(side: Side) {
177
- stopScrolling()
178
- const direction = side === 'bottom' || side === 'top' ? 'scrollTop' : 'scrollLeft'
179
- const multiplier = side === 'bottom' || side === 'right' ? 1 : -1
180
- scrollIntervalId.current = window.setInterval(() => {
181
- element[direction] += SCROLL_PX * multiplier
182
- }, SCROLL_INTERVAL_MS)
183
- }
184
-
185
- const startScrollingBySide = {
186
- left: () => startScrolling('left'),
187
- right: () => startScrolling('right'),
188
- top: () => startScrolling('top'),
189
- bottom: () => startScrolling('bottom'),
190
- }
191
-
192
- const scrollToMaxBySide = {
193
- left: () => element.scrollLeft = 0,
194
- right: () => element.scrollLeft = element.scrollWidth - element.clientWidth,
195
- top: () => element.scrollTop = 0,
196
- bottom: () => element.scrollTop = element.scrollHeight - element.clientHeight,
197
- }
198
-
199
- function enableArrowButton(side: Side) {
200
- const button = element.parentNode?.querySelector(`.scroll-to-${side}`)
201
- if (button?.classList.contains('visible')) return
202
- button?.classList.add('visible')
203
- button?.addEventListener('mouseenter', startScrollingBySide[side])
204
- button?.addEventListener('mouseleave', stopScrolling)
205
- button?.addEventListener('click', scrollToMaxBySide[side])
206
- }
207
-
208
- function disableArrowButton(side: Side) {
209
- const button = element.parentNode?.querySelector(`.scroll-to-${side}`)
210
- if (!button?.classList.contains('visible')) return
211
- button?.classList.remove('visible')
212
- stopScrolling()
213
- button?.removeEventListener('mouseenter', startScrollingBySide[side])
214
- button?.removeEventListener('mouseleave', stopScrolling)
215
- button?.removeEventListener('click', scrollToMaxBySide[side])
216
- }
217
-
218
- if (overflowsRight && fadeRight) enableArrowButton('right')
219
- else disableArrowButton('right')
220
- if (overflowsLeft && fadeLeft) enableArrowButton('left')
221
- else disableArrowButton('left')
222
- if (overflowsTop && fadeTop) enableArrowButton('top')
223
- else disableArrowButton('top')
224
- if (overflowsBottom && fadeBottom) enableArrowButton('bottom')
225
- else disableArrowButton('bottom')
226
- }
227
-
228
- const debouncedCheck = debounce(checkOverflow, MIN_CHECK_INTERVAL_MS)
229
- const resizeObserver = new ResizeObserver(debouncedCheck)
230
- resizeObserver.observe(element)
231
- element.addEventListener('scroll', debouncedCheck)
232
-
233
- return () => {
234
- stopScrolling()
235
- resizeObserver.disconnect()
236
- element.removeEventListener('scroll', debouncedCheck)
237
- }
238
- }, [sides, ref.current, scroll])
239
-
240
- useEffect(() => {
241
- if (!enableHorizontalScrollWithVerticalWheel || !ref.current) return
242
- const element = ref.current
243
-
244
- function scrollWithWheel(event: WheelEvent) {
245
- if (event.deltaY) {
246
- element.scrollLeft += event.deltaY
247
- event.preventDefault()
248
- event.stopPropagation()
249
- }
250
- }
251
-
252
- element.addEventListener('wheel', scrollWithWheel)
253
- return () => element.removeEventListener('wheel', scrollWithWheel)
254
- }, [ref.current, enableHorizontalScrollWithVerticalWheel])
255
-
256
- return scroll === 'arrows' ? (
257
- <OverflowBox {...props} className={listToClass(['scroll-arrows', className])}>
258
- <div className="content" ref={ref}>{children}</div>
259
- <div className="scroll-to-left" aria-hidden><IconBox size="xs"><ChevronLeft /></IconBox></div>
260
- <div className="scroll-to-right" aria-hidden><IconBox size="xs"><ChevronRight /></IconBox></div>
261
- <div className="scroll-to-top" aria-hidden><IconBox size="xs"><ChevronUp /></IconBox></div>
262
- <div className="scroll-to-bottom" aria-hidden><IconBox size="xs"><ChevronDown /></IconBox></div>
263
- </OverflowBox>
264
- ) : <OverflowBox {...props} className={className} ref={ref}>{children}</OverflowBox>
265
- }
1
+ import { IconBox } from '@citric/core'
2
+ import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from '@citric/icons'
3
+ import { listToClass, theme, WithStyle } from '@stack-spot/portal-theme'
4
+ import { debounce } from 'lodash'
5
+ import { useEffect, useRef } from 'react'
6
+ import { styled } from 'styled-components'
7
+
8
+ type Side = 'top' | 'right' | 'left' | 'bottom'
9
+ type ScrollType = 'none' | 'wheel' | 'bar' | 'arrows'
10
+
11
+ interface Props extends WithStyle {
12
+ /**
13
+ * How to scroll the content.
14
+ * - none: overflow is hidden. No scrolling.
15
+ * - wheel: the content is solely scrollable through the mouse wheel and keyboard arrows, no scroll bars are rendered.
16
+ * - bar: this is the normal browser scroll. The content will be scrollable through the mouse wheel, keyboard and scrollbar.
17
+ * - arrows: arrows will be placed in the far edges of the sides that must be scrolled. The content is scrolled through the mouse wheel,
18
+ * keyboard and these arrows. Hovering an arrow slowly scrolls the content, clicking the arrow fully scrolls the content in its direction.
19
+ * There are no scrollbars in this scenario.
20
+ * @default 'none'
21
+ */
22
+ scroll?: ScrollType,
23
+ /**
24
+ * If true, a horizontal scroll can be performed by using the vertical mouse wheel.
25
+ * @default false
26
+ */
27
+ enableHorizontalScrollWithVerticalWheel?: boolean,
28
+ /**
29
+ * Which sides to fade when the content overflows.
30
+ *
31
+ * Important: it seems the current solution doesn't support mixing vertical and horizontal scrolls. I'm not sure if this is possible, I
32
+ * believed combining linear gradients would work, but it doesn't.
33
+ */
34
+ sides?: Side[],
35
+ children?: React.ReactNode,
36
+ }
37
+
38
+ // Scrolling can be very fast, this sets a lower limit for us to run listeners without impacting performance.
39
+ const MIN_CHECK_INTERVAL_MS = 20
40
+ // The interval which to scroll the content when a scroll arrow is hovered.
41
+ const SCROLL_INTERVAL_MS = 20
42
+ // This sets the speed of the scroll when the user hovers a scroll arrow.
43
+ const SCROLL_PX = 4
44
+ // Fade masks
45
+ const masks = {
46
+ right: 'linear-gradient(to left, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
47
+ left: 'linear-gradient(to right, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
48
+ top: 'linear-gradient(to bottom, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
49
+ bottom: 'linear-gradient(to top, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
50
+ horizontal: 'linear-gradient(to left, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgb(0, 0, 0) max(70%, calc(100% - 100px)), rgba(0, 0, 0, 0) 100%)',
51
+ vertical: 'linear-gradient(to top, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgb(0, 0, 0) max(70%, calc(100% - 100px)), rgba(0, 0, 0, 0) 100%)',
52
+ }
53
+
54
+ const OverflowBox = styled.div`
55
+ &.hidden-scroll-bars::-webkit-scrollbar, &.scroll-arrows ::-webkit-scrollbar {
56
+ width: 0;
57
+ height: 0;
58
+ }
59
+
60
+ &.scroll-arrows {
61
+ position: relative;
62
+
63
+ > .content {
64
+ width: 100%;
65
+ }
66
+ }
67
+
68
+ .scroll-to-left, .scroll-to-right, .scroll-to-top, .scroll-to-bottom {
69
+ position: absolute;
70
+ top: 0;
71
+ bottom: 0;
72
+ left: 0;
73
+ right: 0;
74
+ width: 30px;
75
+ height: 30px;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ opacity: 0;
80
+ pointer-events: none;
81
+ transition: opacity 0.3s;
82
+
83
+ ${IconBox} {
84
+ background-color: ${theme.color.light[300]};
85
+ border-radius: 50%;
86
+ width: 16px;
87
+ height: 16px;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ }
92
+
93
+ &.visible {
94
+ opacity: 0.6;
95
+ pointer-events: auto;
96
+
97
+ &:hover {
98
+ opacity: 1;
99
+ }
100
+ }
101
+ }
102
+
103
+ .scroll-to-left {
104
+ right: unset;
105
+ height: unset;
106
+ }
107
+
108
+ .scroll-to-right {
109
+ left: unset;
110
+ height: unset;
111
+ }
112
+
113
+ .scroll-to-top {
114
+ bottom: unset;
115
+ width: unset;
116
+ }
117
+
118
+ .scroll-to-bottom {
119
+ top: unset;
120
+ width: unset;
121
+ }
122
+ `
123
+
124
+ /**
125
+ * This component applies a fading graphical effect to its content if it overflows in any of the sides specified by the props.
126
+ *
127
+ * This also controls how this overflow is scrolled, introducing a new scrolling technique, which applies arrows to the overflowing side.
128
+ */
129
+ export const FadingOverflow = (
130
+ { children, scroll = 'none', sides, enableHorizontalScrollWithVerticalWheel, className, ...props }: Props,
131
+ ) => {
132
+ const ref = useRef<HTMLDivElement>(null)
133
+ const scrollIntervalId = useRef<number>()
134
+ // Add a margin to compensate for rounding inaccuracies
135
+ const SCROLL_MARGIN = 2
136
+
137
+ useEffect(() => {
138
+ if (!ref.current) return
139
+ const element = ref.current
140
+ const fadeTop = !sides || sides.includes('top')
141
+ const fadeRight = !sides || sides.includes('right')
142
+ const fadeBottom = !sides || sides.includes('bottom')
143
+ const fadeLeft = !sides || sides.includes('left')
144
+ const overflow = scroll === 'none' ? 'clip' : 'auto'
145
+ element.style.overflowX = (fadeLeft || fadeRight) ? overflow : ''
146
+ element.style.overflowY = (fadeTop || fadeBottom) ? overflow : ''
147
+ if (scroll === 'arrows' || scroll === 'wheel') element.classList.add('hidden-scroll-bars')
148
+
149
+ function stopScrolling() {
150
+ clearInterval(scrollIntervalId.current)
151
+ }
152
+
153
+ function checkOverflow() {
154
+ // masking (fading)
155
+ const overflowsRight = element.clientWidth < element.scrollWidth && element.scrollLeft < (element.scrollWidth - element.clientWidth)
156
+ const overflowsLeft = element.scrollLeft > 0
157
+ const overflowsBottom = element.clientHeight < element.scrollHeight
158
+ && element.scrollTop < (element.scrollHeight - element.clientHeight - SCROLL_MARGIN)
159
+ const overflowsTop = element.scrollTop > 0
160
+ const masksToApply: string[] = []
161
+ if (overflowsLeft && fadeLeft && overflowsRight && fadeRight) masksToApply.push(masks.horizontal)
162
+ else {
163
+ if (overflowsRight && fadeRight) masksToApply.push(masks.right)
164
+ if (overflowsLeft && fadeLeft) masksToApply.push(masks.left)
165
+ }
166
+ if (overflowsTop && fadeTop && overflowsBottom && fadeBottom) masksToApply.push(masks.vertical)
167
+ else {
168
+ if (overflowsTop && fadeTop) masksToApply.push(masks.top)
169
+ if (overflowsBottom && fadeBottom) masksToApply.push(masks.bottom)
170
+ }
171
+ element.style.maskImage = masksToApply.join(', ')
172
+
173
+ // arrow buttons
174
+ if (scroll !== 'arrows') return
175
+
176
+ function startScrolling(side: Side) {
177
+ stopScrolling()
178
+ const direction = side === 'bottom' || side === 'top' ? 'scrollTop' : 'scrollLeft'
179
+ const multiplier = side === 'bottom' || side === 'right' ? 1 : -1
180
+ scrollIntervalId.current = window.setInterval(() => {
181
+ element[direction] += SCROLL_PX * multiplier
182
+ }, SCROLL_INTERVAL_MS)
183
+ }
184
+
185
+ const startScrollingBySide = {
186
+ left: () => startScrolling('left'),
187
+ right: () => startScrolling('right'),
188
+ top: () => startScrolling('top'),
189
+ bottom: () => startScrolling('bottom'),
190
+ }
191
+
192
+ const scrollToMaxBySide = {
193
+ left: () => element.scrollLeft = 0,
194
+ right: () => element.scrollLeft = element.scrollWidth - element.clientWidth,
195
+ top: () => element.scrollTop = 0,
196
+ bottom: () => element.scrollTop = element.scrollHeight - element.clientHeight,
197
+ }
198
+
199
+ function enableArrowButton(side: Side) {
200
+ const button = element.parentNode?.querySelector(`.scroll-to-${side}`)
201
+ if (button?.classList.contains('visible')) return
202
+ button?.classList.add('visible')
203
+ button?.addEventListener('mouseenter', startScrollingBySide[side])
204
+ button?.addEventListener('mouseleave', stopScrolling)
205
+ button?.addEventListener('click', scrollToMaxBySide[side])
206
+ }
207
+
208
+ function disableArrowButton(side: Side) {
209
+ const button = element.parentNode?.querySelector(`.scroll-to-${side}`)
210
+ if (!button?.classList.contains('visible')) return
211
+ button?.classList.remove('visible')
212
+ stopScrolling()
213
+ button?.removeEventListener('mouseenter', startScrollingBySide[side])
214
+ button?.removeEventListener('mouseleave', stopScrolling)
215
+ button?.removeEventListener('click', scrollToMaxBySide[side])
216
+ }
217
+
218
+ if (overflowsRight && fadeRight) enableArrowButton('right')
219
+ else disableArrowButton('right')
220
+ if (overflowsLeft && fadeLeft) enableArrowButton('left')
221
+ else disableArrowButton('left')
222
+ if (overflowsTop && fadeTop) enableArrowButton('top')
223
+ else disableArrowButton('top')
224
+ if (overflowsBottom && fadeBottom) enableArrowButton('bottom')
225
+ else disableArrowButton('bottom')
226
+ }
227
+
228
+ const debouncedCheck = debounce(checkOverflow, MIN_CHECK_INTERVAL_MS)
229
+ const resizeObserver = new ResizeObserver(debouncedCheck)
230
+ resizeObserver.observe(element)
231
+ element.addEventListener('scroll', debouncedCheck)
232
+
233
+ return () => {
234
+ stopScrolling()
235
+ resizeObserver.disconnect()
236
+ element.removeEventListener('scroll', debouncedCheck)
237
+ }
238
+ }, [sides, ref.current, scroll])
239
+
240
+ useEffect(() => {
241
+ if (!enableHorizontalScrollWithVerticalWheel || !ref.current) return
242
+ const element = ref.current
243
+
244
+ function scrollWithWheel(event: WheelEvent) {
245
+ if (event.deltaY) {
246
+ element.scrollLeft += event.deltaY
247
+ event.preventDefault()
248
+ event.stopPropagation()
249
+ }
250
+ }
251
+
252
+ element.addEventListener('wheel', scrollWithWheel)
253
+ return () => element.removeEventListener('wheel', scrollWithWheel)
254
+ }, [ref.current, enableHorizontalScrollWithVerticalWheel])
255
+
256
+ return scroll === 'arrows' ? (
257
+ <OverflowBox {...props} className={listToClass(['scroll-arrows', className])}>
258
+ <div className="content" ref={ref}>{children}</div>
259
+ <div className="scroll-to-left" aria-hidden><IconBox size="xs"><ChevronLeft /></IconBox></div>
260
+ <div className="scroll-to-right" aria-hidden><IconBox size="xs"><ChevronRight /></IconBox></div>
261
+ <div className="scroll-to-top" aria-hidden><IconBox size="xs"><ChevronUp /></IconBox></div>
262
+ <div className="scroll-to-bottom" aria-hidden><IconBox size="xs"><ChevronDown /></IconBox></div>
263
+ </OverflowBox>
264
+ ) : <OverflowBox {...props} className={className} ref={ref}>{children}</OverflowBox>
265
+ }