@ultraviolet/ui 1.6.0 → 1.7.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/index.d.ts CHANGED
@@ -1717,9 +1717,9 @@ type PieChartProps = {
1717
1717
  */
1718
1718
  declare const PieChart: ({ height, width, data, emptyLegend, content, withLegend, margin, chartProps, }: PieChartProps) => _emotion_react_jsx_runtime.JSX.Element;
1719
1719
 
1720
- type TooltipPlacement = 'top' | 'right' | 'bottom' | 'left' | 'auto';
1720
+ type PopupPlacement = 'top' | 'right' | 'bottom' | 'left' | 'auto';
1721
1721
 
1722
- type TooltipProps = {
1722
+ type TooltipProps$1 = {
1723
1723
  /**
1724
1724
  * Id is automatically generated if not set. It is used for associating tooltip wrapper with tooltip portal.
1725
1725
  */
@@ -1736,7 +1736,7 @@ type TooltipProps = {
1736
1736
  /**
1737
1737
  * `auto` placement will change the position of the tooltip if it doesn't fit in the viewport.
1738
1738
  */
1739
- placement?: TooltipPlacement;
1739
+ placement?: PopupPlacement;
1740
1740
  /**
1741
1741
  * Content of the tooltip, preferably text inside.
1742
1742
  */
@@ -1749,8 +1749,9 @@ type TooltipProps = {
1749
1749
  innerRef?: Ref<HTMLDivElement | null>;
1750
1750
  role?: string;
1751
1751
  'data-testid'?: string;
1752
+ hasArrow?: boolean;
1752
1753
  };
1753
- declare const Tooltip: react.ForwardRefExoticComponent<TooltipProps & react.RefAttributes<HTMLDivElement>>;
1754
+ declare const Popup: react.ForwardRefExoticComponent<TooltipProps$1 & react.RefAttributes<HTMLDivElement>>;
1754
1755
 
1755
1756
  type SentimentType = 'neutral' | 'primary';
1756
1757
  declare const SIZES_WIDTH: {
@@ -1768,7 +1769,7 @@ type PopoverProps = {
1768
1769
  onClose: () => void;
1769
1770
  className?: string;
1770
1771
  'data-testid'?: string;
1771
- } & Pick<ComponentProps<typeof Tooltip>, 'placement'>;
1772
+ } & Pick<ComponentProps<typeof Popup>, 'placement'>;
1772
1773
  declare const Popover: ({ visible, children, placement, content, title, sentiment, size, onClose, className, "data-testid": dataTestId, }: PopoverProps) => _emotion_react_jsx_runtime.JSX.Element;
1773
1774
 
1774
1775
  declare const progressBarSentiments: readonly ["primary", "success", "warning", "info"];
@@ -2410,6 +2411,9 @@ type ToggleProps = {
2410
2411
  };
2411
2412
  declare const Toggle: react.ForwardRefExoticComponent<ToggleProps & react.RefAttributes<HTMLInputElement>>;
2412
2413
 
2414
+ type TooltipProps = Pick<ComponentProps<typeof Popup>, 'id' | 'children' | 'maxWidth' | 'placement' | 'text' | 'className' | 'visible' | 'innerRef' | 'role' | 'data-testid'>;
2415
+ declare const Tooltip: react.ForwardRefExoticComponent<TooltipProps & react.RefAttributes<HTMLDivElement>>;
2416
+
2413
2417
  type VerificationCodeProps = {
2414
2418
  disabled?: boolean;
2415
2419
  error?: boolean;
@@ -163,6 +163,8 @@ const DateInput = _ref24 => {
163
163
  className,
164
164
  'data-testid': dataTestId
165
165
  } = _ref24;
166
+ // Linked to: https://github.com/Hacker0x01/react-datepicker/issues/3834
167
+ const ReactDatePicker = DatePicker.default ?? DatePicker;
166
168
  const localeCode = (typeof locale === 'string' ? locale : locale?.code) ?? 'en-GB';
167
169
  if (typeof locale === 'object') {
168
170
  registerLocale(localeCode, locale);
@@ -171,7 +173,7 @@ const DateInput = _ref24 => {
171
173
  children: [jsx(Global, {
172
174
  styles: css_248z
173
175
  }), jsx(StyledWrapper, {
174
- children: jsx(DatePicker, {
176
+ children: jsx(ReactDatePicker, {
175
177
  "data-testid": dataTestId,
176
178
  className: className,
177
179
  autoFocus: autoFocus,
@@ -3,15 +3,15 @@ import { useRef, useCallback, useEffect } from 'react';
3
3
  import { Button } from '../Button/index.js';
4
4
  import { Stack } from '../Stack/index.js';
5
5
  import { Text } from '../Text/index.js';
6
- import { Tooltip } from '../Tooltip/index.js';
7
6
  import { jsx, jsxs } from '@emotion/react/jsx-runtime';
7
+ import { Popup } from '../../internalComponents/Popup/index.js';
8
8
 
9
9
  const SIZES_WIDTH = {
10
10
  small: 320,
11
11
  medium: 420,
12
12
  large: 520
13
13
  };
14
- const StyledTooltip = /*#__PURE__*/_styled(Tooltip, {
14
+ const StyledPopup = /*#__PURE__*/_styled(Popup, {
15
15
  shouldForwardProp: prop => !['sentiment', 'size'].includes(prop),
16
16
  target: "ejpxv5a0"
17
17
  })("padding:", _ref => {
@@ -38,16 +38,20 @@ const StyledTooltip = /*#__PURE__*/_styled(Tooltip, {
38
38
  return `
39
39
  background: ${theme.colors.neutral.background};
40
40
  box-shadow: ${theme.shadows.popover};
41
- &::after {
42
- border-color: ${theme.colors.neutral.background} transparent transparent transparent;
41
+ &[data-has-arrow='true'] {
42
+ &::after {
43
+ border-color: ${theme.colors.neutral.background} transparent transparent transparent;
44
+ }
43
45
  }
44
46
  `;
45
47
  }
46
48
  return `
47
49
  background: ${theme.colors.primary.backgroundStrong};
48
50
  box-shadow: ${theme.shadows.popover};
49
- &::after {
50
- border-color: ${theme.colors.primary.backgroundStrong} transparent transparent transparent;
51
+ &[data-has-arrow='true'] {
52
+ &::after {
53
+ border-color: ${theme.colors.primary.backgroundStrong} transparent transparent transparent;
54
+ }
51
55
  }
52
56
  `;
53
57
  }, ";");
@@ -109,7 +113,7 @@ const Popover = _ref6 => {
109
113
  document.removeEventListener('click', handleClickOutside, true);
110
114
  };
111
115
  }, [handleClickOutside]);
112
- return jsx(StyledTooltip, {
116
+ return jsx(StyledPopup, {
113
117
  visible: visible,
114
118
  placement: placement,
115
119
  text: jsx(ContentWrapper, {
@@ -1,109 +1,8 @@
1
- import _styled from '@emotion/styled/base';
2
- import { css, keyframes } from '@emotion/react';
3
- import { forwardRef, useRef, useImperativeHandle, useState, useId, useCallback, useEffect } from 'react';
4
- import { createPortal } from 'react-dom';
5
- import { DEFAULT_POSITIONS, computePositions, ARROW_WIDTH } from './helpers.js';
6
- import { jsx, Fragment, jsxs } from '@emotion/react/jsx-runtime';
1
+ import { forwardRef } from 'react';
2
+ import { jsx } from '@emotion/react/jsx-runtime';
3
+ import { Popup } from '../../internalComponents/Popup/index.js';
7
4
 
8
- function _EMOTION_STRINGIFIED_CSS_ERROR__() { return "You have tried to stringify object returned from `css` function. It isn't supposed to be used directly (e.g. as value of the `className` prop), but rather handed to emotion so it can handle it (e.g. as value of `css` prop)."; }
9
- const ANIMATION_DURATION = 230; // in ms
10
-
11
- function noop() {}
12
- const animation = positions => keyframes`
13
- 0% {
14
- opacity: 0;
15
- transform: ${positions.tooltipInitialPosition};
16
- }
17
- 100% {
18
- opacity: 1;
19
- transform: ${positions.tooltipPosition};
20
- }
21
- `;
22
- const exitAnimation = positions => keyframes`
23
- 0% {
24
- opacity: 1;
25
- transform: ${positions.tooltipPosition};
26
- }
27
- 100% {
28
- opacity: 0;
29
- transform: ${positions.tooltipInitialPosition};
30
- }
31
- `;
32
- const StyledTooltip = /*#__PURE__*/_styled("div", {
33
- target: "e1h4zly11"
34
- })("background:", _ref => {
35
- let {
36
- theme
37
- } = _ref;
38
- return theme.colors.neutral.backgroundStronger;
39
- }, ";color:", _ref2 => {
40
- let {
41
- theme
42
- } = _ref2;
43
- return theme.colors.neutral.textStronger;
44
- }, ";border-radius:", _ref3 => {
45
- let {
46
- theme
47
- } = _ref3;
48
- return theme.radii.default;
49
- }, ";padding:", _ref4 => {
50
- let {
51
- theme
52
- } = _ref4;
53
- return `${theme.space['0.5']} ${theme.space['1']}`;
54
- }, ";text-align:center;position:absolute;max-width:", _ref5 => {
55
- let {
56
- maxWidth
57
- } = _ref5;
58
- return maxWidth;
59
- }, "px;font-size:0.8rem;inset:0 auto auto 0;top:0;left:0;transform:", _ref6 => {
60
- let {
61
- positions
62
- } = _ref6;
63
- return positions.tooltipPosition;
64
- }, ";animation:", _ref7 => {
65
- let {
66
- positions,
67
- reverseAnimation
68
- } = _ref7;
69
- return /*#__PURE__*/css(ANIMATION_DURATION, "ms ", !reverseAnimation ? animation(positions) : exitAnimation(positions), " forwards;");
70
- }, ";&::after{content:' ';position:absolute;top:", _ref8 => {
71
- let {
72
- positions
73
- } = _ref8;
74
- return positions.arrowTop;
75
- }, "px;left:", _ref9 => {
76
- let {
77
- positions
78
- } = _ref9;
79
- return positions.arrowLeft;
80
- }, "px;transform:", _ref10 => {
81
- let {
82
- positions
83
- } = _ref10;
84
- return positions.arrowTransform;
85
- }, " rotate(", _ref11 => {
86
- let {
87
- positions
88
- } = _ref11;
89
- return positions.rotate;
90
- }, "deg);margin-left:-", ARROW_WIDTH, "px;border-width:", ARROW_WIDTH, "px;border-style:solid;border-color:", _ref12 => {
91
- let {
92
- theme
93
- } = _ref12;
94
- return theme.colors.neutral.backgroundStronger;
95
- }, " transparent transparent transparent;pointer-events:none;}");
96
- const StyledChildrenContainer = /*#__PURE__*/_styled("div", {
97
- target: "e1h4zly10"
98
- })(process.env.NODE_ENV === "production" ? {
99
- name: "ck88l6",
100
- styles: "display:inherit"
101
- } : {
102
- name: "ck88l6",
103
- styles: "display:inherit",
104
- toString: _EMOTION_STRINGIFIED_CSS_ERROR__
105
- });
106
- const Tooltip = /*#__PURE__*/forwardRef((_ref13, tooltipRef) => {
5
+ const Tooltip = /*#__PURE__*/forwardRef((_ref, tooltipRef) => {
107
6
  let {
108
7
  children,
109
8
  text = '',
@@ -115,160 +14,19 @@ const Tooltip = /*#__PURE__*/forwardRef((_ref13, tooltipRef) => {
115
14
  innerRef,
116
15
  role = 'tooltip',
117
16
  'data-testid': dataTestId
118
- } = _ref13;
119
- const childrenRef = useRef(null);
120
- useImperativeHandle(innerRef, () => childrenRef.current);
121
- const tempTooltipRef = useRef(null);
122
- const innerTooltipRef = tooltipRef || tempTooltipRef;
123
- const timer = useRef();
124
-
125
- // Debounce timer will be used to prevent the tooltip from flickering when the user moves the mouse out and in the children element.
126
- const debounceTimer = useRef();
127
- const [visibleInDom, setVisibleInDom] = useState(false);
128
- const [reverseAnimation, setReverseAnimation] = useState(false);
129
- const [positions, setPositions] = useState({
130
- ...DEFAULT_POSITIONS
131
- });
132
- const uniqueId = useId();
133
- const generatedId = id ?? uniqueId;
134
- const isControlled = visible !== undefined;
135
- const generatePositions = useCallback(() => {
136
- if (childrenRef.current && innerTooltipRef.current) {
137
- setPositions(computePositions({
138
- childrenRef,
139
- placement,
140
- tooltipRef: innerTooltipRef
141
- }));
142
- }
143
- }, [innerTooltipRef, placement]);
144
- const onScrollDetected = useCallback(() => {
145
- // We remove animation on scroll or the animation will restart on every scroll
146
- if (innerTooltipRef.current) {
147
- innerTooltipRef.current.style.animation = 'none';
148
- }
149
- generatePositions();
150
- }, [generatePositions, innerTooltipRef]);
151
-
152
- /**
153
- * This function is called when we need to remove tooltip portal from DOM and remove event listener to it.
154
- */
155
- const unmountTooltip = useCallback(() => {
156
- setVisibleInDom(false);
157
- setReverseAnimation(false);
158
- window.removeEventListener('scroll', onScrollDetected, true);
159
- }, [onScrollDetected]);
160
-
161
- /**
162
- * When mouse hover or stop hovering children this function display or hide tooltip. A timeout is set to allow animation
163
- * end, then remove tooltip from dom.
164
- */
165
- const onPointerEvent = useCallback(isVisible => () => {
166
- // This condition is for when we want to unmount the tooltip
167
- // There is debounce in order to avoid tooltip to flicker when we move the mouse from children to tooltip
168
- // Timer is used to follow the animation duration
169
- if (!isVisible && innerTooltipRef.current && !debounceTimer.current) {
170
- debounceTimer.current = setTimeout(() => {
171
- setReverseAnimation(true);
172
- timer.current = setTimeout(() => unmountTooltip(), ANIMATION_DURATION);
173
- }, 200);
174
- } else if (isVisible) {
175
- // This condition is for when we want to mount the tooltip
176
- // If the timer exists it means the tooltip was about to umount, but we hovered the children again,
177
- // so we clear the timer and the tooltip will not be unmounted
178
- if (timer.current) {
179
- setReverseAnimation(false);
180
- clearTimeout(timer.current);
181
- timer.current = undefined;
182
- }
183
- // And here is when we currently are in a debounce timer, it means tooltip was hovered during
184
- // that period, and so we can clear debounce timer
185
- if (debounceTimer.current) {
186
- clearTimeout(debounceTimer.current);
187
- debounceTimer.current = undefined;
188
- }
189
- setVisibleInDom(true);
190
- }
191
- }, [innerTooltipRef, unmountTooltip]);
192
-
193
- /**
194
- * Once tooltip is visible in the dom we can compute positions, then set it visible on screen and add event to
195
- * recompute positions on scroll or screen resize.
196
- */
197
- useEffect(() => {
198
- if (visibleInDom) {
199
- generatePositions();
200
-
201
- // We want to detect scroll in order to recompute positions of tooltip
202
- // Adding true as third parameter to event listener will detect nested scrolls.
203
- window.addEventListener('scroll', onScrollDetected, true);
204
- }
205
- return () => {
206
- window.removeEventListener('scroll', onScrollDetected, true);
207
- if (timer.current) {
208
- clearTimeout(timer.current);
209
- timer.current = undefined;
210
- }
211
- };
212
- }, [generatePositions, onScrollDetected, visibleInDom]);
213
-
214
- /**
215
- * If tooltip has `visible` prop it means the tooltip is manually controlled through this prop.
216
- * In this cas we don't want to display tooltip on hover, but only when `visible` is true.
217
- */
218
- useEffect(() => {
219
- if (isControlled) {
220
- onPointerEvent(visible)();
221
- }
222
- }, [isControlled, onPointerEvent, visible]);
223
- const onKeyDown = useCallback(event => {
224
- if (event.code === 'Escape') {
225
- unmountTooltip();
226
- }
227
- }, [unmountTooltip]);
228
-
229
- /**
230
- * Will render children conditionally if children is a function or not.
231
- */
232
- const renderChildren = useCallback(() => {
233
- if (typeof children === 'function') {
234
- return children({
235
- onBlur: !isControlled ? onPointerEvent(false) : noop,
236
- onFocus: !isControlled ? onPointerEvent(true) : noop,
237
- onPointerEnter: !isControlled ? onPointerEvent(true) : noop,
238
- onPointerLeave: !isControlled ? onPointerEvent(false) : noop,
239
- ref: childrenRef
240
- });
241
- }
242
- return jsx(StyledChildrenContainer, {
243
- "aria-describedby": generatedId,
244
- onBlur: !isControlled ? onPointerEvent(false) : noop,
245
- onFocus: !isControlled ? onPointerEvent(true) : noop,
246
- onPointerEnter: !isControlled ? onPointerEvent(true) : noop,
247
- onPointerLeave: !isControlled ? onPointerEvent(false) : noop,
248
- ref: childrenRef,
249
- tabIndex: 0,
250
- onKeyDown: onKeyDown,
251
- children: children
252
- });
253
- }, [children, generatedId, isControlled, onKeyDown, onPointerEvent]);
254
- if (!text) {
255
- if (typeof children === 'function') return null;
256
- return jsx(Fragment, {
257
- children: children
258
- });
259
- }
260
- return jsxs(Fragment, {
261
- children: [renderChildren(), visibleInDom ? /*#__PURE__*/createPortal(jsx(StyledTooltip, {
262
- ref: innerTooltipRef,
263
- positions: positions,
264
- maxWidth: maxWidth,
265
- role: role,
266
- id: generatedId,
267
- className: className,
268
- reverseAnimation: reverseAnimation,
269
- "data-testid": dataTestId,
270
- children: text
271
- }), document.body) : null]
17
+ } = _ref;
18
+ return jsx(Popup, {
19
+ id: id,
20
+ ref: tooltipRef,
21
+ role: role,
22
+ "data-testid": dataTestId,
23
+ className: className,
24
+ maxWidth: maxWidth,
25
+ visible: visible,
26
+ placement: placement,
27
+ text: text,
28
+ innerRef: innerRef,
29
+ children: children
272
30
  });
273
31
  });
274
32
 
@@ -0,0 +1,278 @@
1
+ import _styled from '@emotion/styled/base';
2
+ import { css, keyframes } from '@emotion/react';
3
+ import { forwardRef, useRef, useImperativeHandle, useState, useId, useCallback, useEffect } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { DEFAULT_POSITIONS, computePositions, ARROW_WIDTH } from './helpers.js';
6
+ import { jsx, Fragment, jsxs } from '@emotion/react/jsx-runtime';
7
+
8
+ function _EMOTION_STRINGIFIED_CSS_ERROR__() { return "You have tried to stringify object returned from `css` function. It isn't supposed to be used directly (e.g. as value of the `className` prop), but rather handed to emotion so it can handle it (e.g. as value of `css` prop)."; }
9
+ const ANIMATION_DURATION = 230; // in ms
10
+
11
+ function noop() {}
12
+ const animation = positions => keyframes`
13
+ 0% {
14
+ opacity: 0;
15
+ transform: ${positions.tooltipInitialPosition};
16
+ }
17
+ 100% {
18
+ opacity: 1;
19
+ transform: ${positions.tooltipPosition};
20
+ }
21
+ `;
22
+ const exitAnimation = positions => keyframes`
23
+ 0% {
24
+ opacity: 1;
25
+ transform: ${positions.tooltipPosition};
26
+ }
27
+ 100% {
28
+ opacity: 0;
29
+ transform: ${positions.tooltipInitialPosition};
30
+ }
31
+ `;
32
+ const StyledTooltip = /*#__PURE__*/_styled('div', {
33
+ shouldForwardProp: prop => !['maxWidth', 'positions', 'reverseAnimation'].includes(prop),
34
+ target: "efkq4701"
35
+ })("background:", _ref => {
36
+ let {
37
+ theme
38
+ } = _ref;
39
+ return theme.colors.neutral.backgroundStronger;
40
+ }, ";color:", _ref2 => {
41
+ let {
42
+ theme
43
+ } = _ref2;
44
+ return theme.colors.neutral.textStronger;
45
+ }, ";border-radius:", _ref3 => {
46
+ let {
47
+ theme
48
+ } = _ref3;
49
+ return theme.radii.default;
50
+ }, ";padding:", _ref4 => {
51
+ let {
52
+ theme
53
+ } = _ref4;
54
+ return `${theme.space['0.5']} ${theme.space['1']}`;
55
+ }, ";text-align:center;position:absolute;max-width:", _ref5 => {
56
+ let {
57
+ maxWidth
58
+ } = _ref5;
59
+ return maxWidth;
60
+ }, "px;font-size:0.8rem;inset:0 auto auto 0;top:0;left:0;transform:", _ref6 => {
61
+ let {
62
+ positions
63
+ } = _ref6;
64
+ return positions.tooltipPosition;
65
+ }, ";animation:", _ref7 => {
66
+ let {
67
+ positions,
68
+ reverseAnimation
69
+ } = _ref7;
70
+ return /*#__PURE__*/css(ANIMATION_DURATION, "ms ", !reverseAnimation ? animation(positions) : exitAnimation(positions), " forwards;");
71
+ }, ";&[data-has-arrow='true']{&::after{content:' ';position:absolute;top:", _ref8 => {
72
+ let {
73
+ positions
74
+ } = _ref8;
75
+ return positions.arrowTop;
76
+ }, "px;left:", _ref9 => {
77
+ let {
78
+ positions
79
+ } = _ref9;
80
+ return positions.arrowLeft;
81
+ }, "px;transform:", _ref10 => {
82
+ let {
83
+ positions
84
+ } = _ref10;
85
+ return positions.arrowTransform;
86
+ }, " rotate(", _ref11 => {
87
+ let {
88
+ positions
89
+ } = _ref11;
90
+ return positions.rotate;
91
+ }, "deg);margin-left:-", ARROW_WIDTH, "px;border-width:", ARROW_WIDTH, "px;border-style:solid;border-color:", _ref12 => {
92
+ let {
93
+ theme
94
+ } = _ref12;
95
+ return theme.colors.neutral.backgroundStronger;
96
+ }, " transparent transparent transparent;pointer-events:none;}}");
97
+ const StyledChildrenContainer = /*#__PURE__*/_styled("div", {
98
+ target: "efkq4700"
99
+ })(process.env.NODE_ENV === "production" ? {
100
+ name: "ck88l6",
101
+ styles: "display:inherit"
102
+ } : {
103
+ name: "ck88l6",
104
+ styles: "display:inherit",
105
+ toString: _EMOTION_STRINGIFIED_CSS_ERROR__
106
+ });
107
+ const Popup = /*#__PURE__*/forwardRef((_ref13, tooltipRef) => {
108
+ let {
109
+ children,
110
+ text = '',
111
+ placement = 'auto',
112
+ id,
113
+ className,
114
+ maxWidth = 232,
115
+ visible,
116
+ innerRef,
117
+ role = 'tooltip',
118
+ 'data-testid': dataTestId,
119
+ hasArrow = true
120
+ } = _ref13;
121
+ const childrenRef = useRef(null);
122
+ useImperativeHandle(innerRef, () => childrenRef.current);
123
+ const tempTooltipRef = useRef(null);
124
+ const innerTooltipRef = tooltipRef || tempTooltipRef;
125
+ const timer = useRef();
126
+
127
+ // Debounce timer will be used to prevent the tooltip from flickering when the user moves the mouse out and in the children element.
128
+ const debounceTimer = useRef();
129
+ const [visibleInDom, setVisibleInDom] = useState(false);
130
+ const [reverseAnimation, setReverseAnimation] = useState(false);
131
+ const [positions, setPositions] = useState({
132
+ ...DEFAULT_POSITIONS
133
+ });
134
+ const uniqueId = useId();
135
+ const generatedId = id ?? uniqueId;
136
+ const isControlled = visible !== undefined;
137
+ const generatePositions = useCallback(() => {
138
+ if (childrenRef.current && innerTooltipRef.current) {
139
+ setPositions(computePositions({
140
+ childrenRef,
141
+ placement,
142
+ tooltipRef: innerTooltipRef
143
+ }));
144
+ }
145
+ }, [innerTooltipRef, placement]);
146
+ const onScrollDetected = useCallback(() => {
147
+ // We remove animation on scroll or the animation will restart on every scroll
148
+ if (innerTooltipRef.current) {
149
+ innerTooltipRef.current.style.animation = 'none';
150
+ }
151
+ generatePositions();
152
+ }, [generatePositions, innerTooltipRef]);
153
+
154
+ /**
155
+ * This function is called when we need to remove tooltip portal from DOM and remove event listener to it.
156
+ */
157
+ const unmountTooltip = useCallback(() => {
158
+ setVisibleInDom(false);
159
+ setReverseAnimation(false);
160
+ window.removeEventListener('scroll', onScrollDetected, true);
161
+ }, [onScrollDetected]);
162
+
163
+ /**
164
+ * When mouse hover or stop hovering children this function display or hide tooltip. A timeout is set to allow animation
165
+ * end, then remove tooltip from dom.
166
+ */
167
+ const onPointerEvent = useCallback(isVisible => () => {
168
+ // This condition is for when we want to unmount the tooltip
169
+ // There is debounce in order to avoid tooltip to flicker when we move the mouse from children to tooltip
170
+ // Timer is used to follow the animation duration
171
+ if (!isVisible && innerTooltipRef.current && !debounceTimer.current) {
172
+ debounceTimer.current = setTimeout(() => {
173
+ setReverseAnimation(true);
174
+ timer.current = setTimeout(() => unmountTooltip(), ANIMATION_DURATION);
175
+ }, 200);
176
+ } else if (isVisible) {
177
+ // This condition is for when we want to mount the tooltip
178
+ // If the timer exists it means the tooltip was about to umount, but we hovered the children again,
179
+ // so we clear the timer and the tooltip will not be unmounted
180
+ if (timer.current) {
181
+ setReverseAnimation(false);
182
+ clearTimeout(timer.current);
183
+ timer.current = undefined;
184
+ }
185
+ // And here is when we currently are in a debounce timer, it means tooltip was hovered during
186
+ // that period, and so we can clear debounce timer
187
+ if (debounceTimer.current) {
188
+ clearTimeout(debounceTimer.current);
189
+ debounceTimer.current = undefined;
190
+ }
191
+ setVisibleInDom(true);
192
+ }
193
+ }, [innerTooltipRef, unmountTooltip]);
194
+
195
+ /**
196
+ * Once tooltip is visible in the dom we can compute positions, then set it visible on screen and add event to
197
+ * recompute positions on scroll or screen resize.
198
+ */
199
+ useEffect(() => {
200
+ if (visibleInDom) {
201
+ generatePositions();
202
+
203
+ // We want to detect scroll in order to recompute positions of tooltip
204
+ // Adding true as third parameter to event listener will detect nested scrolls.
205
+ window.addEventListener('scroll', onScrollDetected, true);
206
+ }
207
+ return () => {
208
+ window.removeEventListener('scroll', onScrollDetected, true);
209
+ if (timer.current) {
210
+ clearTimeout(timer.current);
211
+ timer.current = undefined;
212
+ }
213
+ };
214
+ }, [generatePositions, onScrollDetected, visibleInDom]);
215
+
216
+ /**
217
+ * If tooltip has `visible` prop it means the tooltip is manually controlled through this prop.
218
+ * In this cas we don't want to display tooltip on hover, but only when `visible` is true.
219
+ */
220
+ useEffect(() => {
221
+ if (isControlled) {
222
+ onPointerEvent(visible)();
223
+ }
224
+ }, [isControlled, onPointerEvent, visible]);
225
+ const onKeyDown = useCallback(event => {
226
+ if (event.code === 'Escape') {
227
+ unmountTooltip();
228
+ }
229
+ }, [unmountTooltip]);
230
+
231
+ /**
232
+ * Will render children conditionally if children is a function or not.
233
+ */
234
+ const renderChildren = useCallback(() => {
235
+ if (typeof children === 'function') {
236
+ return children({
237
+ onBlur: !isControlled ? onPointerEvent(false) : noop,
238
+ onFocus: !isControlled ? onPointerEvent(true) : noop,
239
+ onPointerEnter: !isControlled ? onPointerEvent(true) : noop,
240
+ onPointerLeave: !isControlled ? onPointerEvent(false) : noop,
241
+ ref: childrenRef
242
+ });
243
+ }
244
+ return jsx(StyledChildrenContainer, {
245
+ "aria-describedby": generatedId,
246
+ onBlur: !isControlled ? onPointerEvent(false) : noop,
247
+ onFocus: !isControlled ? onPointerEvent(true) : noop,
248
+ onPointerEnter: !isControlled ? onPointerEvent(true) : noop,
249
+ onPointerLeave: !isControlled ? onPointerEvent(false) : noop,
250
+ ref: childrenRef,
251
+ tabIndex: 0,
252
+ onKeyDown: onKeyDown,
253
+ children: children
254
+ });
255
+ }, [children, generatedId, isControlled, onKeyDown, onPointerEvent]);
256
+ if (!text) {
257
+ if (typeof children === 'function') return null;
258
+ return jsx(Fragment, {
259
+ children: children
260
+ });
261
+ }
262
+ return jsxs(Fragment, {
263
+ children: [renderChildren(), visibleInDom ? /*#__PURE__*/createPortal(jsx(StyledTooltip, {
264
+ ref: innerTooltipRef,
265
+ positions: positions,
266
+ maxWidth: maxWidth,
267
+ role: role,
268
+ id: generatedId,
269
+ className: className,
270
+ reverseAnimation: reverseAnimation,
271
+ "data-testid": dataTestId,
272
+ "data-has-arrow": hasArrow,
273
+ children: text
274
+ }), document.body) : null]
275
+ });
276
+ });
277
+
278
+ export { Popup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultraviolet/ui",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Ultraviolet UI",
5
5
  "homepage": "https://github.com/scaleway/ultraviolet#readme",
6
6
  "repository": {
@@ -67,6 +67,6 @@
67
67
  "react-toastify": "9.1.3",
68
68
  "react-use-clipboard": "1.0.9",
69
69
  "reakit": "1.3.11",
70
- "@ultraviolet/themes": "1.2.0"
70
+ "@ultraviolet/themes": "1.2.1"
71
71
  }
72
72
  }