@stack-spot/citric-react 0.41.2 → 0.42.0-beta.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 (204) hide show
  1. package/CHANGELOG.md +13 -13
  2. package/dist/citric.css +3090 -2846
  3. package/dist/components/Accordion.d.ts +1 -1
  4. package/dist/components/Accordion.js +1 -1
  5. package/dist/components/Alert.d.ts +1 -1
  6. package/dist/components/Alert.js +1 -1
  7. package/dist/components/AsyncContent.d.ts +1 -1
  8. package/dist/components/AsyncContent.js +1 -1
  9. package/dist/components/Autocomplete/Autocomplete.d.ts +211 -0
  10. package/dist/components/Autocomplete/Autocomplete.d.ts.map +1 -0
  11. package/dist/components/Autocomplete/Autocomplete.js +409 -0
  12. package/dist/components/Autocomplete/Autocomplete.js.map +1 -0
  13. package/dist/components/Autocomplete/index.d.ts +3 -0
  14. package/dist/components/Autocomplete/index.d.ts.map +1 -0
  15. package/dist/components/Autocomplete/index.js +2 -0
  16. package/dist/components/Autocomplete/index.js.map +1 -0
  17. package/dist/components/Avatar.d.ts +1 -1
  18. package/dist/components/Avatar.js +1 -1
  19. package/dist/components/AvatarGroup.d.ts +1 -1
  20. package/dist/components/AvatarGroup.js +1 -1
  21. package/dist/components/Badge.d.ts +1 -1
  22. package/dist/components/Badge.js +1 -1
  23. package/dist/components/Blockquote.d.ts +1 -1
  24. package/dist/components/Blockquote.js +1 -1
  25. package/dist/components/Breadcrumb.d.ts +1 -1
  26. package/dist/components/Breadcrumb.js +1 -1
  27. package/dist/components/Button.d.ts +1 -1
  28. package/dist/components/Button.js +1 -1
  29. package/dist/components/ButtonLink.d.ts +1 -1
  30. package/dist/components/ButtonLink.js +1 -1
  31. package/dist/components/Card.d.ts +1 -1
  32. package/dist/components/Card.js +1 -1
  33. package/dist/components/Checkbox.d.ts +1 -1
  34. package/dist/components/Checkbox.d.ts.map +1 -1
  35. package/dist/components/Checkbox.js +2 -2
  36. package/dist/components/Checkbox.js.map +1 -1
  37. package/dist/components/CheckboxGroup.d.ts +1 -1
  38. package/dist/components/CheckboxGroup.js +1 -1
  39. package/dist/components/Circle.d.ts +1 -1
  40. package/dist/components/Circle.js +1 -1
  41. package/dist/components/CitricComponent.d.ts +1 -1
  42. package/dist/components/CitricComponent.d.ts.map +1 -1
  43. package/dist/components/Divider.d.ts +1 -1
  44. package/dist/components/Divider.js +1 -1
  45. package/dist/components/ErrorBoundary.d.ts +1 -1
  46. package/dist/components/ErrorBoundary.js +1 -1
  47. package/dist/components/ErrorMessage.d.ts +1 -1
  48. package/dist/components/ErrorMessage.js +1 -1
  49. package/dist/components/FallbackBoundary.d.ts +1 -1
  50. package/dist/components/FallbackBoundary.js +1 -1
  51. package/dist/components/Favorite.d.ts +1 -1
  52. package/dist/components/Favorite.js +1 -1
  53. package/dist/components/FieldGroup.d.ts +1 -1
  54. package/dist/components/FieldGroup.js +1 -1
  55. package/dist/components/Form.d.ts +2 -2
  56. package/dist/components/Form.js +1 -1
  57. package/dist/components/FormGroup.d.ts +1 -1
  58. package/dist/components/FormGroup.js +1 -1
  59. package/dist/components/Icon.d.ts +1 -1
  60. package/dist/components/Icon.js +1 -1
  61. package/dist/components/IconBox.d.ts +3 -3
  62. package/dist/components/IconBox.js +1 -1
  63. package/dist/components/ImageBox.d.ts +3 -3
  64. package/dist/components/ImageBox.js +1 -1
  65. package/dist/components/ImageWithFallback.d.ts +1 -1
  66. package/dist/components/ImageWithFallback.js +1 -1
  67. package/dist/components/Input.d.ts +1 -1
  68. package/dist/components/Input.js +1 -1
  69. package/dist/components/Link.d.ts +1 -1
  70. package/dist/components/Link.js +1 -1
  71. package/dist/components/LoadingPanel.d.ts +1 -1
  72. package/dist/components/LoadingPanel.js +1 -1
  73. package/dist/components/MenuOverlay/Menu.d.ts +1 -1
  74. package/dist/components/MenuOverlay/Menu.js +1 -1
  75. package/dist/components/MenuOverlay/index.d.ts +1 -1
  76. package/dist/components/MenuOverlay/index.js +1 -1
  77. package/dist/components/Overlay/index.d.ts +1 -1
  78. package/dist/components/Overlay/index.js +1 -1
  79. package/dist/components/Pagination.d.ts +1 -1
  80. package/dist/components/Pagination.js +1 -1
  81. package/dist/components/ProgressBar.d.ts +1 -1
  82. package/dist/components/ProgressBar.js +1 -1
  83. package/dist/components/ProgressCircular.d.ts +1 -1
  84. package/dist/components/ProgressCircular.js +1 -1
  85. package/dist/components/RadioGroup.d.ts +1 -1
  86. package/dist/components/RadioGroup.js +1 -1
  87. package/dist/components/Rating.d.ts +1 -1
  88. package/dist/components/Rating.js +1 -1
  89. package/dist/components/Select/MultiSelect.d.ts +1 -1
  90. package/dist/components/Select/MultiSelect.js +1 -1
  91. package/dist/components/Select/RichSelect.d.ts +1 -1
  92. package/dist/components/Select/RichSelect.js +1 -1
  93. package/dist/components/Select/SimpleSelect.d.ts +1 -1
  94. package/dist/components/Select/SimpleSelect.js +1 -1
  95. package/dist/components/Select/index.d.ts +1 -1
  96. package/dist/components/Select/index.js +1 -1
  97. package/dist/components/SelectBox.d.ts +1 -1
  98. package/dist/components/SelectBox.js +1 -1
  99. package/dist/components/Skeleton.d.ts +1 -1
  100. package/dist/components/Skeleton.js +1 -1
  101. package/dist/components/Slider.d.ts +1 -1
  102. package/dist/components/Slider.js +1 -1
  103. package/dist/components/SmartTable.d.ts +1 -1
  104. package/dist/components/SmartTable.js +1 -1
  105. package/dist/components/Stepper.d.ts +1 -1
  106. package/dist/components/Stepper.js +1 -1
  107. package/dist/components/Table.d.ts +3 -3
  108. package/dist/components/Table.js +1 -1
  109. package/dist/components/Tabs/index.d.ts +1 -1
  110. package/dist/components/Tabs/index.js +1 -1
  111. package/dist/components/Textarea.d.ts +1 -1
  112. package/dist/components/Textarea.js +1 -1
  113. package/dist/components/Tooltip.d.ts +1 -1
  114. package/dist/components/Tooltip.js +1 -1
  115. package/dist/context/CitricProvider.d.ts +1 -1
  116. package/dist/context/CitricProvider.js +1 -1
  117. package/dist/index.d.ts +1 -0
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +1 -0
  120. package/dist/index.js.map +1 -1
  121. package/dist/overlay.js +1 -1
  122. package/dist/theme.css +415 -415
  123. package/package.json +1 -1
  124. package/scripts/build-css.ts +49 -49
  125. package/src/components/Accordion.tsx +130 -130
  126. package/src/components/Alert.tsx +24 -24
  127. package/src/components/AsyncContent.tsx +75 -75
  128. package/src/components/Autocomplete/Autocomplete.tsx +794 -0
  129. package/src/components/Autocomplete/index.ts +3 -0
  130. package/src/components/Avatar.tsx +45 -45
  131. package/src/components/AvatarGroup.tsx +49 -49
  132. package/src/components/Badge.tsx +47 -47
  133. package/src/components/Blockquote.tsx +18 -18
  134. package/src/components/Breadcrumb.tsx +33 -33
  135. package/src/components/Button.tsx +105 -105
  136. package/src/components/ButtonLink.tsx +45 -45
  137. package/src/components/Card.tsx +68 -68
  138. package/src/components/Checkbox.tsx +52 -51
  139. package/src/components/CheckboxGroup.tsx +153 -153
  140. package/src/components/Circle.tsx +43 -43
  141. package/src/components/CitricComponent.ts +47 -47
  142. package/src/components/Divider.tsx +24 -24
  143. package/src/components/ErrorBoundary.tsx +75 -75
  144. package/src/components/ErrorMessage.tsx +11 -11
  145. package/src/components/FallbackBoundary.tsx +40 -40
  146. package/src/components/Favorite.tsx +57 -57
  147. package/src/components/FieldGroup.tsx +46 -46
  148. package/src/components/Form.tsx +36 -36
  149. package/src/components/FormGroup.tsx +57 -57
  150. package/src/components/Icon.tsx +35 -35
  151. package/src/components/IconBox.tsx +134 -134
  152. package/src/components/ImageBox.tsx +125 -125
  153. package/src/components/ImageWithFallback.tsx +65 -65
  154. package/src/components/Input.tsx +49 -49
  155. package/src/components/Link.tsx +55 -55
  156. package/src/components/LoadingPanel.tsx +12 -12
  157. package/src/components/MenuOverlay/Menu.tsx +158 -158
  158. package/src/components/MenuOverlay/context.ts +20 -20
  159. package/src/components/MenuOverlay/index.tsx +55 -55
  160. package/src/components/MenuOverlay/keyboard.ts +60 -60
  161. package/src/components/MenuOverlay/types.ts +171 -171
  162. package/src/components/Overlay/context.ts +10 -10
  163. package/src/components/Overlay/index.tsx +182 -182
  164. package/src/components/Overlay/types.ts +75 -75
  165. package/src/components/Pagination.tsx +133 -133
  166. package/src/components/ProgressBar.tsx +45 -45
  167. package/src/components/ProgressCircular.tsx +45 -45
  168. package/src/components/RadioGroup.tsx +147 -147
  169. package/src/components/Rating.tsx +98 -98
  170. package/src/components/Select/MultiSelect.tsx +217 -217
  171. package/src/components/Select/RichSelect.tsx +128 -128
  172. package/src/components/Select/SimpleSelect.tsx +73 -73
  173. package/src/components/Select/hooks.ts +133 -133
  174. package/src/components/Select/index.tsx +35 -35
  175. package/src/components/Select/types.ts +134 -134
  176. package/src/components/SelectBox.tsx +167 -167
  177. package/src/components/Skeleton.tsx +53 -53
  178. package/src/components/Slider.tsx +89 -89
  179. package/src/components/SmartTable.tsx +227 -227
  180. package/src/components/Stepper.tsx +163 -163
  181. package/src/components/Table.tsx +234 -234
  182. package/src/components/Tabs/TabController.ts +54 -54
  183. package/src/components/Tabs/index.tsx +106 -106
  184. package/src/components/Tabs/types.ts +67 -67
  185. package/src/components/Tabs/utils.ts +6 -6
  186. package/src/components/Text.ts +111 -111
  187. package/src/components/Textarea.tsx +27 -27
  188. package/src/components/Tooltip.tsx +83 -83
  189. package/src/components/layout.tsx +101 -101
  190. package/src/context/CitricContext.tsx +4 -4
  191. package/src/context/CitricProvider.tsx +14 -14
  192. package/src/context/hooks.ts +6 -6
  193. package/src/index.ts +59 -58
  194. package/src/overlay.ts +348 -348
  195. package/src/types.ts +235 -235
  196. package/src/utils/ValueController.ts +28 -28
  197. package/src/utils/acessibility.ts +92 -92
  198. package/src/utils/checkbox.ts +121 -121
  199. package/src/utils/css.ts +119 -119
  200. package/src/utils/options.ts +9 -9
  201. package/src/utils/radio.ts +93 -93
  202. package/src/utils/react.ts +6 -6
  203. package/src/utils/time.ts +5 -5
  204. package/tsconfig.json +10 -10
package/src/overlay.ts CHANGED
@@ -1,348 +1,348 @@
1
- import { createRoot } from 'react-dom/client'
2
- import { HTMLTag, WithDataAttributes } from './types'
3
- import { styleObjectToCssString } from './utils/css'
4
-
5
- export type RelativePosition = 'top' | 'bottom' | 'left' | 'right'
6
-
7
- export interface OverlayOptions<T extends keyof HTMLTag> {
8
- /**
9
- * The tag to render when creating the overlay element.
10
- *
11
- * @default 'div'
12
- */
13
- tag?: T,
14
- /**
15
- * The content of the overlay element.
16
- *
17
- * When this is a string, a number or a boolean, the content will be appended to the overlay element. Otherwise, a new React concurrent
18
- * instance will be created to render the React tree node inside the overlay element.
19
- */
20
- content: React.ReactNode,
21
- /**
22
- * Which element will be used as a reference to position the overlay element.
23
- * This can be either an HTMLElement or an event. If this is a event, the element in `event.target` will be used unless `reference` is
24
- * set to `'mouse'`, in this case, the reference will be the current cursor position.
25
- */
26
- target: Event | HTMLElement,
27
- /**
28
- * Takes effect when `target` is a MouseEvent. If this is set to `'mouse'`, then the position to be used as a reference to place the
29
- * overlay will be the current cursor position.
30
- *
31
- * @default 'element'
32
- */
33
- reference?: 'element' | 'mouse',
34
- /**
35
- * Where, relative to `target`, should we place the overlay?
36
- *
37
- * @default 'top'
38
- */
39
- position?: RelativePosition,
40
- /**
41
- * Affects the positioning of the overlay relative to `target`. While `position` defines the overlay position in the main axis,
42
- * `alignment` defines the position in the cross axis.
43
- *
44
- * @default 'center'
45
- */
46
- alignment?: 'start' | 'center' | 'end',
47
- /**
48
- * TODO: implement this (currently, only 'fade' works).
49
- *
50
- * - fade: the overlay fades in to appear and fades out to disappear.
51
- * - accordion: the element grows to appear and shrinks to disappear. The direction of the growth depends on `position`.
52
- * - bubble: grows from scale(0) to scale(1) from its center to appear. Shrinks from scale(1) to scale(0) to disappear.
53
- * - none: no animation is used.
54
- *
55
- * @default 'fade'
56
- */
57
- animation?: 'fade' | 'accordion' | 'bubble' | 'none',
58
- /**
59
- * The attributes for the HTMLElement that will be created for the overlay.
60
- */
61
- attributes?: JSX.IntrinsicElements[T] & WithDataAttributes & { inert?: boolean },
62
- }
63
-
64
- interface Position {
65
- top: number,
66
- left: number,
67
- }
68
-
69
- interface PositionWithRelativeData extends Position {
70
- relativeTo: RelativePosition,
71
- }
72
-
73
- type CalculatePosOptions = Pick<Required<OverlayOptions<any>>, 'position' | 'target' | 'reference' | 'alignment'>
74
- & { overlay: HTMLElement }
75
-
76
- const animationDurationMS = 300
77
-
78
- function hasMargins(element: HTMLElement) {
79
- const s = element.style
80
- return s.margin || s.marginTop || s.marginBottom || s.marginLeft || s.marginRight
81
- }
82
-
83
- function calculateAlignment(alignment: CalculatePosOptions['alignment'], offset: number, size: number, scroll: number) {
84
- switch (alignment) {
85
- case 'center': return offset + size / 2 + scroll
86
- case 'end': return offset + size + scroll
87
- case 'start': return offset + scroll
88
- }
89
- }
90
-
91
- function calculateAlignmentOffset(alignment: CalculatePosOptions['alignment'], overlaySize: number) {
92
- switch (alignment) {
93
- case 'center': return overlaySize / 2
94
- case 'end': return overlaySize
95
- case 'start': return 0
96
- }
97
- }
98
-
99
- function calculatePosition({
100
- overlay,
101
- reference,
102
- target,
103
- position: relativePosition,
104
- alignment,
105
- }: CalculatePosOptions): Position & { overlayWidth: number, overlayHeight: number } {
106
- const overlayDimensions = overlay.getBoundingClientRect()
107
- if (hasMargins(overlay)) {
108
- const style = overlay.computedStyleMap()
109
- const mt = parseInt(style.get('margin-top')?.toString() ?? '0')
110
- const mb = parseInt(style.get('margin-bottom')?.toString() ?? '0')
111
- const ml = parseInt(style.get('margin-left')?.toString() ?? '0')
112
- const mr = parseInt(style.get('margin-right')?.toString() ?? '0')
113
- if (mt) overlayDimensions.height += mt
114
- if (mb) overlayDimensions.height += mb
115
- if (ml) overlayDimensions.width += ml
116
- if (mr) overlayDimensions.width += mr
117
- }
118
- const referencePosition = { top: 0, left: 0, overlayWidth: overlayDimensions.width, overlayHeight: overlayDimensions.height }
119
- if (reference === 'mouse' && target instanceof MouseEvent) {
120
- referencePosition.top = target.clientY
121
- referencePosition.left = target.clientX
122
- } else {
123
- const element = target instanceof Event ? target.target : target
124
- if (!(element instanceof HTMLElement)) return referencePosition
125
- const elementDimensions = element.getBoundingClientRect()
126
- switch (relativePosition) {
127
- case 'top':
128
- referencePosition.top = elementDimensions.top + window.scrollY
129
- referencePosition.left = calculateAlignment(alignment, elementDimensions.left, elementDimensions.width, window.scrollX)
130
- break
131
- case 'bottom':
132
- referencePosition.top = elementDimensions.bottom + window.scrollY
133
- referencePosition.left = calculateAlignment(alignment, elementDimensions.left, elementDimensions.width, window.scrollX)
134
- break
135
- case 'left':
136
- referencePosition.top = calculateAlignment(alignment, elementDimensions.top, elementDimensions.height, window.scrollY)
137
- referencePosition.left = elementDimensions.left + window.scrollX
138
- break
139
- case 'right':
140
- referencePosition.top = calculateAlignment(alignment, elementDimensions.top, elementDimensions.height, window.scrollY)
141
- referencePosition.left = elementDimensions.right + window.scrollX
142
- }
143
- }
144
- const position = { ...referencePosition }
145
- switch (relativePosition) {
146
- case 'top':
147
- position.top -= overlayDimensions.height
148
- position.left -= calculateAlignmentOffset(alignment, overlayDimensions.width)
149
- break
150
- case 'bottom':
151
- position.left -= calculateAlignmentOffset(alignment, overlayDimensions.width)
152
- break
153
- case 'left':
154
- position.top -= calculateAlignmentOffset(alignment, overlayDimensions.height)
155
- position.left -= overlayDimensions.width
156
- break
157
- case 'right':
158
- position.top -= calculateAlignmentOffset(alignment, overlayDimensions.height)
159
- }
160
- return position
161
- }
162
-
163
- function getSafeOverlayPosition(
164
- options: Omit<CalculatePosOptions, 'position'>, positionPriority: RelativePosition[], fallback?: PositionWithRelativeData,
165
- ): PositionWithRelativeData {
166
- if (!positionPriority.length) return fallback ?? { top: 0, left: 0, relativeTo: 'top' }
167
- const [relativePosition, ...remainingRelativePositions] = positionPriority
168
- const position = { ...calculatePosition({ ...options, position: relativePosition }), relativeTo: relativePosition }
169
- switch (relativePosition) {
170
- case 'top':
171
- if (position.left < 0) position.left = 0
172
- if (position.left + position.overlayWidth > document.body.clientWidth) {
173
- position.left = document.body.clientWidth - position.overlayWidth
174
- }
175
- if (position.top < 0) return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
176
- break
177
- case 'bottom':
178
- if (position.left < 0) position.left = 0
179
- if (position.left + position.overlayWidth > document.body.clientWidth) {
180
- position.left = document.body.clientWidth - position.overlayWidth
181
- }
182
- if (position.top + position.overlayHeight > document.body.clientHeight) {
183
- return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
184
- }
185
- break
186
- case 'left':
187
- if (position.top < 0) position.top = 0
188
- if (position.top + position.overlayHeight > document.body.clientHeight) {
189
- position.top = document.body.clientHeight - position.overlayHeight
190
- }
191
- if (position.left < 0) return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
192
- break
193
- case 'right':
194
- if (position.top < 0) position.top = 0
195
- if (position.top + position.overlayHeight > document.body.clientHeight) {
196
- position.top = document.body.clientHeight - position.overlayHeight
197
- }
198
- if (position.left + position.overlayWidth > document.body.clientWidth) {
199
- return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
200
- }
201
- }
202
- return position
203
- }
204
-
205
- function invert(position: RelativePosition): RelativePosition {
206
- switch (position) {
207
- case 'bottom': return 'top'
208
- case 'top': return 'bottom'
209
- case 'left': return 'right'
210
- case 'right': return 'left'
211
- }
212
- }
213
-
214
- function oppositeAxis(position: RelativePosition): [RelativePosition, RelativePosition] {
215
- return (position === 'top' || position === 'bottom') ? ['left', 'right'] : ['top', 'bottom']
216
- }
217
-
218
- function reactAttributeToHTML(attribute: string) {
219
- return attribute === 'className' ? 'class' : attribute
220
- }
221
-
222
- function setElementAttributes(element: HTMLElement, attributes: Record<string, any> | undefined, ignore: string[] = []) {
223
- for (const attr in attributes) {
224
- if (attributes[attr] !== undefined && !ignore.includes(attr)) element.setAttribute(reactAttributeToHTML(attr), attributes[attr])
225
- }
226
- }
227
-
228
- function getClosestScrollable(element: HTMLElement, limit: HTMLElement = document.body): HTMLElement | null | undefined {
229
- if (element === limit) return
230
- const hasScrollableContent = element.scrollHeight > element.clientHeight
231
- const scrollable = hasScrollableContent && ['auto', 'scroll'].includes(getComputedStyle(element).overflowY)
232
- return scrollable ? element : getClosestScrollable(element.parentElement!, limit)
233
- }
234
-
235
- function isElementVisible(element: HTMLElement, scrollable: HTMLElement) {
236
- const elementRect = element.getBoundingClientRect()
237
- const scrollableRect = scrollable.getBoundingClientRect()
238
- const diffX = elementRect.left - scrollableRect.left + scrollable.scrollLeft
239
- const isVisibleX = diffX + elementRect.width <= scrollable.scrollLeft + scrollableRect.width &&
240
- diffX >= scrollable.scrollLeft
241
- const diffY = elementRect.top - scrollableRect.top + scrollable.scrollTop
242
- const isVisibleY = diffY + elementRect.height <= scrollable.scrollTop + scrollableRect.height &&
243
- diffY >= scrollable.scrollTop
244
- return isVisibleX && isVisibleY
245
- }
246
-
247
- /**
248
- * The tooltip may be inside a scrollable element. If this is the case, we must update its position whenever the container is scrolled.
249
- * If the container is scrolled enough to hide the element that triggered the overlay, we hide the overlay.
250
- */
251
- function attachScrollEffects(target: OverlayOptions<any>['target'], overlay: HTMLElement, hide: () => void) {
252
- const element = target instanceof Event ? target.target as HTMLElement : target
253
- const closestScrollableFromTarget = getClosestScrollable(element, overlay.parentNode as HTMLElement)
254
- if (closestScrollableFromTarget) {
255
- let lastScrollX = closestScrollableFromTarget.scrollLeft
256
- let lastScrollY = closestScrollableFromTarget.scrollTop
257
- const updatePosition = () => {
258
- if (!isElementVisible(element, closestScrollableFromTarget)) return hide()
259
- const diffX = closestScrollableFromTarget.scrollLeft - lastScrollX
260
- const diffY = closestScrollableFromTarget.scrollTop - lastScrollY
261
- overlay.style.left = `${parseInt(overlay.style.left) - diffX}px`
262
- overlay.style.top = `${parseInt(overlay.style.top) - diffY}px`
263
- lastScrollX = closestScrollableFromTarget.scrollLeft
264
- lastScrollY = closestScrollableFromTarget.scrollTop
265
- }
266
- closestScrollableFromTarget.addEventListener('scroll', updatePosition)
267
- return () => closestScrollableFromTarget.removeEventListener('scroll', updatePosition)
268
- }
269
- }
270
-
271
- /**
272
- * Appends a new HTML Element to the tag "body". This element is absolutely positioned and its position is calculated according to the
273
- * options passed as parameter.
274
- *
275
- * This function returns both the newly created HTML Element and a function to remove it.
276
- * @param options {@link OverlayOptions}.
277
- * @returns an object with two keys: "overlay" (the HTML Element created) and "hide" (a function to remove the element from the document).
278
- */
279
- export function showOverlay<T extends keyof HTMLTag = 'div'>(
280
- { tag, content, target, reference = 'element', position = 'top', alignment = 'center', attributes }: OverlayOptions<T>,
281
- ) {
282
- let removeScrollEffects: (() => void) | undefined
283
- const overlay = document.createElement(tag || 'div')
284
- overlay.style = `z-index: 9999; pointer-events: none; position: absolute; opacity: 0; transition: opacity ${animationDurationMS / 1000}s; ${styleObjectToCssString(attributes?.style)}`
285
- overlay.inert = true
286
- setElementAttributes(overlay, attributes, ['style', 'inert'])
287
- document.body.append(overlay)
288
- let unmount: (() => void) | undefined
289
- if (['string', 'number', 'boolean'].includes(typeof content)) {
290
- overlay.append(`${content}`)
291
- unmount = () => document.body.removeChild(overlay)
292
- } else {
293
- const root = createRoot(overlay)
294
- root.render(content)
295
- unmount = () => {
296
- root.unmount()
297
- document.body.removeChild(overlay)
298
- }
299
- }
300
-
301
- const hide = (immediately = false) => new Promise<void>((resolve) => {
302
- overlay.style.opacity = '0'
303
- overlay.style.pointerEvents = 'none'
304
- overlay.inert = true
305
- const action = () => {
306
- try {
307
- removeScrollEffects?.()
308
- unmount()
309
- } catch { /* empty */ }
310
- resolve()
311
- }
312
- if (immediately) {
313
- action()
314
- } else {
315
- setTimeout(action, animationDurationMS)
316
- }
317
- })
318
-
319
- setTimeout(() => {
320
- const overlayPos = getSafeOverlayPosition(
321
- { alignment, overlay, reference, target },
322
- [position, invert(position), ...oppositeAxis(position)],
323
- )
324
- overlay.style = `z-index: 9999; position: absolute; opacity: 1; transition: opacity ${animationDurationMS / 1000}s; top: ${overlayPos.top}px; left: ${overlayPos.left}px; ${styleObjectToCssString(attributes?.style)}`
325
- if (attributes && 'inert' in attributes && attributes.inert) overlay.style.pointerEvents = 'none'
326
- else overlay.inert = false
327
- overlay.classList.add(overlayPos.relativeTo, `align-${alignment}`)
328
- removeScrollEffects = attachScrollEffects(target, overlay, hide)
329
- }, 0)
330
-
331
- return {
332
- /**
333
- * The overlay element created.
334
- */
335
- overlay,
336
- /**
337
- * Removes the overlay element.
338
- * @returns a promise that completes when the element is fully removed (after any animation).
339
- */
340
- hide,
341
- /**
342
- * Returns a promise that resolves as soon as the overlay finishes the animation to show up.
343
- */
344
- ready: new Promise<void>((resolve) => {
345
- setTimeout(resolve, animationDurationMS)
346
- }),
347
- }
348
- }
1
+ import { createRoot } from 'react-dom/client'
2
+ import { HTMLTag, WithDataAttributes } from './types'
3
+ import { styleObjectToCssString } from './utils/css'
4
+
5
+ export type RelativePosition = 'top' | 'bottom' | 'left' | 'right'
6
+
7
+ export interface OverlayOptions<T extends keyof HTMLTag> {
8
+ /**
9
+ * The tag to render when creating the overlay element.
10
+ *
11
+ * @default 'div'
12
+ */
13
+ tag?: T,
14
+ /**
15
+ * The content of the overlay element.
16
+ *
17
+ * When this is a string, a number or a boolean, the content will be appended to the overlay element. Otherwise, a new React concurrent
18
+ * instance will be created to render the React tree node inside the overlay element.
19
+ */
20
+ content: React.ReactNode,
21
+ /**
22
+ * Which element will be used as a reference to position the overlay element.
23
+ * This can be either an HTMLElement or an event. If this is a event, the element in `event.target` will be used unless `reference` is
24
+ * set to `'mouse'`, in this case, the reference will be the current cursor position.
25
+ */
26
+ target: Event | HTMLElement,
27
+ /**
28
+ * Takes effect when `target` is a MouseEvent. If this is set to `'mouse'`, then the position to be used as a reference to place the
29
+ * overlay will be the current cursor position.
30
+ *
31
+ * @default 'element'
32
+ */
33
+ reference?: 'element' | 'mouse',
34
+ /**
35
+ * Where, relative to `target`, should we place the overlay?
36
+ *
37
+ * @default 'top'
38
+ */
39
+ position?: RelativePosition,
40
+ /**
41
+ * Affects the positioning of the overlay relative to `target`. While `position` defines the overlay position in the main axis,
42
+ * `alignment` defines the position in the cross axis.
43
+ *
44
+ * @default 'center'
45
+ */
46
+ alignment?: 'start' | 'center' | 'end',
47
+ /**
48
+ * TODO: implement this (currently, only 'fade' works).
49
+ *
50
+ * - fade: the overlay fades in to appear and fades out to disappear.
51
+ * - accordion: the element grows to appear and shrinks to disappear. The direction of the growth depends on `position`.
52
+ * - bubble: grows from scale(0) to scale(1) from its center to appear. Shrinks from scale(1) to scale(0) to disappear.
53
+ * - none: no animation is used.
54
+ *
55
+ * @default 'fade'
56
+ */
57
+ animation?: 'fade' | 'accordion' | 'bubble' | 'none',
58
+ /**
59
+ * The attributes for the HTMLElement that will be created for the overlay.
60
+ */
61
+ attributes?: JSX.IntrinsicElements[T] & WithDataAttributes & { inert?: boolean },
62
+ }
63
+
64
+ interface Position {
65
+ top: number,
66
+ left: number,
67
+ }
68
+
69
+ interface PositionWithRelativeData extends Position {
70
+ relativeTo: RelativePosition,
71
+ }
72
+
73
+ type CalculatePosOptions = Pick<Required<OverlayOptions<any>>, 'position' | 'target' | 'reference' | 'alignment'>
74
+ & { overlay: HTMLElement }
75
+
76
+ const animationDurationMS = 300
77
+
78
+ function hasMargins(element: HTMLElement) {
79
+ const s = element.style
80
+ return s.margin || s.marginTop || s.marginBottom || s.marginLeft || s.marginRight
81
+ }
82
+
83
+ function calculateAlignment(alignment: CalculatePosOptions['alignment'], offset: number, size: number, scroll: number) {
84
+ switch (alignment) {
85
+ case 'center': return offset + size / 2 + scroll
86
+ case 'end': return offset + size + scroll
87
+ case 'start': return offset + scroll
88
+ }
89
+ }
90
+
91
+ function calculateAlignmentOffset(alignment: CalculatePosOptions['alignment'], overlaySize: number) {
92
+ switch (alignment) {
93
+ case 'center': return overlaySize / 2
94
+ case 'end': return overlaySize
95
+ case 'start': return 0
96
+ }
97
+ }
98
+
99
+ function calculatePosition({
100
+ overlay,
101
+ reference,
102
+ target,
103
+ position: relativePosition,
104
+ alignment,
105
+ }: CalculatePosOptions): Position & { overlayWidth: number, overlayHeight: number } {
106
+ const overlayDimensions = overlay.getBoundingClientRect()
107
+ if (hasMargins(overlay)) {
108
+ const style = overlay.computedStyleMap()
109
+ const mt = parseInt(style.get('margin-top')?.toString() ?? '0')
110
+ const mb = parseInt(style.get('margin-bottom')?.toString() ?? '0')
111
+ const ml = parseInt(style.get('margin-left')?.toString() ?? '0')
112
+ const mr = parseInt(style.get('margin-right')?.toString() ?? '0')
113
+ if (mt) overlayDimensions.height += mt
114
+ if (mb) overlayDimensions.height += mb
115
+ if (ml) overlayDimensions.width += ml
116
+ if (mr) overlayDimensions.width += mr
117
+ }
118
+ const referencePosition = { top: 0, left: 0, overlayWidth: overlayDimensions.width, overlayHeight: overlayDimensions.height }
119
+ if (reference === 'mouse' && target instanceof MouseEvent) {
120
+ referencePosition.top = target.clientY
121
+ referencePosition.left = target.clientX
122
+ } else {
123
+ const element = target instanceof Event ? target.target : target
124
+ if (!(element instanceof HTMLElement)) return referencePosition
125
+ const elementDimensions = element.getBoundingClientRect()
126
+ switch (relativePosition) {
127
+ case 'top':
128
+ referencePosition.top = elementDimensions.top + window.scrollY
129
+ referencePosition.left = calculateAlignment(alignment, elementDimensions.left, elementDimensions.width, window.scrollX)
130
+ break
131
+ case 'bottom':
132
+ referencePosition.top = elementDimensions.bottom + window.scrollY
133
+ referencePosition.left = calculateAlignment(alignment, elementDimensions.left, elementDimensions.width, window.scrollX)
134
+ break
135
+ case 'left':
136
+ referencePosition.top = calculateAlignment(alignment, elementDimensions.top, elementDimensions.height, window.scrollY)
137
+ referencePosition.left = elementDimensions.left + window.scrollX
138
+ break
139
+ case 'right':
140
+ referencePosition.top = calculateAlignment(alignment, elementDimensions.top, elementDimensions.height, window.scrollY)
141
+ referencePosition.left = elementDimensions.right + window.scrollX
142
+ }
143
+ }
144
+ const position = { ...referencePosition }
145
+ switch (relativePosition) {
146
+ case 'top':
147
+ position.top -= overlayDimensions.height
148
+ position.left -= calculateAlignmentOffset(alignment, overlayDimensions.width)
149
+ break
150
+ case 'bottom':
151
+ position.left -= calculateAlignmentOffset(alignment, overlayDimensions.width)
152
+ break
153
+ case 'left':
154
+ position.top -= calculateAlignmentOffset(alignment, overlayDimensions.height)
155
+ position.left -= overlayDimensions.width
156
+ break
157
+ case 'right':
158
+ position.top -= calculateAlignmentOffset(alignment, overlayDimensions.height)
159
+ }
160
+ return position
161
+ }
162
+
163
+ function getSafeOverlayPosition(
164
+ options: Omit<CalculatePosOptions, 'position'>, positionPriority: RelativePosition[], fallback?: PositionWithRelativeData,
165
+ ): PositionWithRelativeData {
166
+ if (!positionPriority.length) return fallback ?? { top: 0, left: 0, relativeTo: 'top' }
167
+ const [relativePosition, ...remainingRelativePositions] = positionPriority
168
+ const position = { ...calculatePosition({ ...options, position: relativePosition }), relativeTo: relativePosition }
169
+ switch (relativePosition) {
170
+ case 'top':
171
+ if (position.left < 0) position.left = 0
172
+ if (position.left + position.overlayWidth > document.body.clientWidth) {
173
+ position.left = document.body.clientWidth - position.overlayWidth
174
+ }
175
+ if (position.top < 0) return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
176
+ break
177
+ case 'bottom':
178
+ if (position.left < 0) position.left = 0
179
+ if (position.left + position.overlayWidth > document.body.clientWidth) {
180
+ position.left = document.body.clientWidth - position.overlayWidth
181
+ }
182
+ if (position.top + position.overlayHeight > document.body.clientHeight) {
183
+ return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
184
+ }
185
+ break
186
+ case 'left':
187
+ if (position.top < 0) position.top = 0
188
+ if (position.top + position.overlayHeight > document.body.clientHeight) {
189
+ position.top = document.body.clientHeight - position.overlayHeight
190
+ }
191
+ if (position.left < 0) return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
192
+ break
193
+ case 'right':
194
+ if (position.top < 0) position.top = 0
195
+ if (position.top + position.overlayHeight > document.body.clientHeight) {
196
+ position.top = document.body.clientHeight - position.overlayHeight
197
+ }
198
+ if (position.left + position.overlayWidth > document.body.clientWidth) {
199
+ return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
200
+ }
201
+ }
202
+ return position
203
+ }
204
+
205
+ function invert(position: RelativePosition): RelativePosition {
206
+ switch (position) {
207
+ case 'bottom': return 'top'
208
+ case 'top': return 'bottom'
209
+ case 'left': return 'right'
210
+ case 'right': return 'left'
211
+ }
212
+ }
213
+
214
+ function oppositeAxis(position: RelativePosition): [RelativePosition, RelativePosition] {
215
+ return (position === 'top' || position === 'bottom') ? ['left', 'right'] : ['top', 'bottom']
216
+ }
217
+
218
+ function reactAttributeToHTML(attribute: string) {
219
+ return attribute === 'className' ? 'class' : attribute
220
+ }
221
+
222
+ function setElementAttributes(element: HTMLElement, attributes: Record<string, any> | undefined, ignore: string[] = []) {
223
+ for (const attr in attributes) {
224
+ if (attributes[attr] !== undefined && !ignore.includes(attr)) element.setAttribute(reactAttributeToHTML(attr), attributes[attr])
225
+ }
226
+ }
227
+
228
+ function getClosestScrollable(element: HTMLElement, limit: HTMLElement = document.body): HTMLElement | null | undefined {
229
+ if (element === limit) return
230
+ const hasScrollableContent = element.scrollHeight > element.clientHeight
231
+ const scrollable = hasScrollableContent && ['auto', 'scroll'].includes(getComputedStyle(element).overflowY)
232
+ return scrollable ? element : getClosestScrollable(element.parentElement!, limit)
233
+ }
234
+
235
+ function isElementVisible(element: HTMLElement, scrollable: HTMLElement) {
236
+ const elementRect = element.getBoundingClientRect()
237
+ const scrollableRect = scrollable.getBoundingClientRect()
238
+ const diffX = elementRect.left - scrollableRect.left + scrollable.scrollLeft
239
+ const isVisibleX = diffX + elementRect.width <= scrollable.scrollLeft + scrollableRect.width &&
240
+ diffX >= scrollable.scrollLeft
241
+ const diffY = elementRect.top - scrollableRect.top + scrollable.scrollTop
242
+ const isVisibleY = diffY + elementRect.height <= scrollable.scrollTop + scrollableRect.height &&
243
+ diffY >= scrollable.scrollTop
244
+ return isVisibleX && isVisibleY
245
+ }
246
+
247
+ /**
248
+ * The tooltip may be inside a scrollable element. If this is the case, we must update its position whenever the container is scrolled.
249
+ * If the container is scrolled enough to hide the element that triggered the overlay, we hide the overlay.
250
+ */
251
+ function attachScrollEffects(target: OverlayOptions<any>['target'], overlay: HTMLElement, hide: () => void) {
252
+ const element = target instanceof Event ? target.target as HTMLElement : target
253
+ const closestScrollableFromTarget = getClosestScrollable(element, overlay.parentNode as HTMLElement)
254
+ if (closestScrollableFromTarget) {
255
+ let lastScrollX = closestScrollableFromTarget.scrollLeft
256
+ let lastScrollY = closestScrollableFromTarget.scrollTop
257
+ const updatePosition = () => {
258
+ if (!isElementVisible(element, closestScrollableFromTarget)) return hide()
259
+ const diffX = closestScrollableFromTarget.scrollLeft - lastScrollX
260
+ const diffY = closestScrollableFromTarget.scrollTop - lastScrollY
261
+ overlay.style.left = `${parseInt(overlay.style.left) - diffX}px`
262
+ overlay.style.top = `${parseInt(overlay.style.top) - diffY}px`
263
+ lastScrollX = closestScrollableFromTarget.scrollLeft
264
+ lastScrollY = closestScrollableFromTarget.scrollTop
265
+ }
266
+ closestScrollableFromTarget.addEventListener('scroll', updatePosition)
267
+ return () => closestScrollableFromTarget.removeEventListener('scroll', updatePosition)
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Appends a new HTML Element to the tag "body". This element is absolutely positioned and its position is calculated according to the
273
+ * options passed as parameter.
274
+ *
275
+ * This function returns both the newly created HTML Element and a function to remove it.
276
+ * @param options {@link OverlayOptions}.
277
+ * @returns an object with two keys: "overlay" (the HTML Element created) and "hide" (a function to remove the element from the document).
278
+ */
279
+ export function showOverlay<T extends keyof HTMLTag = 'div'>(
280
+ { tag, content, target, reference = 'element', position = 'top', alignment = 'center', attributes }: OverlayOptions<T>,
281
+ ) {
282
+ let removeScrollEffects: (() => void) | undefined
283
+ const overlay = document.createElement(tag || 'div')
284
+ overlay.style = `z-index: 9999; pointer-events: none; position: absolute; opacity: 0; transition: opacity ${animationDurationMS / 1000}s; ${styleObjectToCssString(attributes?.style)}`
285
+ overlay.inert = true
286
+ setElementAttributes(overlay, attributes, ['style', 'inert'])
287
+ document.body.append(overlay)
288
+ let unmount: (() => void) | undefined
289
+ if (['string', 'number', 'boolean'].includes(typeof content)) {
290
+ overlay.append(`${content}`)
291
+ unmount = () => document.body.removeChild(overlay)
292
+ } else {
293
+ const root = createRoot(overlay)
294
+ root.render(content)
295
+ unmount = () => {
296
+ root.unmount()
297
+ document.body.removeChild(overlay)
298
+ }
299
+ }
300
+
301
+ const hide = (immediately = false) => new Promise<void>((resolve) => {
302
+ overlay.style.opacity = '0'
303
+ overlay.style.pointerEvents = 'none'
304
+ overlay.inert = true
305
+ const action = () => {
306
+ try {
307
+ removeScrollEffects?.()
308
+ unmount()
309
+ } catch { /* empty */ }
310
+ resolve()
311
+ }
312
+ if (immediately) {
313
+ action()
314
+ } else {
315
+ setTimeout(action, animationDurationMS)
316
+ }
317
+ })
318
+
319
+ setTimeout(() => {
320
+ const overlayPos = getSafeOverlayPosition(
321
+ { alignment, overlay, reference, target },
322
+ [position, invert(position), ...oppositeAxis(position)],
323
+ )
324
+ overlay.style = `z-index: 9999; position: absolute; opacity: 1; transition: opacity ${animationDurationMS / 1000}s; top: ${overlayPos.top}px; left: ${overlayPos.left}px; ${styleObjectToCssString(attributes?.style)}`
325
+ if (attributes && 'inert' in attributes && attributes.inert) overlay.style.pointerEvents = 'none'
326
+ else overlay.inert = false
327
+ overlay.classList.add(overlayPos.relativeTo, `align-${alignment}`)
328
+ removeScrollEffects = attachScrollEffects(target, overlay, hide)
329
+ }, 0)
330
+
331
+ return {
332
+ /**
333
+ * The overlay element created.
334
+ */
335
+ overlay,
336
+ /**
337
+ * Removes the overlay element.
338
+ * @returns a promise that completes when the element is fully removed (after any animation).
339
+ */
340
+ hide,
341
+ /**
342
+ * Returns a promise that resolves as soon as the overlay finishes the animation to show up.
343
+ */
344
+ ready: new Promise<void>((resolve) => {
345
+ setTimeout(resolve, animationDurationMS)
346
+ }),
347
+ }
348
+ }