@stack-spot/portal-components 2.27.1 → 2.27.3

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 (247) hide show
  1. package/CHANGELOG.md +642 -628
  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 +62 -62
  63. package/dist/components/SelectionList.js.map +1 -1
  64. package/dist/components/StatusCircle.d.ts +1 -1
  65. package/dist/components/StatusCircle.js +6 -6
  66. package/dist/components/Stepper/Navigation.js +4 -4
  67. package/dist/components/Stepper/Step.js +3 -3
  68. package/dist/components/Stepper/Stepper.js +6 -6
  69. package/dist/components/Stepper/headers.js +22 -22
  70. package/dist/components/Table/HeaderItem.js +1 -1
  71. package/dist/components/Table/SettingsVerticalMenu.d.ts +1 -1
  72. package/dist/components/Table/SettingsVerticalMenu.js +1 -1
  73. package/dist/components/Table/StyledLinkTable.d.ts +1 -1
  74. package/dist/components/Table/StyledLinkTable.js +5 -5
  75. package/dist/components/Table/TableData.d.ts +1 -1
  76. package/dist/components/Table/TableData.js +25 -25
  77. package/dist/components/TimelineSection.d.ts +1 -1
  78. package/dist/components/TimelineSection.js +14 -14
  79. package/dist/components/error/ErrorFeedback.d.ts +1 -1
  80. package/dist/components/error/ErrorFeedback.js +35 -35
  81. package/dist/components/error/NotFound.d.ts +1 -1
  82. package/dist/components/error/NotFound.js +1 -1
  83. package/dist/components/error/UnderMaintenance.d.ts +1 -1
  84. package/dist/components/error/UnderMaintenance.js +1 -1
  85. package/dist/components/form/Form/Form.d.ts +1 -1
  86. package/dist/components/form/Form/Form.js +1 -1
  87. package/dist/components/form/Form/FormGroup.d.ts +2 -2
  88. package/dist/components/form/Form/FormGroup.js +1 -1
  89. package/dist/components/form/SearchInput.d.ts +1 -1
  90. package/dist/components/form/SearchInput.js +1 -1
  91. package/dist/components/form/Select/CustomSelect.d.ts +1 -1
  92. package/dist/components/form/Select/CustomSelect.js +1 -1
  93. package/dist/components/form/Select/DetailedSelect.d.ts +1 -1
  94. package/dist/components/form/Select/DetailedSelect.js +1 -1
  95. package/dist/components/form/Select/Select.d.ts +1 -1
  96. package/dist/components/form/Select/Select.js +1 -1
  97. package/dist/components/form/Select/styled.js +161 -161
  98. package/dist/components/form/Select/utils.js +1 -1
  99. package/dist/components/notification/NotificationComponent.d.ts +1 -1
  100. package/dist/components/notification/NotificationComponent.js +54 -54
  101. package/dist/components/notification/NotificationItem.d.ts +1 -1
  102. package/dist/components/notification/NotificationItem.d.ts.map +1 -1
  103. package/dist/components/notification/NotificationItem.js +8 -2
  104. package/dist/components/notification/NotificationItem.js.map +1 -1
  105. package/dist/components/notification/NotificationList.d.ts +1 -1
  106. package/dist/components/notification/NotificationList.d.ts.map +1 -1
  107. package/dist/components/notification/NotificationList.js +44 -44
  108. package/dist/components/notification/NotificationList.js.map +1 -1
  109. package/dist/components/notification/NotificationPlaceholder.d.ts +1 -1
  110. package/dist/components/notification/NotificationPlaceholder.d.ts.map +1 -1
  111. package/dist/components/notification/NotificationPlaceholder.js +2 -2
  112. package/dist/components/notification/NotificationPlaceholder.js.map +1 -1
  113. package/dist/containers/NotificationsPage.d.ts +1 -1
  114. package/dist/containers/NotificationsPage.js +10 -10
  115. package/dist/context/anchor.d.ts +1 -1
  116. package/dist/context/anchor.js +1 -1
  117. package/dist/context/loading.d.ts +1 -1
  118. package/dist/context/loading.js +1 -1
  119. package/dist/context/notification/context.d.ts +1 -1
  120. package/dist/context/notification/context.js +1 -1
  121. package/dist/hooks/date.js +1 -1
  122. package/dist/hooks/service-now.js +28 -28
  123. package/dist/svg/AI.d.ts +1 -1
  124. package/dist/svg/AI.js +1 -1
  125. package/dist/svg/CS.d.ts +1 -1
  126. package/dist/svg/CS.js +1 -1
  127. package/dist/svg/EDP.d.ts +1 -1
  128. package/dist/svg/EDP.js +1 -1
  129. package/dist/svg/Forbidden.d.ts +1 -1
  130. package/dist/svg/Forbidden.js +1 -1
  131. package/dist/svg/GenericPlaceholder.d.ts +4 -2
  132. package/dist/svg/GenericPlaceholder.d.ts.map +1 -1
  133. package/dist/svg/GenericPlaceholder.js +2 -2
  134. package/dist/svg/GenericPlaceholder.js.map +1 -1
  135. package/dist/svg/HUB.d.ts +1 -1
  136. package/dist/svg/HUB.js +1 -1
  137. package/dist/svg/Logo.d.ts +1 -1
  138. package/dist/svg/Logo.js +1 -1
  139. package/dist/svg/MiniLogo.d.ts +1 -1
  140. package/dist/svg/MiniLogo.js +1 -1
  141. package/dist/svg/NotFound.d.ts +1 -1
  142. package/dist/svg/NotFound.js +1 -1
  143. package/dist/svg/ServerError.d.ts +1 -1
  144. package/dist/svg/ServerError.js +1 -1
  145. package/dist/svg/Unauthenticated.d.ts +1 -1
  146. package/dist/svg/Unauthenticated.js +1 -1
  147. package/package.json +80 -80
  148. package/readme.md +66 -66
  149. package/src/components/AnimatedHeight.tsx +174 -174
  150. package/src/components/AsyncContent.tsx +78 -78
  151. package/src/components/BannerWarning.tsx +91 -91
  152. package/src/components/Breadcrumb/index.tsx +76 -76
  153. package/src/components/Breadcrumb/styled.ts +37 -37
  154. package/src/components/ButtonLoading.tsx +29 -29
  155. package/src/components/ChatBot.tsx +82 -82
  156. package/src/components/ContentValidateFilter.tsx +15 -15
  157. package/src/components/FadingOverflow.tsx +265 -265
  158. package/src/components/FileTreeView/More.tsx +114 -114
  159. package/src/components/FileTreeView/index.tsx +186 -186
  160. package/src/components/InfiniteScroll.tsx +24 -24
  161. package/src/components/InfoMaintenanceBanner.tsx +29 -29
  162. package/src/components/LazyMarkdown/BlockquoteMd.tsx +107 -107
  163. package/src/components/LazyMarkdown/CodeViewer.tsx +161 -161
  164. package/src/components/LazyMarkdown/Markdown.tsx +122 -122
  165. package/src/components/LazyMarkdown/MarkdownButton.tsx +24 -24
  166. package/src/components/LazyMarkdown/Video.tsx +13 -13
  167. package/src/components/LazyMarkdown/index.tsx +21 -21
  168. package/src/components/Placeholder.tsx +123 -118
  169. package/src/components/ScrollView.tsx +57 -57
  170. package/src/components/Select/BadgeItem.tsx +58 -58
  171. package/src/components/Select/ClearInput.tsx +24 -24
  172. package/src/components/Select/CloseItem.tsx +38 -38
  173. package/src/components/Select/CreatableSelect.tsx +155 -155
  174. package/src/components/Select/CustomMenu.tsx +16 -16
  175. package/src/components/Select/LabelItem.tsx +8 -8
  176. package/src/components/Select/MultiValue.tsx +49 -49
  177. package/src/components/Select/SelectInfiniteScroll.tsx +82 -82
  178. package/src/components/Select/SelectSearch.tsx +195 -195
  179. package/src/components/Select/index.tsx +7 -7
  180. package/src/components/Select/types.ts +8 -8
  181. package/src/components/SelectionList.tsx +427 -427
  182. package/src/components/StatusCircle.tsx +67 -67
  183. package/src/components/Stepper/Navigation.tsx +97 -97
  184. package/src/components/Stepper/Step.tsx +30 -30
  185. package/src/components/Stepper/Stepper.tsx +113 -113
  186. package/src/components/Stepper/headers.tsx +64 -64
  187. package/src/components/Stepper/index.ts +3 -3
  188. package/src/components/Table/HeaderItem.tsx +52 -52
  189. package/src/components/Table/SettingsVerticalMenu.tsx +50 -50
  190. package/src/components/Table/StyledLinkTable.tsx +22 -22
  191. package/src/components/Table/TableData.tsx +251 -251
  192. package/src/components/Table/index.tsx +2 -2
  193. package/src/components/TimelineSection.tsx +66 -66
  194. package/src/components/error/ErrorFeedback.tsx +217 -217
  195. package/src/components/error/NotFound.tsx +24 -24
  196. package/src/components/error/UnderMaintenance.tsx +30 -30
  197. package/src/components/error/index.ts +4 -4
  198. package/src/components/form/Form/Form.tsx +101 -101
  199. package/src/components/form/Form/FormGroup.tsx +221 -221
  200. package/src/components/form/Form/index.ts +2 -2
  201. package/src/components/form/SearchInput.tsx +69 -69
  202. package/src/components/form/Select/CustomSelect.tsx +232 -232
  203. package/src/components/form/Select/DetailedSelect.tsx +85 -85
  204. package/src/components/form/Select/Select.tsx +67 -67
  205. package/src/components/form/Select/index.ts +4 -4
  206. package/src/components/form/Select/styled.ts +165 -165
  207. package/src/components/form/Select/types.ts +112 -112
  208. package/src/components/form/Select/utils.tsx +28 -28
  209. package/src/components/notification/NotificationComponent.tsx +340 -340
  210. package/src/components/notification/NotificationItem.tsx +345 -337
  211. package/src/components/notification/NotificationList.tsx +179 -178
  212. package/src/components/notification/NotificationPlaceholder.tsx +44 -43
  213. package/src/components/notification/types.ts +72 -72
  214. package/src/containers/NotificationsPage.tsx +119 -119
  215. package/src/context/anchor.tsx +37 -37
  216. package/src/context/loading.tsx +36 -36
  217. package/src/context/notification/LazyNotificationList.ts +103 -103
  218. package/src/context/notification/NotificationController.ts +104 -104
  219. package/src/context/notification/context.tsx +23 -23
  220. package/src/context/notification/hooks.ts +98 -98
  221. package/src/context/notification/types.ts +66 -66
  222. package/src/hooks/date.ts +31 -31
  223. package/src/hooks/keyboard.tsx +128 -128
  224. package/src/hooks/manual-render.tsx +10 -10
  225. package/src/hooks/service-now.tsx +233 -233
  226. package/src/hooks/text.tsx +30 -30
  227. package/src/hooks/title.tsx +28 -28
  228. package/src/hooks/use-effect-once.tsx +43 -43
  229. package/src/index.ts +19 -19
  230. package/src/notifications.ts +11 -11
  231. package/src/svg/AI.tsx +41 -41
  232. package/src/svg/CS.tsx +48 -48
  233. package/src/svg/EDP.tsx +31 -31
  234. package/src/svg/Forbidden.tsx +22 -22
  235. package/src/svg/GenericPlaceholder.tsx +20 -20
  236. package/src/svg/HUB.tsx +48 -48
  237. package/src/svg/Logo.tsx +16 -16
  238. package/src/svg/MiniLogo.tsx +12 -12
  239. package/src/svg/NotFound.tsx +16 -16
  240. package/src/svg/ServerError.tsx +33 -33
  241. package/src/svg/Unauthenticated.tsx +16 -16
  242. package/src/svg/index.ts +11 -11
  243. package/src/utils/accessibility.ts +135 -135
  244. package/src/utils/cookie.ts +73 -73
  245. package/src/utils/promise.ts +5 -5
  246. package/src/utils/read-file.ts +16 -16
  247. 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
+ }