@ultraviolet/ui 1.24.2 → 1.26.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.
@@ -1,57 +1,75 @@
1
1
  const ARROW_WIDTH = 8; // in px
2
2
  const SPACE = 4; // in px
3
- const TOTAL_USED_SPACE = ARROW_WIDTH + SPACE; // in px
3
+ const TOTAL_USED_SPACE = 0; // in px
4
4
  const DEFAULT_POSITIONS = {
5
5
  arrowLeft: -999,
6
6
  arrowTop: -999,
7
7
  arrowTransform: 'translate(-50%, -50)',
8
8
  placement: 'top',
9
9
  rotate: 135,
10
- tooltipInitialPosition: 'translate3d(-999px, -999px, 0)',
11
- tooltipPosition: 'translate3d(-999px, -999px, 0)'
10
+ popupInitialPosition: 'translate3d(-999px, -999px, 0)',
11
+ popupPosition: 'translate3d(-999px, -999px, 0)'
12
12
  };
13
13
  /**
14
- * This function will find the best placement in a window for tooltip based on children position and tooltip size
14
+ * This function will find the best placement in a window for popup based on children position and popup size
15
15
  */
16
16
  const computePlacement = _ref => {
17
17
  let {
18
18
  childrenStructuredRef,
19
- tooltipStructuredRef
19
+ popupStructuredRef,
20
+ offsetParentRect,
21
+ popupPortalTarget
20
22
  } = _ref;
21
23
  const {
22
- top: childrenX,
23
- left: childrenY,
24
+ top: childrenTop,
25
+ left: childrenLeft,
24
26
  right: childrenRight
25
27
  } = childrenStructuredRef;
26
28
  const {
27
- width: tooltipWidth,
28
- height: tooltipHeight
29
- } = tooltipStructuredRef;
30
- if (childrenX - tooltipHeight - TOTAL_USED_SPACE < 0) {
29
+ top: parentTop,
30
+ left: parentLeft,
31
+ right: parentRight
32
+ } = offsetParentRect;
33
+ const overloadedChildrenLeft = popupPortalTarget === document.body ? childrenLeft : childrenLeft - parentLeft;
34
+ const overloadedChildrenTop = popupPortalTarget === document.body ? childrenTop : childrenTop - parentTop;
35
+ const overloadedChildrenRight = popupPortalTarget === document.body ? childrenRight : childrenRight - parentRight;
36
+ const {
37
+ width: popupWidth,
38
+ height: popupHeight
39
+ } = popupStructuredRef;
40
+ if (overloadedChildrenTop - popupHeight - TOTAL_USED_SPACE < 0) {
31
41
  return 'bottom';
32
42
  }
33
- if (childrenY - tooltipWidth - TOTAL_USED_SPACE < 0) {
43
+ if (overloadedChildrenLeft - popupWidth - TOTAL_USED_SPACE < 0) {
34
44
  return 'right';
35
45
  }
36
- if (childrenRight + tooltipWidth + TOTAL_USED_SPACE > window.innerWidth) {
46
+ if (overloadedChildrenRight + popupWidth + TOTAL_USED_SPACE > window.innerWidth) {
37
47
  return 'left';
38
48
  }
39
49
  return 'top';
40
50
  };
41
51
  /**
42
- * This function will compute the positions of tooltip and arrow based on children position and tooltip size
52
+ * This function will compute the positions of popup and arrow based on children position and popup size
43
53
  */
44
54
  const computePositions = _ref2 => {
45
55
  let {
46
56
  placement,
47
57
  childrenRef,
48
- tooltipRef
58
+ popupRef,
59
+ popupPortalTarget
49
60
  } = _ref2;
50
61
  const childrenStructuredRef = childrenRef.current.getBoundingClientRect();
51
- const tooltipStructuredRef = tooltipRef.current.getBoundingClientRect();
62
+ const offsetParentRect = childrenRef?.current?.offsetParent?.getBoundingClientRect() ?? {
63
+ top: 0,
64
+ left: 0,
65
+ right: 0
66
+ };
67
+ const popupStructuredRef = popupRef.current.getBoundingClientRect();
52
68
  const placementBasedOnWindowSize = placement === 'auto' ? computePlacement({
53
69
  childrenStructuredRef,
54
- tooltipStructuredRef
70
+ popupStructuredRef,
71
+ offsetParentRect: offsetParentRect,
72
+ popupPortalTarget
55
73
  }) : placement;
56
74
  const {
57
75
  top: childrenTop,
@@ -61,68 +79,78 @@ const computePositions = _ref2 => {
61
79
  height: childrenHeight
62
80
  } = childrenStructuredRef;
63
81
  const {
64
- width: tooltipWidth,
65
- height: tooltipHeight
66
- } = tooltipStructuredRef;
82
+ top: parentTop,
83
+ left: parentLeft,
84
+ right: parentRight
85
+ } = offsetParentRect;
86
+ const {
87
+ width: popupWidth,
88
+ height: popupHeight
89
+ } = popupStructuredRef;
90
+
91
+ // It will get how much scroll is done on the page to compute the position of the popup
92
+ const scrollTopValue = popupPortalTarget === document.body ? document.documentElement.scrollTop : 0;
67
93
 
68
- // It will get how much scroll is done on the page to compute the position of the tooltip
69
- const scrollTopValue = document.documentElement.scrollTop;
94
+ // We need to compute the position of the popup based on the parent element in the case the popup is not in the body
95
+ const overloadedChildrenLeft = popupPortalTarget === document.body ? childrenLeft : childrenLeft - parentLeft;
96
+ const overloadedChildrenTop = popupPortalTarget === document.body ? childrenTop : childrenTop - parentTop;
97
+ const overloadedChildrenRight = popupPortalTarget === document.body ? childrenRight : childrenRight + childrenWidth + ARROW_WIDTH + SPACE - parentRight / 2;
70
98
  switch (placementBasedOnWindowSize) {
71
99
  case 'bottom':
72
100
  {
73
- const positionX = childrenLeft + childrenWidth / 2 - tooltipWidth / 2;
74
- const positionY = childrenTop + scrollTopValue + childrenHeight + ARROW_WIDTH + SPACE;
101
+ const positionX = overloadedChildrenLeft + childrenWidth / 2 - popupWidth / 2;
102
+ const positionY = overloadedChildrenTop + scrollTopValue + childrenHeight + ARROW_WIDTH + SPACE;
75
103
  return {
76
- arrowLeft: tooltipWidth / 2,
104
+ arrowLeft: popupWidth / 2,
77
105
  arrowTop: -ARROW_WIDTH - 5,
78
106
  arrowTransform: '',
79
107
  placement: 'bottom',
80
108
  rotate: 180,
81
- tooltipInitialPosition: `translate3d(${positionX}px, ${positionY - TOTAL_USED_SPACE}px, 0)`,
82
- tooltipPosition: `translate3d(${positionX}px, ${positionY}px, 0)`
109
+ popupInitialPosition: `translate3d(${positionX}px, ${positionY - TOTAL_USED_SPACE}px, 0)`,
110
+ popupPosition: `translate3d(${positionX}px, ${positionY}px, 0)`
83
111
  };
84
112
  }
85
113
  case 'left':
86
114
  {
87
- const positionX = childrenLeft - tooltipWidth - ARROW_WIDTH - SPACE * 2;
88
- const positionY = childrenTop + scrollTopValue - tooltipHeight / 2 + childrenHeight / 2;
115
+ const positionX = overloadedChildrenLeft - popupWidth - ARROW_WIDTH - SPACE * 2;
116
+ const positionY = overloadedChildrenTop + scrollTopValue - popupHeight / 2 + childrenHeight / 2;
89
117
  return {
90
- arrowLeft: tooltipWidth + ARROW_WIDTH + 5,
91
- arrowTop: tooltipHeight / 2,
118
+ arrowLeft: popupWidth + ARROW_WIDTH + 5,
119
+ arrowTop: popupHeight / 2,
92
120
  arrowTransform: 'translate(-50%, -50%)',
93
121
  placement: 'left',
94
122
  rotate: -90,
95
- tooltipInitialPosition: `translate3d(${positionX + TOTAL_USED_SPACE}px, ${positionY}px, 0)`,
96
- tooltipPosition: `translate3d(${positionX}px, ${positionY}px, 0)`
123
+ popupInitialPosition: `translate3d(${positionX + TOTAL_USED_SPACE}px, ${positionY}px, 0)`,
124
+ popupPosition: `translate3d(${positionX}px, ${positionY}px, 0)`
97
125
  };
98
126
  }
99
127
  case 'right':
100
128
  {
101
- const positionX = childrenRight + ARROW_WIDTH + SPACE * 2;
102
- const positionY = childrenTop + scrollTopValue - tooltipHeight / 2 + childrenHeight / 2;
129
+ const positionX = overloadedChildrenRight + ARROW_WIDTH + SPACE * 2;
130
+ const positionY = overloadedChildrenTop + scrollTopValue - popupHeight / 2 + childrenHeight / 2;
103
131
  return {
104
132
  arrowLeft: -ARROW_WIDTH - 5,
105
- arrowTop: tooltipHeight / 2,
133
+ arrowTop: popupHeight / 2,
106
134
  arrowTransform: 'translate(50%, -50%)',
107
135
  placement: 'right',
108
136
  rotate: 90,
109
- tooltipInitialPosition: `translate3d(${positionX - TOTAL_USED_SPACE}px, ${positionY}px, 0)`,
110
- tooltipPosition: `translate3d(${positionX}px, ${positionY}px, 0)`
137
+ popupInitialPosition: `translate3d(${positionX - TOTAL_USED_SPACE}px, ${positionY}px, 0)`,
138
+ popupPosition: `translate3d(${positionX}px, ${positionY}px, 0)`
111
139
  };
112
140
  }
113
141
  default:
114
142
  {
115
143
  // top placement is default value
116
- const positionX = childrenLeft + childrenWidth / 2 - tooltipWidth / 2;
117
- const positionY = childrenTop + scrollTopValue - tooltipHeight - ARROW_WIDTH - SPACE;
144
+ const positionX = overloadedChildrenLeft + childrenWidth / 2 - popupWidth / 2;
145
+ const positionY = overloadedChildrenTop + scrollTopValue - popupHeight - ARROW_WIDTH - SPACE;
118
146
  return {
119
- arrowLeft: tooltipWidth / 2,
120
- arrowTop: tooltipHeight - 1,
147
+ arrowLeft: popupWidth / 2,
148
+ arrowTop: popupHeight - 1,
121
149
  arrowTransform: '',
122
150
  placement: 'top',
123
151
  rotate: 0,
124
- tooltipInitialPosition: `translate3d(${positionX}px, ${positionY + TOTAL_USED_SPACE}px, 0)`,
125
- tooltipPosition: `translate3d(${positionX}px, ${positionY}px, 0)`
152
+ popupInitialPosition: `translate3d(${positionX}px, ${positionY + TOTAL_USED_SPACE}px, 0)`,
153
+ popupPosition: `translate3d(${positionX}px, ${positionY}px, 0)`
126
154
  };
127
155
  }
128
156
  }
@@ -1,36 +1,18 @@
1
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';
2
+ import { css } from '@emotion/react';
3
+ import { forwardRef, useRef, useImperativeHandle, useMemo, useState, useId, useCallback, useEffect } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
+ import { animation, exitAnimation } from './animations.js';
5
6
  import { DEFAULT_POSITIONS, computePositions, ARROW_WIDTH } from './helpers.js';
6
7
  import { jsx, Fragment, jsxs } from '@emotion/react/jsx-runtime';
7
8
 
8
9
  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
10
  const DEFAULT_ANIMATION_DURATION = 230; // in ms
10
- const DEFAULT_DEBOUNCE_DURATION = 200;
11
+ const DEFAULT_DEBOUNCE_DURATION = 200; // in ms
12
+
11
13
  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', 'maxHeight', 'animationDuration'].includes(prop),
14
+ const StyledPopup = /*#__PURE__*/_styled('div', {
15
+ shouldForwardProp: prop => !['maxWidth', 'positions', 'reverseAnimation', 'maxHeight', 'animationDuration', 'isDialog'].includes(prop),
34
16
  target: "e4h1g861"
35
17
  })("background:", _ref => {
36
18
  let {
@@ -67,11 +49,11 @@ const StyledTooltip = /*#__PURE__*/_styled('div', {
67
49
  maxHeight
68
50
  } = _ref7;
69
51
  return maxHeight ? 'auto' : undefined;
70
- }, ";overflow-wrap:break-word;font-size:0.8rem;inset:0 auto auto 0;top:0;left:0;transform:", _ref8 => {
52
+ }, ";overflow-wrap:break-word;font-size:0.8rem;inset:0 auto auto 0;top:0;left:0;z-index:1;transform:", _ref8 => {
71
53
  let {
72
54
  positions
73
55
  } = _ref8;
74
- return positions.tooltipPosition;
56
+ return positions.popupPosition;
75
57
  }, ";animation:", _ref9 => {
76
58
  let {
77
59
  positions,
@@ -131,7 +113,7 @@ const Popup = /*#__PURE__*/forwardRef((_ref15, ref) => {
131
113
  maxHeight,
132
114
  visible,
133
115
  innerRef,
134
- role = 'tooltip',
116
+ role = 'popup',
135
117
  'data-testid': dataTestId,
136
118
  hasArrow = true,
137
119
  onClose,
@@ -140,18 +122,34 @@ const Popup = /*#__PURE__*/forwardRef((_ref15, ref) => {
140
122
  'aria-haspopup': ariaHasPopup,
141
123
  hideOnClickOutside = false,
142
124
  needDebounce = true,
143
- disableAnimation = false
125
+ disableAnimation = false,
126
+ portalTarget
144
127
  } = _ref15;
145
128
  const childrenRef = useRef(null);
146
129
  useImperativeHandle(innerRef, () => childrenRef.current);
147
- const innerTooltipRef = useRef(null);
148
- useImperativeHandle(ref, () => innerTooltipRef.current);
130
+ const innerPopupRef = useRef(null);
131
+ useImperativeHandle(ref, () => innerPopupRef.current);
149
132
  const timer = useRef();
133
+ const popupPortalTarget = useMemo(() => {
134
+ if (role === 'dialog') {
135
+ if (portalTarget) return portalTarget;
136
+ if (childrenRef.current) return childrenRef.current;
137
+ if (typeof window !== 'undefined') return document.body;
138
+ return null;
139
+ }
140
+
141
+ // We check if window exists for SSR
142
+ if (typeof window !== 'undefined') {
143
+ return document.body;
144
+ }
145
+ return null;
146
+ // eslint-disable-next-line react-hooks/exhaustive-deps
147
+ }, [portalTarget, role, childrenRef.current]);
150
148
 
151
149
  // There are some issue when mixing animation and maxHeight on some browsers, so we disable animation if maxHeight is set.
152
150
  const animationDuration = disableAnimation || maxHeight ? 0 : DEFAULT_ANIMATION_DURATION;
153
151
 
154
- // Debounce timer will be used to prevent the tooltip from flickering when the user moves the mouse out and in the children element.
152
+ // Debounce timer will be used to prevent the popup from flickering when the user moves the mouse out and in the children element.
155
153
  const debounceTimer = useRef();
156
154
  const [visibleInDom, setVisibleInDom] = useState(false);
157
155
  const [reverseAnimation, setReverseAnimation] = useState(false);
@@ -161,70 +159,71 @@ const Popup = /*#__PURE__*/forwardRef((_ref15, ref) => {
161
159
  const uniqueId = useId();
162
160
  const generatedId = id ?? uniqueId;
163
161
  const isControlled = visible !== undefined;
164
- const generatePositions = useCallback(() => {
165
- if (childrenRef.current && innerTooltipRef.current) {
162
+ const generatePopupPositions = useCallback(() => {
163
+ if (childrenRef.current && innerPopupRef.current) {
166
164
  setPositions(computePositions({
167
165
  childrenRef,
168
166
  placement,
169
- tooltipRef: innerTooltipRef
167
+ popupRef: innerPopupRef,
168
+ popupPortalTarget: popupPortalTarget
170
169
  }));
171
170
  }
172
- }, [innerTooltipRef, placement]);
171
+ }, [placement, popupPortalTarget]);
173
172
 
174
173
  /**
175
- * This function is called when we need to recompute positions of tooltip due to window scroll or resize.
174
+ * This function is called when we need to recompute positions of popup due to window scroll or resize.
176
175
  */
177
176
  const onWindowChangeDetected = useCallback(() => {
178
177
  // We remove animation on scroll or the animation will restart on every scroll
179
- if (innerTooltipRef.current) {
180
- innerTooltipRef.current.style.animation = 'none';
178
+ if (innerPopupRef.current) {
179
+ innerPopupRef.current.style.animation = 'none';
181
180
  }
182
- generatePositions();
183
- }, [generatePositions, innerTooltipRef]);
181
+ generatePopupPositions();
182
+ }, [generatePopupPositions, innerPopupRef]);
184
183
 
185
184
  /**
186
- * This function is called when we need to remove tooltip portal from DOM and remove event listener to it.
185
+ * This function is called when we need to remove popup portal from DOM and remove event listener to it.
187
186
  */
188
- const unmountTooltip = useCallback(() => {
187
+ const unmountPopupFromDom = useCallback(() => {
189
188
  setVisibleInDom(false);
190
189
  setReverseAnimation(false);
191
190
  window.removeEventListener('scroll', onWindowChangeDetected, true);
192
191
  }, [onWindowChangeDetected]);
193
192
 
194
193
  /**
195
- * This function is called when we need to hide tooltip. A timeout is set to allow animation end, then remove
196
- * tooltip from dom.
194
+ * This function is called when we need to hide popup. A timeout is set to allow animation end, then remove
195
+ * popup from dom.
197
196
  */
198
- const hideTooltip = useCallback(() => {
197
+ const closePopup = useCallback(() => {
199
198
  debounceTimer.current = setTimeout(() => {
200
199
  setReverseAnimation(true);
201
200
  timer.current = setTimeout(() => {
202
- unmountTooltip();
201
+ unmountPopupFromDom();
203
202
  onClose?.();
204
203
  }, animationDuration);
205
204
  }, needDebounce && !disableAnimation ? DEFAULT_DEBOUNCE_DURATION : 0);
206
- }, [animationDuration, disableAnimation, needDebounce, onClose, unmountTooltip]);
205
+ }, [animationDuration, disableAnimation, needDebounce, onClose, unmountPopupFromDom]);
207
206
 
208
207
  /**
209
- * When mouse hover or stop hovering children this function display or hide tooltip. A timeout is set to allow animation
210
- * end, then remove tooltip from dom.
208
+ * When mouse hover or stop hovering children this function display or hide popup. A timeout is set to allow animation
209
+ * end, then remove popup from dom.
211
210
  */
212
211
  const onPointerEvent = useCallback(isVisible => () => {
213
- // This condition is for when we want to unmount the tooltip
214
- // There is debounce in order to avoid tooltip to flicker when we move the mouse from children to tooltip
212
+ // This condition is for when we want to unmount the popup
213
+ // There is debounce in order to avoid popup to flicker when we move the mouse from children to popup
215
214
  // Timer is used to follow the animation duration
216
- if (!isVisible && innerTooltipRef.current && !debounceTimer.current) {
217
- hideTooltip();
215
+ if (!isVisible && innerPopupRef.current && !debounceTimer.current) {
216
+ closePopup();
218
217
  } else if (isVisible) {
219
- // This condition is for when we want to mount the tooltip
220
- // If the timer exists it means the tooltip was about to umount, but we hovered the children again,
221
- // so we clear the timer and the tooltip will not be unmounted
218
+ // This condition is for when we want to mount the popup
219
+ // If the timer exists it means the popup was about to umount, but we hovered the children again,
220
+ // so we clear the timer and the popup will not be unmounted
222
221
  if (timer.current) {
223
222
  setReverseAnimation(false);
224
223
  clearTimeout(timer.current);
225
224
  timer.current = undefined;
226
225
  }
227
- // And here is when we currently are in a debounce timer, it means tooltip was hovered during
226
+ // And here is when we currently are in a debounce timer, it means popup was hovered during
228
227
  // that period, and so we can clear debounce timer
229
228
  if (debounceTimer.current) {
230
229
  clearTimeout(debounceTimer.current);
@@ -232,20 +231,21 @@ const Popup = /*#__PURE__*/forwardRef((_ref15, ref) => {
232
231
  }
233
232
  setVisibleInDom(true);
234
233
  }
235
- }, [hideTooltip, innerTooltipRef]);
234
+ }, [closePopup, innerPopupRef]);
236
235
 
237
236
  /**
238
- * Once tooltip is visible in the dom we can compute positions, then set it visible on screen and add event to
237
+ * Once popup is visible in the dom we can compute positions, then set it visible on screen and add event to
239
238
  * recompute positions on scroll or screen resize.
240
239
  */
241
240
  useEffect(() => {
242
241
  if (visibleInDom) {
243
- generatePositions();
244
-
245
- // We want to detect scroll and resize in order to recompute positions of tooltip
246
- // Adding true as third parameter to event listener will detect nested scrolls.
247
- window.addEventListener('scroll', onWindowChangeDetected, true);
248
- window.addEventListener('resize', onWindowChangeDetected, true);
242
+ generatePopupPositions();
243
+ if (popupPortalTarget === document.body) {
244
+ // We want to detect scroll and resize in order to recompute positions of popup
245
+ // Adding true as third parameter to event listener will detect nested scrolls.
246
+ window.addEventListener('scroll', onWindowChangeDetected, true);
247
+ window.addEventListener('resize', onWindowChangeDetected, true);
248
+ }
249
249
  }
250
250
  return () => {
251
251
  window.removeEventListener('scroll', onWindowChangeDetected, true);
@@ -255,22 +255,17 @@ const Popup = /*#__PURE__*/forwardRef((_ref15, ref) => {
255
255
  timer.current = undefined;
256
256
  }
257
257
  };
258
- }, [generatePositions, onWindowChangeDetected, visibleInDom, maxWidth]);
258
+ }, [generatePopupPositions, onWindowChangeDetected, visibleInDom, maxWidth, popupPortalTarget]);
259
259
 
260
260
  /**
261
- * If tooltip has `visible` prop it means the tooltip is manually controlled through this prop.
262
- * In this cas we don't want to display tooltip on hover, but only when `visible` is true.
261
+ * If popup has `visible` prop it means the popup is manually controlled through this prop.
262
+ * In this cas we don't want to display popup on hover, but only when `visible` is true.
263
263
  */
264
264
  useEffect(() => {
265
265
  if (isControlled) {
266
266
  onPointerEvent(visible)();
267
267
  }
268
268
  }, [isControlled, onPointerEvent, visible]);
269
- const onLocalKeyDown = useCallback(event => {
270
- if (event.code === 'Escape') {
271
- unmountTooltip();
272
- }
273
- }, [unmountTooltip]);
274
269
 
275
270
  // Handle hide on esc press and hide on click outside
276
271
  useEffect(() => {
@@ -278,17 +273,17 @@ const Popup = /*#__PURE__*/forwardRef((_ref15, ref) => {
278
273
  if (event.key === 'Escape') {
279
274
  event.preventDefault();
280
275
  event.stopPropagation();
281
- hideTooltip();
276
+ closePopup();
282
277
  }
283
278
  };
284
279
  const handleClickOutside = event => {
285
- const tooltipCurrent = innerTooltipRef.current;
280
+ const popupCurrent = innerPopupRef.current;
286
281
  const childrenCurrent = childrenRef.current;
287
- if (tooltipCurrent && hideOnClickOutside && !event.defaultPrevented) {
288
- if (event.target && event.target !== tooltipCurrent && event.target !== childrenCurrent && !childrenCurrent?.contains(event.target) && !tooltipCurrent.contains(event.target)) {
282
+ if (popupCurrent && hideOnClickOutside && !event.defaultPrevented) {
283
+ if (event.target && event.target !== popupCurrent && event.target !== childrenCurrent && !childrenCurrent?.contains(event.target) && !popupCurrent.contains(event.target)) {
289
284
  event.preventDefault();
290
285
  event.stopPropagation();
291
- hideTooltip();
286
+ closePopup();
292
287
  }
293
288
  }
294
289
  };
@@ -300,7 +295,35 @@ const Popup = /*#__PURE__*/forwardRef((_ref15, ref) => {
300
295
  document.body.removeEventListener('keyup', handleEscPress);
301
296
  document.body.removeEventListener('click', handleClickOutside);
302
297
  };
303
- }, [hideTooltip, visibleInDom, innerTooltipRef, childrenRef, hideOnClickOutside]);
298
+ }, [closePopup, visibleInDom, innerPopupRef, childrenRef, hideOnClickOutside]);
299
+
300
+ /**
301
+ * This event will occur only for dialog and will trap focus inside the dialog.
302
+ */
303
+ const handleFocusTrap = useCallback(event => {
304
+ const isTabPressed = event.key === 'Tab';
305
+ if (!isTabPressed) {
306
+ return;
307
+ }
308
+ event.stopPropagation();
309
+ const focusableEls = innerPopupRef.current?.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled])') ?? [];
310
+
311
+ // Handle case when no interactive element are within the modal (including close icon)
312
+ if (focusableEls.length === 0) {
313
+ event.preventDefault();
314
+ }
315
+ const firstFocusableEl = focusableEls[0];
316
+ const lastFocusableEl = focusableEls[focusableEls.length - 1];
317
+ if (event.shiftKey) {
318
+ if (document.activeElement === firstFocusableEl || document.activeElement === innerPopupRef.current) {
319
+ lastFocusableEl.focus();
320
+ event.preventDefault();
321
+ }
322
+ } else if (document.activeElement === lastFocusableEl || document.activeElement === innerPopupRef.current) {
323
+ firstFocusableEl.focus();
324
+ event.preventDefault();
325
+ }
326
+ }, []);
304
327
 
305
328
  /**
306
329
  * Will render children conditionally if children is a function or not.
@@ -326,13 +349,12 @@ const Popup = /*#__PURE__*/forwardRef((_ref15, ref) => {
326
349
  tabIndex: tabIndex,
327
350
  onKeyDown: event => {
328
351
  onKeyDown?.(event);
329
- onLocalKeyDown(event);
330
352
  },
331
353
  "data-container-full-width": containerFullWidth,
332
354
  "aria-haspopup": ariaHasPopup,
333
355
  children: children
334
356
  });
335
- }, [ariaHasPopup, children, containerFullWidth, generatedId, isControlled, onKeyDown, onLocalKeyDown, onPointerEvent, tabIndex]);
357
+ }, [ariaHasPopup, children, containerFullWidth, generatedId, isControlled, onKeyDown, onPointerEvent, tabIndex]);
336
358
  if (!text) {
337
359
  if (typeof children === 'function') return null;
338
360
  return jsx(Fragment, {
@@ -347,8 +369,8 @@ const Popup = /*#__PURE__*/forwardRef((_ref15, ref) => {
347
369
  event.nativeEvent.stopImmediatePropagation();
348
370
  };
349
371
  return jsxs(Fragment, {
350
- children: [renderChildren(), visibleInDom ? /*#__PURE__*/createPortal(jsx(StyledTooltip, {
351
- ref: innerTooltipRef,
372
+ children: [renderChildren(), visibleInDom ? /*#__PURE__*/createPortal(jsx(StyledPopup, {
373
+ ref: innerPopupRef,
352
374
  positions: positions,
353
375
  maxWidth: maxWidth,
354
376
  maxHeight: maxHeight,
@@ -360,8 +382,10 @@ const Popup = /*#__PURE__*/forwardRef((_ref15, ref) => {
360
382
  "data-has-arrow": hasArrow,
361
383
  onClick: stopClickPropagation,
362
384
  animationDuration: animationDuration,
385
+ onKeyDown: role === 'dialog' ? handleFocusTrap : undefined,
386
+ isDialog: role === 'dialog',
363
387
  children: text
364
- }), document.body) : null]
388
+ }), popupPortalTarget) : null]
365
389
  });
366
390
  });
367
391
 
@@ -1,6 +1,6 @@
1
1
  import _styled from '@emotion/styled/base';
2
2
  import { Icon } from '@ultraviolet/icons';
3
- import { useState, Children } from 'react';
3
+ import { useReducer, Children } from 'react';
4
4
  import { CopyButton } from '../CopyButton/index.js';
5
5
  import { Expandable } from '../Expandable/index.js';
6
6
  import { Stack } from '../Stack/index.js';
@@ -196,9 +196,10 @@ const Snippet = _ref19 => {
196
196
  hideText = 'Hide',
197
197
  prefix,
198
198
  className,
199
- 'data-testid': dataTestId
199
+ 'data-testid': dataTestId,
200
+ initiallyExpanded
200
201
  } = _ref19;
201
- const [showMore, setShowMore] = useState(false);
202
+ const [showMore, setShowMore] = useReducer(value => !value, initiallyExpanded ?? false);
202
203
  const lines = children.split(LINES_BREAK_REGEX).filter(Boolean);
203
204
  const numberOfLines = lines.length;
204
205
  const multiline = numberOfLines > 1;
@@ -235,7 +236,7 @@ const Snippet = _ref19 => {
235
236
  showMore: showMore,
236
237
  children: jsx(StyledButton, {
237
238
  type: "button",
238
- onClick: () => setShowMore(!showMore),
239
+ onClick: setShowMore,
239
240
  "aria-expanded": showMore,
240
241
  children: jsxs(AlignCenterText, {
241
242
  as: "span",