@stack-spot/citric-react 0.27.2 → 0.29.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.
- package/dist/citric.css +39 -10
- package/dist/components/AsyncContent.d.ts +1 -0
- package/dist/components/AsyncContent.d.ts.map +1 -1
- package/dist/components/AsyncContent.js +1 -0
- package/dist/components/AsyncContent.js.map +1 -1
- package/dist/components/Button.d.ts +5 -5
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +3 -3
- package/dist/components/Button.js.map +1 -1
- package/dist/components/ButtonLink.d.ts +2 -2
- package/dist/components/ButtonLink.d.ts.map +1 -1
- package/dist/components/ButtonLink.js +3 -3
- package/dist/components/ButtonLink.js.map +1 -1
- package/dist/components/IconBox.d.ts +6 -6
- package/dist/components/IconBox.d.ts.map +1 -1
- package/dist/components/IconBox.js +5 -5
- package/dist/components/IconBox.js.map +1 -1
- package/dist/components/ImageBox.d.ts +8 -6
- package/dist/components/ImageBox.d.ts.map +1 -1
- package/dist/components/ImageBox.js +7 -5
- package/dist/components/ImageBox.js.map +1 -1
- package/dist/components/Link.d.ts +5 -5
- package/dist/components/Link.d.ts.map +1 -1
- package/dist/components/Link.js +3 -3
- package/dist/components/Link.js.map +1 -1
- package/dist/components/Overlay/index.d.ts +1 -1
- package/dist/components/Overlay/index.d.ts.map +1 -1
- package/dist/components/Overlay/index.js +5 -4
- package/dist/components/Overlay/index.js.map +1 -1
- package/dist/components/Select/MultiSelect.d.ts.map +1 -1
- package/dist/components/Select/MultiSelect.js +1 -3
- package/dist/components/Select/MultiSelect.js.map +1 -1
- package/dist/components/Select/RichSelect.d.ts.map +1 -1
- package/dist/components/Select/RichSelect.js +1 -3
- package/dist/components/Select/RichSelect.js.map +1 -1
- package/dist/overlay.d.ts +1 -3
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +82 -24
- package/dist/overlay.js.map +1 -1
- package/dist/types.d.ts +4 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AsyncContent.tsx +1 -0
- package/src/components/Button.tsx +6 -6
- package/src/components/ButtonLink.tsx +3 -3
- package/src/components/IconBox.tsx +8 -8
- package/src/components/ImageBox.tsx +10 -8
- package/src/components/Link.tsx +6 -6
- package/src/components/Overlay/index.tsx +5 -3
- package/src/components/Select/MultiSelect.tsx +1 -2
- package/src/components/Select/RichSelect.tsx +1 -2
- package/src/overlay.ts +87 -25
- package/src/types.ts +4 -4
package/src/components/Link.tsx
CHANGED
|
@@ -11,14 +11,14 @@ export interface BaseLinkProps extends WithColor {
|
|
|
11
11
|
*/
|
|
12
12
|
appearance?: TextAppearance,
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Metadata for the general onClick event, set by the CitricController. Useful for creating analytics data.
|
|
15
15
|
*
|
|
16
|
-
* This only takes effect if there's a CitricController in React's context. The value of `
|
|
16
|
+
* This only takes effect if there's a CitricController in React's context. The value of `metadata` is passed to the function
|
|
17
17
|
* `onClickLink` of the controller.
|
|
18
18
|
*
|
|
19
19
|
* @default false
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
metadata?: any,
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export type LinkProps = React.JSX.IntrinsicElements['a'] & BaseLinkProps
|
|
@@ -27,9 +27,9 @@ export type LinkProps = React.JSX.IntrinsicElements['a'] & BaseLinkProps
|
|
|
27
27
|
* Renders an html anchor by default, the actual component to render may be set on a CitricController, through the function `renderLink`.
|
|
28
28
|
*
|
|
29
29
|
* Whenever a link is clicked, the function `onClickLink` of the nearest CitricController is called with the event and the value of the
|
|
30
|
-
* prop `
|
|
30
|
+
* prop `metadata`.
|
|
31
31
|
*/
|
|
32
|
-
export const Link = withRef(({ appearance, color, style, className, children, onClick,
|
|
32
|
+
export const Link = withRef(({ appearance, color, style, className, children, onClick, metadata, ...props }: LinkProps) => {
|
|
33
33
|
const citric = useCitricController()
|
|
34
34
|
const linkProps = {
|
|
35
35
|
component: 'link',
|
|
@@ -37,7 +37,7 @@ export const Link = withRef(({ appearance, color, style, className, children, on
|
|
|
37
37
|
className: applyTextAppearance(className, appearance),
|
|
38
38
|
onClick: (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
39
39
|
onClick?.(e)
|
|
40
|
-
citric?.onClickLink?.(e,
|
|
40
|
+
citric?.onClickLink?.(e, metadata ?? false)
|
|
41
41
|
},
|
|
42
42
|
...props,
|
|
43
43
|
} as const
|
|
@@ -44,6 +44,7 @@ export function Overlay<T extends keyof HTMLTag>({
|
|
|
44
44
|
content,
|
|
45
45
|
position = 'top',
|
|
46
46
|
triggerOn = 'hover',
|
|
47
|
+
alignment = 'center',
|
|
47
48
|
attributes,
|
|
48
49
|
onRenderChild,
|
|
49
50
|
autoFocusBehavior = 'keyboard',
|
|
@@ -53,11 +54,11 @@ export function Overlay<T extends keyof HTMLTag>({
|
|
|
53
54
|
const controller = useRef<OverlayController>({ close: () => Promise.resolve() })
|
|
54
55
|
const wrapper = useRef<HTMLDivElement | null>(null)
|
|
55
56
|
// props that don't require removing and reattaching the event listeners
|
|
56
|
-
const dynamic = useRef({ tag, content, position, attributes })
|
|
57
|
+
const dynamic = useRef({ tag, content, position, alignment, attributes })
|
|
57
58
|
|
|
58
59
|
useEffect(() => {
|
|
59
|
-
dynamic.current = { tag, content, position, attributes }
|
|
60
|
-
}, [tag, content, position, attributes])
|
|
60
|
+
dynamic.current = { tag, content, position, alignment, attributes }
|
|
61
|
+
}, [tag, content, position, alignment, attributes])
|
|
61
62
|
|
|
62
63
|
useEffect(() => {
|
|
63
64
|
let visible = false
|
|
@@ -88,6 +89,7 @@ export function Overlay<T extends keyof HTMLTag>({
|
|
|
88
89
|
: <OverlayProvider value={controller.current}>{dynamic.current.content}</OverlayProvider>,
|
|
89
90
|
target,
|
|
90
91
|
position: dynamic.current.position,
|
|
92
|
+
alignment: dynamic.current.alignment,
|
|
91
93
|
attributes: dynamic.current.attributes,
|
|
92
94
|
})
|
|
93
95
|
hideOverlay = hideFn
|
package/src/overlay.ts
CHANGED
|
@@ -38,8 +38,6 @@ export interface OverlayOptions<T extends keyof HTMLTag> {
|
|
|
38
38
|
*/
|
|
39
39
|
position?: RelativePosition,
|
|
40
40
|
/**
|
|
41
|
-
* TODO: implement this.
|
|
42
|
-
*
|
|
43
41
|
* Affects the positioning of the overlay relative to `target`. While `position` defines the overlay position in the main axis,
|
|
44
42
|
* `alignment` defines the position in the cross axis.
|
|
45
43
|
*
|
|
@@ -72,7 +70,7 @@ interface PositionWithRelativeData extends Position {
|
|
|
72
70
|
relativeTo: RelativePosition,
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
type CalculatePosOptions = Pick<Required<OverlayOptions<any>>, 'position' | 'target' | 'reference'>
|
|
73
|
+
type CalculatePosOptions = Pick<Required<OverlayOptions<any>>, 'position' | 'target' | 'reference' | 'alignment'>
|
|
76
74
|
& { overlay: HTMLElement }
|
|
77
75
|
|
|
78
76
|
const animationDurationMS = 300
|
|
@@ -82,11 +80,28 @@ function hasMargins(element: HTMLElement) {
|
|
|
82
80
|
return s.margin || s.marginTop || s.marginBottom || s.marginLeft || s.marginRight
|
|
83
81
|
}
|
|
84
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
|
+
|
|
85
99
|
function calculatePosition({
|
|
86
100
|
overlay,
|
|
87
101
|
reference,
|
|
88
102
|
target,
|
|
89
103
|
position: relativePosition,
|
|
104
|
+
alignment,
|
|
90
105
|
}: CalculatePosOptions): Position & { overlayWidth: number, overlayHeight: number } {
|
|
91
106
|
const overlayDimensions = overlay.getBoundingClientRect()
|
|
92
107
|
if (hasMargins(overlay)) {
|
|
@@ -111,18 +126,18 @@ function calculatePosition({
|
|
|
111
126
|
switch (relativePosition) {
|
|
112
127
|
case 'top':
|
|
113
128
|
referencePosition.top = elementDimensions.top + window.scrollY
|
|
114
|
-
referencePosition.left = elementDimensions.left
|
|
129
|
+
referencePosition.left = calculateAlignment(alignment, elementDimensions.left, elementDimensions.width, window.scrollX)
|
|
115
130
|
break
|
|
116
131
|
case 'bottom':
|
|
117
132
|
referencePosition.top = elementDimensions.bottom + window.scrollY
|
|
118
|
-
referencePosition.left = elementDimensions.left
|
|
133
|
+
referencePosition.left = calculateAlignment(alignment, elementDimensions.left, elementDimensions.width, window.scrollX)
|
|
119
134
|
break
|
|
120
135
|
case 'left':
|
|
121
|
-
referencePosition.top = elementDimensions.top
|
|
136
|
+
referencePosition.top = calculateAlignment(alignment, elementDimensions.top, elementDimensions.height, window.scrollY)
|
|
122
137
|
referencePosition.left = elementDimensions.left + window.scrollX
|
|
123
138
|
break
|
|
124
139
|
case 'right':
|
|
125
|
-
referencePosition.top = elementDimensions.top
|
|
140
|
+
referencePosition.top = calculateAlignment(alignment, elementDimensions.top, elementDimensions.height, window.scrollY)
|
|
126
141
|
referencePosition.left = elementDimensions.right + window.scrollX
|
|
127
142
|
}
|
|
128
143
|
}
|
|
@@ -130,17 +145,17 @@ function calculatePosition({
|
|
|
130
145
|
switch (relativePosition) {
|
|
131
146
|
case 'top':
|
|
132
147
|
position.top -= overlayDimensions.height
|
|
133
|
-
position.left -= overlayDimensions.width
|
|
148
|
+
position.left -= calculateAlignmentOffset(alignment, overlayDimensions.width)
|
|
134
149
|
break
|
|
135
150
|
case 'bottom':
|
|
136
|
-
position.left -= overlayDimensions.width
|
|
151
|
+
position.left -= calculateAlignmentOffset(alignment, overlayDimensions.width)
|
|
137
152
|
break
|
|
138
153
|
case 'left':
|
|
139
|
-
position.top -= overlayDimensions.height
|
|
154
|
+
position.top -= calculateAlignmentOffset(alignment, overlayDimensions.height)
|
|
140
155
|
position.left -= overlayDimensions.width
|
|
141
156
|
break
|
|
142
157
|
case 'right':
|
|
143
|
-
position.top -= overlayDimensions.height
|
|
158
|
+
position.top -= calculateAlignmentOffset(alignment, overlayDimensions.height)
|
|
144
159
|
}
|
|
145
160
|
return position
|
|
146
161
|
}
|
|
@@ -210,6 +225,47 @@ function setElementAttributes(element: HTMLElement, attributes: Record<string, a
|
|
|
210
225
|
}
|
|
211
226
|
}
|
|
212
227
|
|
|
228
|
+
function getClosestScrollable(element: HTMLElement, limit: HTMLElement = document.body): HTMLElement | null | undefined {
|
|
229
|
+
if (element === limit) return
|
|
230
|
+
return element.clientHeight === element.scrollHeight ? getClosestScrollable(element.parentElement!, limit) : element
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isElementVisible(element: HTMLElement, scrollable: HTMLElement) {
|
|
234
|
+
const elementRect = element.getBoundingClientRect()
|
|
235
|
+
const scrollableRect = scrollable.getBoundingClientRect()
|
|
236
|
+
const diffX = elementRect.left - scrollableRect.left + scrollable.scrollLeft
|
|
237
|
+
const isVisibleX = diffX + elementRect.width <= scrollable.scrollLeft + scrollableRect.width &&
|
|
238
|
+
diffX >= scrollable.scrollLeft
|
|
239
|
+
const diffY = elementRect.top - scrollableRect.top + scrollable.scrollTop
|
|
240
|
+
const isVisibleY = diffY + elementRect.height <= scrollable.scrollTop + scrollableRect.height &&
|
|
241
|
+
diffY >= scrollable.scrollTop
|
|
242
|
+
return isVisibleX && isVisibleY
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The tooltip may be inside a scrollable element. If this is the case, we must update its position whenever the container is scrolled.
|
|
247
|
+
* If the container is scrolled enough to hide the element that triggered the overlay, we hide the overlay.
|
|
248
|
+
*/
|
|
249
|
+
function attachScrollEffects(target: OverlayOptions<any>['target'], overlay: HTMLElement, hide: () => void) {
|
|
250
|
+
const element = target instanceof Event ? target.target as HTMLElement : target
|
|
251
|
+
const closestScrollableFromTarget = getClosestScrollable(element, overlay.parentNode as HTMLElement)
|
|
252
|
+
if (closestScrollableFromTarget) {
|
|
253
|
+
let lastScrollX = closestScrollableFromTarget.scrollLeft
|
|
254
|
+
let lastScrollY = closestScrollableFromTarget.scrollTop
|
|
255
|
+
const updatePosition = () => {
|
|
256
|
+
if (!isElementVisible(element, closestScrollableFromTarget)) return hide()
|
|
257
|
+
const diffX = closestScrollableFromTarget.scrollLeft - lastScrollX
|
|
258
|
+
const diffY = closestScrollableFromTarget.scrollTop - lastScrollY
|
|
259
|
+
overlay.style.left = `${parseInt(overlay.style.left) - diffX}px`
|
|
260
|
+
overlay.style.top = `${parseInt(overlay.style.top) - diffY}px`
|
|
261
|
+
lastScrollX = closestScrollableFromTarget.scrollLeft
|
|
262
|
+
lastScrollY = closestScrollableFromTarget.scrollTop
|
|
263
|
+
}
|
|
264
|
+
closestScrollableFromTarget.addEventListener('scroll', updatePosition)
|
|
265
|
+
return () => closestScrollableFromTarget.removeEventListener('scroll', updatePosition)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
213
269
|
/**
|
|
214
270
|
* Appends a new HTML Element to the tag "body". This element is absolutely positioned and its position is calculated according to the
|
|
215
271
|
* options passed as parameter.
|
|
@@ -219,8 +275,9 @@ function setElementAttributes(element: HTMLElement, attributes: Record<string, a
|
|
|
219
275
|
* @returns an object with two keys: "overlay" (the HTML Element created) and "hide" (a function to remove the element from the document).
|
|
220
276
|
*/
|
|
221
277
|
export function showOverlay<T extends keyof HTMLTag = 'div'>(
|
|
222
|
-
{ tag, content, target, reference = 'element', position = 'top', attributes }: OverlayOptions<T>,
|
|
278
|
+
{ tag, content, target, reference = 'element', position = 'top', alignment = 'center', attributes }: OverlayOptions<T>,
|
|
223
279
|
) {
|
|
280
|
+
let removeScrollEffects: (() => void) | undefined
|
|
224
281
|
const overlay = document.createElement(tag || 'div')
|
|
225
282
|
overlay.style = `z-index: 9999; pointer-events: none; position: absolute; opacity: 0; transition: opacity ${animationDurationMS / 1000}s; ${styleObjectToCssString(attributes?.style)}`
|
|
226
283
|
overlay.inert = true
|
|
@@ -238,15 +295,30 @@ export function showOverlay<T extends keyof HTMLTag = 'div'>(
|
|
|
238
295
|
document.body.removeChild(overlay)
|
|
239
296
|
}
|
|
240
297
|
}
|
|
298
|
+
|
|
299
|
+
const hide = () => new Promise<void>((resolve) => {
|
|
300
|
+
overlay.style.opacity = '0'
|
|
301
|
+
overlay.style.pointerEvents = 'none'
|
|
302
|
+
overlay.inert = true
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
try {
|
|
305
|
+
removeScrollEffects?.()
|
|
306
|
+
unmount()
|
|
307
|
+
} catch { /* empty */ }
|
|
308
|
+
resolve()
|
|
309
|
+
}, animationDurationMS)
|
|
310
|
+
})
|
|
311
|
+
|
|
241
312
|
setTimeout(() => {
|
|
242
313
|
const overlayPos = getSafeOverlayPosition(
|
|
243
|
-
{ overlay, reference, target },
|
|
314
|
+
{ alignment, overlay, reference, target },
|
|
244
315
|
[position, invert(position), ...oppositeAxis(position)],
|
|
245
316
|
)
|
|
246
317
|
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)}`
|
|
247
318
|
if (attributes && 'inert' in attributes && attributes.inert) overlay.style.pointerEvents = 'none'
|
|
248
319
|
else overlay.inert = false
|
|
249
|
-
overlay.classList.add(overlayPos.relativeTo)
|
|
320
|
+
overlay.classList.add(overlayPos.relativeTo, `align-${alignment}`)
|
|
321
|
+
removeScrollEffects = attachScrollEffects(target, overlay, hide)
|
|
250
322
|
}, 0)
|
|
251
323
|
|
|
252
324
|
return {
|
|
@@ -258,17 +330,7 @@ export function showOverlay<T extends keyof HTMLTag = 'div'>(
|
|
|
258
330
|
* Removes the overlay element.
|
|
259
331
|
* @returns a promise that completes when the element is fully removed (after any animation).
|
|
260
332
|
*/
|
|
261
|
-
hide
|
|
262
|
-
overlay.style.opacity = '0'
|
|
263
|
-
overlay.style.pointerEvents = 'none'
|
|
264
|
-
overlay.inert = true
|
|
265
|
-
setTimeout(() => {
|
|
266
|
-
try {
|
|
267
|
-
unmount()
|
|
268
|
-
} catch { /* empty */ }
|
|
269
|
-
resolve()
|
|
270
|
-
}, animationDurationMS)
|
|
271
|
-
}),
|
|
333
|
+
hide,
|
|
272
334
|
/**
|
|
273
335
|
* Returns a promise that resolves as soon as the overlay finishes the animation to show up.
|
|
274
336
|
*/
|
package/src/types.ts
CHANGED
|
@@ -180,15 +180,15 @@ export interface CitricController {
|
|
|
180
180
|
/**
|
|
181
181
|
* A function to run whenever the component Button is clicked.
|
|
182
182
|
* @param event the click event.
|
|
183
|
-
* @param
|
|
183
|
+
* @param metadata metadata of the button.
|
|
184
184
|
*/
|
|
185
|
-
onClickButton?: (event: React.MouseEvent<HTMLButtonElement>,
|
|
185
|
+
onClickButton?: (event: React.MouseEvent<HTMLButtonElement>, metadata: any) => void,
|
|
186
186
|
/**
|
|
187
187
|
* A function to run whenever the component Link is clicked.
|
|
188
188
|
* @param event the click event.
|
|
189
|
-
* @param
|
|
189
|
+
* @param metadata metadata of the anchor.
|
|
190
190
|
*/
|
|
191
|
-
onClickLink?: (event: React.MouseEvent<HTMLAnchorElement>,
|
|
191
|
+
onClickLink?: (event: React.MouseEvent<HTMLAnchorElement>, metadata: any) => void,
|
|
192
192
|
/**
|
|
193
193
|
* A custom renderer for error feedbacks.
|
|
194
194
|
*
|