@ultraviolet/ui 1.25.0 → 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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import react__default, { ReactNode, ComponentProps, ButtonHTMLAttributes, AriaRole, MouseEventHandler, JSX, MouseEvent, InputHTMLAttributes, HTMLAttributeAnchorTarget, AnchorHTMLAttributes, ReactElement, Ref, KeyboardEventHandler, RefObject, HTMLAttributes, CSSProperties, ChangeEventHandler, FocusEventHandler, ForwardRefExoticComponent, ForwardedRef, ElementType, TextareaHTMLAttributes } from 'react';
2
+ import react__default, { ReactNode, ComponentProps, ButtonHTMLAttributes, AriaRole, MouseEventHandler, JSX, MouseEvent, InputHTMLAttributes, HTMLAttributeAnchorTarget, AnchorHTMLAttributes, ReactElement, Ref, RefObject, KeyboardEventHandler, HTMLAttributes, CSSProperties, ChangeEventHandler, FocusEventHandler, ForwardRefExoticComponent, ForwardedRef, ElementType, TextareaHTMLAttributes } from 'react';
3
3
  import * as _emotion_react_jsx_runtime from '@emotion/react/jsx-runtime';
4
4
  import * as _emotion_styled from '@emotion/styled';
5
5
  import * as _emotion_react from '@emotion/react';
@@ -1232,7 +1232,7 @@ type BarStackProps = {
1232
1232
  /**
1233
1233
  * BarStack is a graphic component that is used to show data in one dimension.
1234
1234
  */
1235
- declare const BarStack: ({ data, total, className, "data-testid": dataTestId, }: BarStackProps) => JSX.Element;
1235
+ declare const BarStack: ({ data, total, className, "data-testid": dataTestId, }: BarStackProps) => _emotion_react_jsx_runtime.JSX.Element;
1236
1236
 
1237
1237
  type ItemProps$1 = {
1238
1238
  children: ReactNode;
@@ -1329,8 +1329,8 @@ type CarouselProps = {
1329
1329
  * Carousel component allows you to scroll horizontally through a list of items.
1330
1330
  */
1331
1331
  declare const Carousel: {
1332
- ({ children, className, "data-testid": dataTestId, }: CarouselProps): JSX.Element;
1333
- Item: ({ children, width, }: CarouselItemProps) => JSX.Element;
1332
+ ({ children, className, "data-testid": dataTestId, }: CarouselProps): _emotion_react_jsx_runtime.JSX.Element;
1333
+ Item: ({ children, width, }: CarouselItemProps) => _emotion_react_jsx_runtime.JSX.Element;
1334
1334
  };
1335
1335
 
1336
1336
  type CheckboxProps = {
@@ -1695,7 +1695,7 @@ type PasswordStrengthMeterProps$1 = {
1695
1695
  /**
1696
1696
  * Show strength of a password based on different criteria.
1697
1697
  */
1698
- declare const Meter: ({ strength, title, value, className, "data-testid": dataTestId, id, }: PasswordStrengthMeterProps$1) => JSX.Element;
1698
+ declare const Meter: ({ strength, title, value, className, "data-testid": dataTestId, id, }: PasswordStrengthMeterProps$1) => _emotion_react_jsx_runtime.JSX.Element;
1699
1699
 
1700
1700
  type ModalSize = 'large' | 'medium' | 'small' | 'xsmall' | 'xxsmall';
1701
1701
  type ModalPlacement = 'bottom' | 'bottom-left' | 'bottom-right' | 'center' | 'top' | 'top-left' | 'top-right' | 'right' | 'left';
@@ -1856,7 +1856,7 @@ type PaginationProps = {
1856
1856
  * Pagination is a component to navigate between pages, it is composed of 2 buttons to go to the previous and next page,
1857
1857
  * and a list of buttons to go to a specific page.
1858
1858
  */
1859
- declare const Pagination: ({ disabled, page, pageCount, onChange, pageTabCount, className, "data-testid": dataTestId, }: PaginationProps) => JSX.Element;
1859
+ declare const Pagination: ({ disabled, page, pageCount, onChange, pageTabCount, className, "data-testid": dataTestId, }: PaginationProps) => _emotion_react_jsx_runtime.JSX.Element;
1860
1860
 
1861
1861
  type Rule = {
1862
1862
  name: string;
@@ -1915,7 +1915,7 @@ type PasswordStrengthMeterProps = {
1915
1915
  * PasswordStrengthMeter is a component that displays a password strength meter.
1916
1916
  * @deprecated use Meter component instead
1917
1917
  */
1918
- declare const PasswordStrengthMeter: ({ password, onChange, strength, title, estimate, forbiddenInputs, className, "data-testid": dataTestId, }: PasswordStrengthMeterProps) => JSX.Element;
1918
+ declare const PasswordStrengthMeter: ({ password, onChange, strength, title, estimate, forbiddenInputs, className, "data-testid": dataTestId, }: PasswordStrengthMeterProps) => _emotion_react_jsx_runtime.JSX.Element;
1919
1919
 
1920
1920
  type Data = {
1921
1921
  name?: string | null;
@@ -1961,6 +1961,11 @@ declare const Popover: react.ForwardRefExoticComponent<{
1961
1961
  'data-testid'?: string | undefined;
1962
1962
  maxWidth?: string | undefined;
1963
1963
  maxHeight?: string | undefined;
1964
+ /**
1965
+ * By default, the portal target is children container or document.body if children is a function. You can override this
1966
+ * behavior by setting a portalTarget prop.
1967
+ */
1968
+ portalTarget?: HTMLElement | undefined;
1964
1969
  } & Pick<{
1965
1970
  id?: string | undefined;
1966
1971
  children: ReactNode | ((renderProps: {
@@ -1983,17 +1988,18 @@ declare const Popover: react.ForwardRefExoticComponent<{
1983
1988
  hasArrow?: boolean | undefined;
1984
1989
  onClose?: (() => void) | undefined;
1985
1990
  tabIndex?: number | undefined;
1986
- onKeyDown?: KeyboardEventHandler | undefined;
1991
+ onKeyDown?: react.KeyboardEventHandler | undefined;
1987
1992
  'aria-haspopup'?: boolean | "menu" | "dialog" | "grid" | "listbox" | "tree" | "false" | "true" | undefined;
1988
1993
  hideOnClickOutside?: boolean | undefined;
1989
1994
  needDebounce?: boolean | undefined;
1990
1995
  maxHeight?: string | number | undefined;
1991
1996
  disableAnimation?: boolean | undefined;
1997
+ portalTarget?: HTMLElement | undefined;
1992
1998
  } & react.RefAttributes<HTMLDivElement>, "placement"> & react.RefAttributes<HTMLDivElement>>;
1993
1999
 
1994
2000
  type PopupProps = {
1995
2001
  /**
1996
- * Id is automatically generated if not set. It is used for associating tooltip wrapper with tooltip portal.
2002
+ * Id is automatically generated if not set. It is used for associating popup wrapper with popup portal.
1997
2003
  */
1998
2004
  id?: string;
1999
2005
  children: ReactNode | ((renderProps: {
@@ -2006,20 +2012,20 @@ type PopupProps = {
2006
2012
  }) => ReactNode);
2007
2013
  maxWidth?: number | string;
2008
2014
  /**
2009
- * `auto` placement will change the position of the tooltip if it doesn't fit in the viewport.
2015
+ * `auto` placement will change the position of the popup if it doesn't fit in the viewport.
2010
2016
  */
2011
2017
  placement?: PopupPlacement;
2012
2018
  /**
2013
- * Content of the tooltip, preferably text inside.
2019
+ * Content of the popup, preferably text inside.
2014
2020
  */
2015
2021
  text?: ReactNode;
2016
2022
  className?: string;
2017
2023
  /**
2018
- * It will add `width: 100%` to the tooltip container.
2024
+ * It will add `width: 100%` to the popup container.
2019
2025
  */
2020
2026
  containerFullWidth?: boolean;
2021
2027
  /**
2022
- * It will force display tooltip. This can be useful if you need to always display the tooltip without hover needed.
2028
+ * It will force display popup. This can be useful if you need to always display the popup without hover needed.
2023
2029
  */
2024
2030
  visible?: boolean;
2025
2031
  innerRef?: Ref<HTMLDivElement | null>;
@@ -2040,6 +2046,11 @@ type PopupProps = {
2040
2046
  * Will remove the animation on the popup if set to false.
2041
2047
  */
2042
2048
  disableAnimation?: boolean;
2049
+ /**
2050
+ * By default, the portal target is children container or document.body if children is a function. You can override this
2051
+ * behavior by setting a portalTarget prop.
2052
+ */
2053
+ portalTarget?: HTMLElement;
2043
2054
  };
2044
2055
  /**
2045
2056
  * @experimental This component is experimental and may be subject to breaking changes in the future.
@@ -2200,7 +2211,7 @@ type SeparatorProps = {
2200
2211
  /**
2201
2212
  * Separator component used to separate content with a horizontal or vertical line.
2202
2213
  */
2203
- declare const Separator: ({ direction, thickness, color, icon, className, "data-testid": dataTestId, }: SeparatorProps) => JSX.Element;
2214
+ declare const Separator: ({ direction, thickness, color, icon, className, "data-testid": dataTestId, }: SeparatorProps) => _emotion_react_jsx_runtime.JSX.Element;
2204
2215
 
2205
2216
  declare const variants$1: {
2206
2217
  readonly block: ({ length }: {
@@ -2258,12 +2269,13 @@ type SnippetProps = {
2258
2269
  showText?: string;
2259
2270
  hideText?: string;
2260
2271
  'data-testid'?: string;
2272
+ initiallyExpanded?: boolean;
2261
2273
  } & Pick<ComponentProps<typeof CopyButton>, 'copyText' | 'copiedText'>;
2262
2274
  /**
2263
2275
  * Snippet component is used to display code snippets with the ability to copy the code.
2264
2276
  * It also has the ability to show/hide the code snippet if it has more than 4 lines.
2265
2277
  */
2266
- declare const Snippet: ({ children, copyText, copiedText, showText, hideText, prefix, className, "data-testid": dataTestId, }: SnippetProps) => _emotion_react_jsx_runtime.JSX.Element;
2278
+ declare const Snippet: ({ children, copyText, copiedText, showText, hideText, prefix, className, "data-testid": dataTestId, initiallyExpanded, }: SnippetProps) => _emotion_react_jsx_runtime.JSX.Element;
2267
2279
 
2268
2280
  type StackProps = {
2269
2281
  gap?: keyof UltravioletUITheme['space'] | number;
@@ -2314,7 +2326,7 @@ type StatusProps = {
2314
2326
  /**
2315
2327
  * Status component used to display a colored circle with a tooltip for additional information.
2316
2328
  */
2317
- declare const Status: ({ animated, className, tooltip, sentiment, "data-testid": dataTestId, }: StatusProps) => JSX.Element;
2329
+ declare const Status: ({ animated, className, tooltip, sentiment, "data-testid": dataTestId, }: StatusProps) => _emotion_react_jsx_runtime.JSX.Element;
2318
2330
 
2319
2331
  type Sizes = 'small' | 'medium';
2320
2332
  type ContentProps = {
@@ -2485,10 +2497,11 @@ declare const Tabs: {
2485
2497
  className?: string | undefined;
2486
2498
  counter?: string | number | undefined;
2487
2499
  disabled?: boolean | undefined;
2488
- value?: string | number | undefined;
2489
2500
  onClick?: react.MouseEventHandler<HTMLElement> | undefined;
2490
2501
  onKeyDown?: react.KeyboardEventHandler<HTMLElement> | undefined;
2502
+ subtitle?: string | undefined;
2491
2503
  tooltip?: string | undefined;
2504
+ value?: string | number | undefined;
2492
2505
  } & Omit<any, "tooltip" | "children" | "role" | "className" | "as" | "disabled" | "value" | "badge" | "counter">, "ref"> & react.RefAttributes<HTMLElement>>;
2493
2506
  Menu: react.ForwardRefExoticComponent<{
2494
2507
  children: ReactNode;
@@ -2569,7 +2582,7 @@ type TagInputProps = {
2569
2582
  * TagInput is a component that allows users to input tags.
2570
2583
  * @experimental This component is experimental and may be subject to breaking changes in the future.
2571
2584
  */
2572
- declare const TagInput: ({ disabled, id, manualInput, name, onChange, onChangeError, placeholder, tags, variant, className, "data-testid": dataTestId, }: TagInputProps) => JSX.Element;
2585
+ declare const TagInput: ({ disabled, id, manualInput, name, onChange, onChangeError, placeholder, tags, variant, className, "data-testid": dataTestId, }: TagInputProps) => _emotion_react_jsx_runtime.JSX.Element;
2573
2586
 
2574
2587
  type TagListProps = {
2575
2588
  /**
@@ -2598,7 +2611,7 @@ type TagListProps = {
2598
2611
  /**
2599
2612
  * This component is used to display a list of tags with a threshold and a popover when there are too many tags.
2600
2613
  */
2601
- declare const TagList: ({ maxLength, tags, threshold, multiline, popoverTitle, copiable, copyText, copiedText, className, "data-testid": dataTestId, }: TagListProps) => JSX.Element | null;
2614
+ declare const TagList: ({ maxLength, tags, threshold, multiline, popoverTitle, copiable, copyText, copiedText, className, "data-testid": dataTestId, }: TagListProps) => _emotion_react_jsx_runtime.JSX.Element | null;
2602
2615
 
2603
2616
  declare const PROMINENCES: {
2604
2617
  default: string;
@@ -2800,7 +2813,7 @@ declare const ToggleGroup: {
2800
2813
  Toggle: ({ disabled, name, value, label, helper, className, "data-testid": dataTestId, }: ToggleGroupToggleProps) => _emotion_react_jsx_runtime.JSX.Element;
2801
2814
  };
2802
2815
 
2803
- type TooltipProps = Pick<ComponentProps<typeof Popup>, 'id' | 'children' | 'maxWidth' | 'placement' | 'text' | 'className' | 'visible' | 'innerRef' | 'role' | 'data-testid' | 'containerFullWidth'>;
2816
+ type TooltipProps = Pick<ComponentProps<typeof Popup>, 'id' | 'children' | 'maxWidth' | 'placement' | 'text' | 'className' | 'visible' | 'innerRef' | 'role' | 'data-testid' | 'containerFullWidth' | 'portalTarget'>;
2804
2817
  /**
2805
2818
  * Tooltip component is used to display additional information on hover or focus.
2806
2819
  * It is used to explain the purpose of the element it is attached to.
@@ -2838,7 +2851,7 @@ type VerificationCodeProps = {
2838
2851
  /**
2839
2852
  * Verification code allows you to enter a code in multiple fields (4 by default).
2840
2853
  */
2841
- declare const VerificationCode: ({ disabled, className, error, fields, initialValue, inputId, inputStyle, onChange, onComplete, placeholder, required, type, "data-testid": dataTestId, "aria-label": ariaLabel, }: VerificationCodeProps) => JSX.Element;
2854
+ declare const VerificationCode: ({ disabled, className, error, fields, initialValue, inputId, inputStyle, onChange, onComplete, placeholder, required, type, "data-testid": dataTestId, "aria-label": ariaLabel, }: VerificationCodeProps) => _emotion_react_jsx_runtime.JSX.Element;
2842
2855
 
2843
2856
  type RadioGroupRadioProps = Omit<ComponentProps<typeof Radio>, 'onChange' | 'checked' | 'required'>;
2844
2857
  type RadioGroupProps = {
@@ -2879,6 +2892,11 @@ type MenuProps = {
2879
2892
  'data-testid'?: string;
2880
2893
  maxHeight?: string;
2881
2894
  maxWidth?: string;
2895
+ /**
2896
+ * By default, the portal target is children container or document.body if children is a function. You can override this
2897
+ * behavior by setting a portalTarget prop.
2898
+ */
2899
+ portalTarget?: HTMLElement;
2882
2900
  };
2883
2901
  /**
2884
2902
  * A menu is a widget that offers a list of choices to the user, such as a set of actions or functions.
@@ -53,7 +53,8 @@ const FwdMenu = /*#__PURE__*/forwardRef((_ref7, ref) => {
53
53
  className,
54
54
  'data-testid': dataTestId,
55
55
  maxHeight,
56
- maxWidth
56
+ maxWidth,
57
+ portalTarget
57
58
  } = _ref7;
58
59
  const [isVisible, setIsVisible] = useState(visible);
59
60
  const popupRef = useRef(null);
@@ -67,27 +68,8 @@ const FwdMenu = /*#__PURE__*/forwardRef((_ref7, ref) => {
67
68
  });
68
69
  const innerRef = useRef(target);
69
70
  useImperativeHandle(ref, () => innerRef.current);
70
- const toggleVisible = () => {
71
- setIsVisible(!isVisible);
72
-
73
- // Focus the first item when the menu is opened
74
- if (!isVisible) {
75
- setTimeout(() => {
76
- // We have to wait for the popup to be inserted in the DOM
77
- if (popupRef.current?.firstChild?.firstChild instanceof HTMLElement) {
78
- popupRef.current.firstChild.firstChild.focus();
79
- }
80
- }, 1);
81
- }
82
- };
83
- const onClose = () => {
84
- setIsVisible(false);
85
-
86
- // Focus the disclosure when the menu is closed
87
- disclosureRef.current?.focus();
88
- };
89
71
  const finalDisclosure = /*#__PURE__*/cloneElement(target, {
90
- onClick: toggleVisible,
72
+ onClick: () => setIsVisible(!isVisible),
91
73
  'aria-haspopup': 'dialog',
92
74
  'aria-expanded': isVisible,
93
75
  // @ts-expect-error not sure how to fix this
@@ -105,7 +87,7 @@ const FwdMenu = /*#__PURE__*/forwardRef((_ref7, ref) => {
105
87
  role: "dialog",
106
88
  id: finalId,
107
89
  ref: popupRef,
108
- onClose: onClose,
90
+ onClose: () => setIsVisible(false),
109
91
  tabIndex: -1,
110
92
  maxHeight: maxHeight,
111
93
  maxWidth: maxWidth,
@@ -114,9 +96,10 @@ const FwdMenu = /*#__PURE__*/forwardRef((_ref7, ref) => {
114
96
  className: className,
115
97
  role: "menu",
116
98
  children: typeof children === 'function' ? children({
117
- toggle: toggleVisible
99
+ toggle: () => setIsVisible(!isVisible)
118
100
  }) : children
119
101
  }),
102
+ portalTarget: portalTarget,
120
103
  children: finalDisclosure
121
104
  });
122
105
  });
@@ -11,7 +11,7 @@ const StyledBackdrop = /*#__PURE__*/_styled("div", {
11
11
  theme
12
12
  } = _ref;
13
13
  return theme.colors.overlay;
14
- }, ";&[data-open='true']{padding:", _ref2 => {
14
+ }, ";z-index:1;&[data-open='true']{padding:", _ref2 => {
15
15
  let {
16
16
  theme
17
17
  } = _ref2;
@@ -175,7 +175,7 @@ const Dialog = _ref9 => {
175
175
  event.preventDefault();
176
176
  event.stopPropagation();
177
177
  };
178
- return /*#__PURE__*/createPortal(jsx(StyledBackdrop, {
178
+ return open ? /*#__PURE__*/createPortal(jsx(StyledBackdrop, {
179
179
  "data-open": open,
180
180
  onClick: handleClose,
181
181
  className: backdropClassName,
@@ -201,7 +201,7 @@ const Dialog = _ref9 => {
201
201
  tabIndex: 0,
202
202
  children: open ? children : null
203
203
  })
204
- }), containerRef.current);
204
+ }), containerRef.current) : null;
205
205
  };
206
206
 
207
207
  export { Dialog };
@@ -72,10 +72,6 @@ const ContentWrapper = _ref6 => {
72
72
  children,
73
73
  sentiment
74
74
  } = _ref6;
75
- const buttonRef = useRef(null);
76
- useEffect(() => {
77
- buttonRef.current?.focus();
78
- }, []);
79
75
  return jsxs(StyledStack, {
80
76
  gap: 1,
81
77
  children: [jsxs(Stack, {
@@ -93,8 +89,7 @@ const ContentWrapper = _ref6 => {
93
89
  onClick: onClose,
94
90
  size: "small",
95
91
  icon: "close",
96
- "aria-label": "close",
97
- ref: buttonRef
92
+ "aria-label": "close"
98
93
  })]
99
94
  }), typeof children === 'string' ? jsx(Text, {
100
95
  variant: "bodySmall",
@@ -122,7 +117,8 @@ const Popover = /*#__PURE__*/forwardRef((_ref7, ref) => {
122
117
  className,
123
118
  maxWidth,
124
119
  maxHeight,
125
- 'data-testid': dataTestId
120
+ 'data-testid': dataTestId,
121
+ portalTarget
126
122
  } = _ref7;
127
123
  const innerRef = useRef(null);
128
124
  const [localVisible, setLocalVisible] = useState(visible);
@@ -131,21 +127,9 @@ const Popover = /*#__PURE__*/forwardRef((_ref7, ref) => {
131
127
  useEffect(() => {
132
128
  setLocalVisible(visible);
133
129
  }, [visible]);
134
-
135
- // When space key is pressed we show the popover
136
- const onKeyDownSpace = useCallback(event => {
137
- if (event.code === 'Space') {
138
- event.preventDefault();
139
- event.stopPropagation();
140
- setLocalVisible(true);
141
- }
142
- }, []);
143
-
144
- // When we close we hide the popover and focus the disclosure element
145
130
  const localOnClose = useCallback(() => {
146
131
  setLocalVisible(false);
147
132
  onClose?.();
148
- innerRef.current?.focus();
149
133
  }, [onClose]);
150
134
  return jsx(StyledPopup, {
151
135
  hideOnClickOutside: true,
@@ -164,11 +148,12 @@ const Popover = /*#__PURE__*/forwardRef((_ref7, ref) => {
164
148
  size: size,
165
149
  role: "dialog",
166
150
  ref: ref,
151
+ tabIndex: -1,
167
152
  innerRef: innerRef,
168
153
  onClose: localOnClose,
169
- onKeyDown: onKeyDownSpace,
170
154
  maxWidth: maxWidth,
171
155
  maxHeight: maxHeight,
156
+ portalTarget: portalTarget,
172
157
  children: children
173
158
  });
174
159
  });
@@ -0,0 +1,24 @@
1
+ import { keyframes } from '@emotion/react';
2
+
3
+ const animation = positions => keyframes`
4
+ 0% {
5
+ opacity: 0;
6
+ transform: ${positions.popupInitialPosition};
7
+ }
8
+ 100% {
9
+ opacity: 1;
10
+ transform: ${positions.popupPosition};
11
+ }
12
+ `;
13
+ const exitAnimation = positions => keyframes`
14
+ 0% {
15
+ opacity: 1;
16
+ transform: ${positions.popupPosition};
17
+ }
18
+ 100% {
19
+ opacity: 0;
20
+ transform: ${positions.popupInitialPosition};
21
+ }
22
+ `;
23
+
24
+ export { animation, exitAnimation };
@@ -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",
@@ -1,12 +1,14 @@
1
1
  import _styled from '@emotion/styled/base';
2
2
  import { forwardRef, useMemo } from 'react';
3
3
  import { Badge } from '../Badge/index.js';
4
+ import { Stack } from '../Stack/index.js';
5
+ import { Text } from '../Text/index.js';
4
6
  import { Tooltip } from '../Tooltip/index.js';
5
7
  import { useTabsContext } from './TabsContext.js';
6
8
  import { jsx, jsxs } from '@emotion/react/jsx-runtime';
7
9
 
8
10
  const StyledBadge = /*#__PURE__*/_styled(Badge, {
9
- target: "e1hzf7cr3"
11
+ target: "e1hzf7cr4"
10
12
  })("padding:0 ", _ref => {
11
13
  let {
12
14
  theme
@@ -18,6 +20,9 @@ const StyledBadge = /*#__PURE__*/_styled(Badge, {
18
20
  } = _ref2;
19
21
  return theme.space['1'];
20
22
  }, ";");
23
+ const StyledText = /*#__PURE__*/_styled(Text, {
24
+ target: "e1hzf7cr3"
25
+ })();
21
26
  const StyledTooltip = /*#__PURE__*/_styled(Tooltip, {
22
27
  target: "e1hzf7cr2"
23
28
  })();
@@ -36,7 +41,7 @@ const StyledTabButton = /*#__PURE__*/_styled('button', {
36
41
  theme
37
42
  } = _ref4;
38
43
  return `${theme.space['1']} ${theme.space['2']}`;
39
- }, ";cursor:pointer;justify-content:center;align-items:center;white-space:nowrap;color:", _ref5 => {
44
+ }, ";cursor:pointer;justify-content:center;align-items:baseline;white-space:nowrap;color:", _ref5 => {
40
45
  let {
41
46
  theme
42
47
  } = _ref5;
@@ -81,33 +86,43 @@ const StyledTabButton = /*#__PURE__*/_styled('button', {
81
86
  theme
82
87
  } = _ref13;
83
88
  return theme.colors.primary.border;
84
- }, ";}&[aria-disabled='false']:not(:disabled){&:hover,&:focus,&:active{outline:none;color:", _ref14 => {
89
+ }, ";", StyledText, "{color:", _ref14 => {
85
90
  let {
86
91
  theme
87
92
  } = _ref14;
88
93
  return theme.colors.primary.text;
89
- }, ";border-bottom-color:", _ref15 => {
94
+ }, ";}}&[aria-disabled='false']:not(:disabled){&:hover,&:focus,&:active{outline:none;color:", _ref15 => {
90
95
  let {
91
96
  theme
92
97
  } = _ref15;
93
- return theme.colors.primary.border;
94
- }, ";&[data-is-selected='false']{", StyledBadge, "{background-color:", _ref16 => {
98
+ return theme.colors.primary.text;
99
+ }, ";border-bottom-color:", _ref16 => {
95
100
  let {
96
101
  theme
97
102
  } = _ref16;
98
- return theme.colors.primary.background;
99
- }, ";border-color:", _ref17 => {
103
+ return theme.colors.primary.border;
104
+ }, ";&[data-is-selected='false']{", StyledBadge, "{background-color:", _ref17 => {
100
105
  let {
101
106
  theme
102
107
  } = _ref17;
103
108
  return theme.colors.primary.background;
104
- }, ";color:", _ref18 => {
109
+ }, ";border-color:", _ref18 => {
105
110
  let {
106
111
  theme
107
112
  } = _ref18;
113
+ return theme.colors.primary.background;
114
+ }, ";color:", _ref19 => {
115
+ let {
116
+ theme
117
+ } = _ref19;
118
+ return theme.colors.primary.text;
119
+ }, ";}", StyledText, "{color:", _ref20 => {
120
+ let {
121
+ theme
122
+ } = _ref20;
108
123
  return theme.colors.primary.text;
109
124
  }, ";}}}}&[aria-disabled='true'],&:disabled{cursor:not-allowed;filter:grayscale(1) opacity(50%);}");
110
- const Tab = /*#__PURE__*/forwardRef((_ref19, ref) => {
125
+ const Tab = /*#__PURE__*/forwardRef((_ref21, ref) => {
111
126
  let {
112
127
  as,
113
128
  badge,
@@ -115,12 +130,13 @@ const Tab = /*#__PURE__*/forwardRef((_ref19, ref) => {
115
130
  className,
116
131
  counter,
117
132
  disabled = false,
118
- value,
119
133
  onClick,
120
134
  onKeyDown,
135
+ subtitle,
121
136
  tooltip,
137
+ value,
122
138
  ...props
123
- } = _ref19;
139
+ } = _ref21;
124
140
  const {
125
141
  selected,
126
142
  onChange
@@ -129,7 +145,7 @@ const Tab = /*#__PURE__*/forwardRef((_ref19, ref) => {
129
145
  const isSelected = useMemo(() => value !== undefined && selected === value, [value, selected]);
130
146
  return jsx(StyledTooltip, {
131
147
  text: tooltip,
132
- children: jsxs(StyledTabButton, {
148
+ children: jsx(StyledTabButton, {
133
149
  role: "tab",
134
150
  ref: ref,
135
151
  className: className,
@@ -151,14 +167,31 @@ const Tab = /*#__PURE__*/forwardRef((_ref19, ref) => {
151
167
  },
152
168
  "data-is-selected": isSelected,
153
169
  ...props,
154
- children: [children, typeof counter === 'number' || typeof counter === 'string' ? jsx(StyledBadge, {
155
- sentiment: isSelected ? 'primary' : 'neutral',
156
- prominence: isSelected ? 'strong' : 'default',
157
- size: "medium",
158
- children: counter
159
- }) : null, badge ? jsx(BadgeContainer, {
160
- children: badge
161
- }) : null]
170
+ children: jsxs(Stack, {
171
+ direction: "column",
172
+ gap: 0.5,
173
+ children: [jsxs(Stack, {
174
+ direction: "row",
175
+ alignItems: "center",
176
+ children: [children, typeof counter === 'number' || typeof counter === 'string' ? jsx(StyledBadge, {
177
+ sentiment: isSelected ? 'primary' : 'neutral',
178
+ prominence: isSelected ? 'strong' : 'default',
179
+ size: "medium",
180
+ children: counter
181
+ }) : null, badge ? jsx(BadgeContainer, {
182
+ children: badge
183
+ }) : null]
184
+ }), subtitle ? jsx(Stack, {
185
+ direction: "row",
186
+ children: jsx(StyledText, {
187
+ as: "span",
188
+ variant: "bodySmall",
189
+ sentiment: "neutral",
190
+ prominence: "weak",
191
+ children: subtitle
192
+ })
193
+ }) : null]
194
+ })
162
195
  })
163
196
  });
164
197
  });
@@ -23,6 +23,7 @@ const TabMenu = /*#__PURE__*/forwardRef((_ref2, ref) => {
23
23
  visible,
24
24
  id,
25
25
  disabled,
26
+ className,
26
27
  ...props
27
28
  } = _ref2;
28
29
  return jsx(Menu, {
@@ -34,6 +35,7 @@ const TabMenu = /*#__PURE__*/forwardRef((_ref2, ref) => {
34
35
  "aria-disabled": disabled ?? 'false',
35
36
  disabled: disabled,
36
37
  "aria-haspopup": "menu",
38
+ className: className,
37
39
  ...props,
38
40
  children: [disclosure, jsx(ArrowIcon, {
39
41
  name: "arrow-down"
@@ -1,5 +1,5 @@
1
1
  import _styled from '@emotion/styled/base';
2
- import { useRef, useState, useMemo, useEffect } from 'react';
2
+ import { useRef, useState, useMemo, useEffect, Children, isValidElement, cloneElement } from 'react';
3
3
  import { Tab, StyledTabButton } from './Tab.js';
4
4
  import { TabMenu } from './TabMenu.js';
5
5
  import { TabMenuItem } from './TabMenuItem.js';
@@ -23,7 +23,9 @@ const MenuContainer = /*#__PURE__*/_styled("div", {
23
23
  theme
24
24
  } = _ref3;
25
25
  return `${theme.space['1']} ${theme.space['2']}`;
26
- }, ";border-bottom-width:1px;width:100%;cursor:pointer;min-width:110px;background-color:transparent;}");
26
+ }, ";border-bottom-width:1.5px;width:100%;cursor:pointer;min-width:110px;background-color:transparent;&[aria-disabled='true'],&:disabled{cursor:not-allowed;filter:grayscale(1) opacity(50%);}}");
27
+
28
+ // Migration to MenuV2 will not work as expected here.
27
29
  const StyledTabMenu = /*#__PURE__*/_styled(TabMenu, {
28
30
  target: "ewug27g1"
29
31
  })("position:sticky;right:0;top:0;bottom:0;background:", _ref4 => {
@@ -61,7 +63,7 @@ const Tabs = _ref7 => {
61
63
  ...props
62
64
  } = _ref7;
63
65
  const tabsRef = useRef({});
64
- const moreStaticRef = useRef({});
66
+ const moreStaticRef = useRef(null);
65
67
  const [displayMore, setDisplayMore] = useState(false);
66
68
  const value = useMemo(() => ({
67
69
  onChange,
@@ -82,12 +84,14 @@ const Tabs = _ref7 => {
82
84
  }
83
85
  }, [selected]);
84
86
 
85
- // Change the moreButton style automatically based on the scroll
87
+ // Change the moreButton style automatically based on the scroll to show that a scroll effect is possible.
86
88
  useEffect(() => {
87
89
  const element = tabsRef.current;
88
90
  const moreElement = moreStaticRef.current;
89
91
  const handler = () => {
90
- moreElement.style.boxShadow = element.scrollLeft + SHADOW_THRESHOLD > element.scrollWidth - element.clientWidth ? 'none' : '';
92
+ if (moreElement?.style) {
93
+ moreElement.style.boxShadow = element.scrollLeft + SHADOW_THRESHOLD > element.scrollWidth - element.clientWidth ? 'none' : '';
94
+ }
91
95
  };
92
96
  if (displayMore) {
93
97
  element.addEventListener('scroll', handler);
@@ -96,6 +100,17 @@ const Tabs = _ref7 => {
96
100
  if (displayMore) element.removeEventListener('scroll', handler);
97
101
  };
98
102
  }, [displayMore]);
103
+
104
+ // mapping of tab children to avoid using subtitle props
105
+ const menuItemChildren = Children.map(children, child => {
106
+ if ( /*#__PURE__*/isValidElement(child)) {
107
+ return /*#__PURE__*/cloneElement(child, {
108
+ ...child.props,
109
+ subtitle: null
110
+ });
111
+ }
112
+ return null;
113
+ });
99
114
  return jsx(TabsContext.Provider, {
100
115
  value: value,
101
116
  children: jsxs(TabsContainer, {
@@ -108,7 +123,7 @@ const Tabs = _ref7 => {
108
123
  ref: moreStaticRef,
109
124
  disclosure: moreDisclosure,
110
125
  children: jsx(MenuContainer, {
111
- children: children
126
+ children: menuItemChildren
112
127
  })
113
128
  }) : null]
114
129
  })
@@ -31,7 +31,8 @@ const Tooltip = /*#__PURE__*/forwardRef((_ref, tooltipRef) => {
31
31
  visible,
32
32
  innerRef,
33
33
  role = 'tooltip',
34
- 'data-testid': dataTestId
34
+ 'data-testid': dataTestId,
35
+ portalTarget
35
36
  } = _ref;
36
37
  return jsx(StyledPopup, {
37
38
  id: id,
@@ -45,6 +46,7 @@ const Tooltip = /*#__PURE__*/forwardRef((_ref, tooltipRef) => {
45
46
  placement: placement,
46
47
  text: text,
47
48
  innerRef: innerRef,
49
+ portalTarget: portalTarget,
48
50
  children: children
49
51
  });
50
52
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultraviolet/ui",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "description": "Ultraviolet UI",
5
5
  "homepage": "https://github.com/scaleway/ultraviolet#readme",
6
6
  "repository": {
@@ -39,7 +39,7 @@
39
39
  "react-dom": "18.2.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@babel/core": "7.23.2",
42
+ "@babel/core": "7.23.3",
43
43
  "@emotion/babel-plugin": "11.11.0",
44
44
  "@emotion/react": "11.11.1",
45
45
  "@emotion/styled": "11.11.0",
@@ -67,7 +67,7 @@
67
67
  "react-use-clipboard": "1.0.9",
68
68
  "reakit": "1.3.11",
69
69
  "@ultraviolet/themes": "1.5.0",
70
- "@ultraviolet/icons": "2.5.4"
70
+ "@ultraviolet/icons": "2.5.5"
71
71
  },
72
72
  "scripts": {
73
73
  "build": "rollup -c ../../rollup.config.mjs"