@apify/ui-library 1.91.0 → 1.92.1-featimprovetooltip-7e1224.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/src/components/floating/floating_component_base.d.ts +41 -0
  2. package/dist/src/components/floating/floating_component_base.d.ts.map +1 -0
  3. package/dist/src/components/floating/floating_component_base.js +151 -0
  4. package/dist/src/components/floating/floating_component_base.js.map +1 -0
  5. package/dist/src/components/floating/index.d.ts +2 -0
  6. package/dist/src/components/floating/index.d.ts.map +1 -1
  7. package/dist/src/components/floating/index.js +2 -0
  8. package/dist/src/components/floating/index.js.map +1 -1
  9. package/dist/src/components/floating/tooltip.d.ts +35 -0
  10. package/dist/src/components/floating/tooltip.d.ts.map +1 -0
  11. package/dist/src/components/floating/tooltip.js +56 -0
  12. package/dist/src/components/floating/tooltip.js.map +1 -0
  13. package/dist/src/components/floating/tooltip_content.d.ts +5 -0
  14. package/dist/src/components/floating/tooltip_content.d.ts.map +1 -0
  15. package/dist/src/components/floating/tooltip_content.js +51 -0
  16. package/dist/src/components/floating/tooltip_content.js.map +1 -0
  17. package/dist/src/components/index.d.ts +1 -0
  18. package/dist/src/components/index.d.ts.map +1 -1
  19. package/dist/src/components/index.js +1 -0
  20. package/dist/src/components/index.js.map +1 -1
  21. package/dist/src/components/shortcut.d.ts +9 -0
  22. package/dist/src/components/shortcut.d.ts.map +1 -0
  23. package/dist/src/components/shortcut.js +28 -0
  24. package/dist/src/components/shortcut.js.map +1 -0
  25. package/dist/tsconfig.build.tsbuildinfo +1 -1
  26. package/package.json +3 -3
  27. package/src/components/button.stories.jsx +17 -19
  28. package/src/components/floating/floating_component_base.tsx +263 -0
  29. package/src/components/floating/index.ts +2 -0
  30. package/src/components/floating/tooltip.stories.jsx +128 -0
  31. package/src/components/floating/tooltip.tsx +120 -0
  32. package/src/components/floating/tooltip_content.tsx +80 -0
  33. package/src/components/index.ts +1 -0
  34. package/src/components/shortcut.stories.jsx +57 -0
  35. package/src/components/shortcut.tsx +54 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apify/ui-library",
3
- "version": "1.91.0",
3
+ "version": "1.92.1-featimprovetooltip-7e1224.30+6abbff38cad",
4
4
  "description": "React UI library used by apify.com",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -26,7 +26,7 @@
26
26
  "It's not nice, but helps us to get around the problem of multiple react instances."
27
27
  ],
28
28
  "dependencies": {
29
- "@apify/ui-icons": "^1.15.1",
29
+ "@apify/ui-icons": "^1.16.0",
30
30
  "@floating-ui/react": "^0.26.2",
31
31
  "@react-hook/resize-observer": "^2.0.2",
32
32
  "clsx": "^2.0.0",
@@ -66,5 +66,5 @@
66
66
  "src",
67
67
  "style"
68
68
  ],
69
- "gitHead": "b3fa3cfa3a161f319afedcd69317e9f153719626"
69
+ "gitHead": "6abbff38cad3234fbbecd94b382df5fc97f644ae"
70
70
  }
@@ -1,8 +1,6 @@
1
- import React from 'react';
2
1
  import styled from 'styled-components';
3
2
 
4
- // eslint-disable-next-line import/no-extraneous-dependencies
5
- import { X16 } from '@apify/icons';
3
+ import { CrossIcon } from '@apify/ui-icons';
6
4
 
7
5
  import { theme } from '../design_system/theme.ts';
8
6
  import { Button } from './button.tsx';
@@ -53,50 +51,50 @@ const ButtonSection = ({
53
51
 
54
52
  <h6>With leading icon</h6>
55
53
  <ButtonGrid>
56
- <Button {...props} LeftIcon={X16} variant='primary'>Primary</Button>
57
- <Button {...props} LeftIcon={X16} variant='secondary'>Secondary</Button>
58
- <Button {...props} LeftIcon={X16} variant='tertiary'>Tertiary</Button>
59
- <Button {...props} LeftIcon={X16} disabled>Disabled</Button>
54
+ <Button {...props} LeftIcon={CrossIcon} variant='primary'>Primary</Button>
55
+ <Button {...props} LeftIcon={CrossIcon} variant='secondary'>Secondary</Button>
56
+ <Button {...props} LeftIcon={CrossIcon} variant='tertiary'>Tertiary</Button>
57
+ <Button {...props} LeftIcon={CrossIcon} disabled>Disabled</Button>
60
58
  </ButtonGrid>
61
59
 
62
60
  <h6>With trailing icon</h6>
63
61
  <ButtonGrid>
64
- <Button {...props} RightIcon={X16} variant='primary'>Primary</Button>
65
- <Button {...props} RightIcon={X16} variant='secondary'>Secondary</Button>
66
- <Button {...props} RightIcon={X16} variant='tertiary'>Tertiary</Button>
67
- <Button {...props} RightIcon={X16} disabled>Disabled</Button>
62
+ <Button {...props} RightIcon={CrossIcon} variant='primary'>Primary</Button>
63
+ <Button {...props} RightIcon={CrossIcon} variant='secondary'>Secondary</Button>
64
+ <Button {...props} RightIcon={CrossIcon} variant='tertiary'>Tertiary</Button>
65
+ <Button {...props} RightIcon={CrossIcon} disabled>Disabled</Button>
68
66
  </ButtonGrid>
69
67
 
70
68
  <h6>With both icons</h6>
71
69
  <ButtonGrid>
72
70
  <Button
73
71
  {...props}
74
- RightIcon={X16}
75
- LeftIcon={X16}
72
+ RightIcon={CrossIcon}
73
+ LeftIcon={CrossIcon}
76
74
  variant='primary'
77
75
  >
78
76
  Primary test
79
77
  </Button>
80
78
  <Button
81
79
  {...props}
82
- RightIcon={X16}
83
- LeftIcon={X16}
80
+ RightIcon={CrossIcon}
81
+ LeftIcon={CrossIcon}
84
82
  variant='secondary'
85
83
  >
86
84
  Secondary
87
85
  </Button>
88
86
  <Button
89
87
  {...props}
90
- RightIcon={X16}
91
- LeftIcon={X16}
88
+ RightIcon={CrossIcon}
89
+ LeftIcon={CrossIcon}
92
90
  variant='tertiary'
93
91
  >
94
92
  Tertiary
95
93
  </Button>
96
94
  <Button
97
95
  {...props}
98
- RightIcon={X16}
99
- LeftIcon={X16}
96
+ RightIcon={CrossIcon}
97
+ LeftIcon={CrossIcon}
100
98
  disabled
101
99
  >
102
100
  Disabled
@@ -0,0 +1,263 @@
1
+ import {
2
+ arrow,
3
+ autoPlacement,
4
+ autoUpdate,
5
+ flip,
6
+ FloatingPortal,
7
+ hide,
8
+ offset,
9
+ shift,
10
+ type Strategy,
11
+ useFloating,
12
+ } from '@floating-ui/react';
13
+ import clsx from 'clsx';
14
+ import { type ComponentType, type CSSProperties, forwardRef, type MouseEvent, type ReactNode, type RefObject, useMemo, useRef } from 'react';
15
+ import styled, { keyframes } from 'styled-components';
16
+
17
+ import { theme } from '../../design_system/theme.js';
18
+
19
+ export const classNames = {
20
+ ARROW: 'FloatingComponent-arrow',
21
+ CHILDREN: 'FloatingComponent-children',
22
+ };
23
+
24
+ export const FLOATING_PLACEMENT = {
25
+ TOP: 'top',
26
+ TOP_START: 'top-start',
27
+ TOP_END: 'top-end',
28
+ RIGHT: 'right',
29
+ RIGHT_START: 'right-start',
30
+ RIGHT_END: 'right-end',
31
+ BOTTOM: 'bottom',
32
+ BOTTOM_START: 'bottom-start',
33
+ BOTTOM_END: 'bottom-end',
34
+ LEFT: 'left',
35
+ LEFT_START: 'left-start',
36
+ LEFT_END: 'left-end',
37
+ } as const;
38
+
39
+ export type FloatingPlacement = typeof FLOATING_PLACEMENT[keyof typeof FLOATING_PLACEMENT];
40
+
41
+ interface FadeInProps {
42
+ children: ReactNode;
43
+ duration?: number;
44
+ delay?: number;
45
+ $minHeight?: string;
46
+ }
47
+
48
+ interface FloatingComponentWrapStyledProps {
49
+ arrowRotationDegs: number;
50
+ }
51
+
52
+ interface FloatingComponentWrapProps extends FloatingComponentWrapStyledProps {
53
+ showInPortal?: boolean;
54
+ className?: string;
55
+ style?: CSSProperties;
56
+ onClick?: (e: MouseEvent<HTMLElement>) => void;
57
+ children: ReactNode;
58
+ }
59
+
60
+ export interface FloatingComponentBaseProps {
61
+ placement?: FloatingPlacement;
62
+ autoPlacements?: FloatingPlacement[];
63
+ strategy?: Strategy;
64
+ content?: ReactNode | string;
65
+ children?: ReactNode;
66
+ isOpen?: boolean;
67
+ triggerRef?: RefObject<HTMLDivElement>;
68
+ className?: string;
69
+ offsetPx?: number;
70
+ contentWrapClassName?: string;
71
+ CloseButtonComponent?: ComponentType;
72
+ showInPortal?: boolean;
73
+ }
74
+
75
+ const fadeIn = keyframes`
76
+ from {
77
+ opacity: 0;
78
+ }
79
+ to {
80
+ opacity: 1;
81
+ }
82
+ `;
83
+
84
+ const FadeIn = styled.div<FadeInProps>`
85
+ @media (prefers-reduced-motion: no-preference) {
86
+ animation-name: ${fadeIn};
87
+ animation-fill-mode: backwards;
88
+ animation-duration: ${(props) => `${props.duration || 240}ms`};
89
+ animation-delay: ${(props) => `${props.delay || 0}ms`};
90
+ }
91
+
92
+ min-height: ${({ $minHeight }) => $minHeight || 'unset'};
93
+ `;
94
+
95
+ const FloatingComponentWrapStyled = styled.span<FloatingComponentWrapStyledProps>`
96
+ box-shadow: ${theme.shadow.shadow2};
97
+ padding: ${theme.space.space16};
98
+ ${theme.typography.shared.mobile.bodyM};
99
+ border-radius: 0.8rem;
100
+ z-index: 999;
101
+ white-space: normal;
102
+ word-break: break-word;
103
+ cursor: default;
104
+ text-align: left;
105
+
106
+ .${classNames.ARROW} {
107
+ position: absolute;
108
+ background-color: inherit;
109
+ border: inherit;
110
+ width: 10px;
111
+ height: 10px;
112
+ transform: ${({ arrowRotationDegs }) => `rotate(${arrowRotationDegs}deg)`} ;
113
+ z-index:-1;
114
+ }
115
+
116
+ @media ${theme.device.tablet} {
117
+ ${theme.typography.shared.tablet.bodyM};
118
+ }
119
+
120
+ @media ${theme.device.desktop} {
121
+ ${theme.typography.shared.desktop.bodyM};
122
+ }
123
+ `;
124
+
125
+ const ChildrenWrap = styled.div`
126
+ .${classNames.CHILDREN} {
127
+ width: fit-content;
128
+ }
129
+ `;
130
+
131
+ const StyledPopoverBox = styled.div`
132
+ display: flex;
133
+ align-items: center;
134
+ gap: ${theme.space.space4};
135
+ button {
136
+ color: inherit;
137
+ }
138
+ `;
139
+
140
+ const FloatingComponentWrap = forwardRef<HTMLSpanElement, FloatingComponentWrapProps>((props, ref) => {
141
+ const { showInPortal, ...rest } = props;
142
+ const component = <FloatingComponentWrapStyled {...rest} ref={ref} />;
143
+ if (showInPortal) {
144
+ return <FloatingPortal>{component}</FloatingPortal>;
145
+ }
146
+ return component;
147
+ });
148
+
149
+ FloatingComponentWrap.displayName = 'FloatingComponentWrap';
150
+
151
+ /**
152
+ * This is just the base component for Tooltips and Popovers.
153
+ * Don't use this - use Tooltip/HelpPopover instead.
154
+ */
155
+ export const FloatingComponentBase = ({
156
+ placement = FLOATING_PLACEMENT.TOP,
157
+ autoPlacements,
158
+ strategy = 'fixed',
159
+ content,
160
+ children,
161
+ isOpen = false,
162
+ triggerRef,
163
+ className,
164
+ offsetPx = 10,
165
+ contentWrapClassName,
166
+ CloseButtonComponent,
167
+ showInPortal = false,
168
+ }: FloatingComponentBaseProps) => {
169
+ const arrowRef = useRef<HTMLDivElement>(null);
170
+
171
+ const {
172
+ x,
173
+ y,
174
+ refs: { setReference, setFloating },
175
+ placement: effectivePlacement,
176
+ strategy: effectiveStrategy,
177
+ middlewareData: {
178
+ arrow: { x: arrowX, y: arrowY } = {},
179
+ hide: refHidden,
180
+ },
181
+ } = useFloating({
182
+ placement,
183
+ strategy,
184
+ whileElementsMounted: autoUpdate,
185
+ middleware: [
186
+ offset(offsetPx),
187
+ autoPlacements?.length ? autoPlacement({ allowedPlacements: autoPlacements }) : flip(),
188
+ shift({ padding: 5 }),
189
+ arrow({ element: arrowRef, padding: 9 }),
190
+ hide({
191
+ strategy: 'referenceHidden',
192
+ }),
193
+ ],
194
+ });
195
+
196
+ const arrowStyle = useMemo(() => {
197
+ const staticSide = {
198
+ top: 'bottom',
199
+ right: 'left',
200
+ bottom: 'top',
201
+ left: 'right',
202
+ }[effectivePlacement.split('-')[0] as 'top' | 'right' | 'bottom' | 'left'];
203
+
204
+ let borderNone: CSSProperties = { borderLeft: 0, borderTop: 0 };
205
+ if (staticSide === 'bottom') borderNone = { borderRight: 0, borderBottom: 0 };
206
+ if (staticSide === 'right') borderNone = { borderLeft: 0, borderBottom: 0 };
207
+ if (staticSide === 'left') borderNone = { borderRight: 0, borderTop: 0 };
208
+
209
+ const style: CSSProperties = {
210
+ left: arrowX != null ? `${arrowX}px` : '',
211
+ top: arrowY != null ? `${arrowY}px` : '',
212
+ ...borderNone,
213
+ };
214
+
215
+ if (staticSide) {
216
+ (style as Record<string, string | number>)[staticSide] = '-6px';
217
+ }
218
+
219
+ return style;
220
+ }, [arrowX, arrowY, effectivePlacement]);
221
+
222
+ const arrowRotationDegs = effectivePlacement.includes(FLOATING_PLACEMENT.TOP) || effectivePlacement.includes(FLOATING_PLACEMENT.BOTTOM) ? 225 : 45;
223
+
224
+ if (!content) return <span>{children}</span>;
225
+
226
+ return (
227
+ <>
228
+ {/* Adding className to children for easier identifying in DevTools */}
229
+ <ChildrenWrap className={clsx(classNames.CHILDREN, contentWrapClassName)} ref={setReference}>
230
+ {children}
231
+ </ChildrenWrap>
232
+ {isOpen && (
233
+ <FloatingComponentWrap arrowRotationDegs={arrowRotationDegs}
234
+ className={className}
235
+ ref={setFloating}
236
+ style={{
237
+ position: effectiveStrategy,
238
+ top: y ?? 0,
239
+ left: x ?? 0,
240
+ width: 'max-content',
241
+ visibility: refHidden?.referenceHidden ? 'hidden' : 'visible',
242
+ }}
243
+ onClick={(e) => e.stopPropagation()}
244
+ showInPortal={showInPortal}
245
+ >
246
+ <FadeIn delay={60} ref={triggerRef}>
247
+ {CloseButtonComponent
248
+ ? <StyledPopoverBox>
249
+ {content}
250
+ <CloseButtonComponent />
251
+ </StyledPopoverBox>
252
+ : content}
253
+ </FadeIn>
254
+ <div
255
+ ref={arrowRef}
256
+ className={classNames.ARROW}
257
+ style={arrowStyle}
258
+ />
259
+ </FloatingComponentWrap>
260
+ )}
261
+ </>
262
+ );
263
+ };
@@ -1,3 +1,5 @@
1
1
  export * from './menu.js';
2
2
  export * from './menu_components.js';
3
3
  export * from './menu_common.js';
4
+ export * from './tooltip.js';
5
+ export { FLOATING_PLACEMENT, type FloatingPlacement } from './floating_component_base.js';
@@ -0,0 +1,128 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import { FLOATING_PLACEMENT } from './floating_component_base.tsx';
5
+ import { Tooltip } from './tooltip.tsx';
6
+
7
+ const Child = () => <div style={{ padding: '8px', border: '1px solid black', borderRadius: '4px' }}>Hover me</div>;
8
+
9
+ const longText = 'This is a tooltip with a longer text. This is a tooltip with a longer text. This is a tooltip with a longer text.';
10
+
11
+ export default {
12
+ title: 'UI-Library/Tooltip',
13
+ component: Tooltip,
14
+ parameters: {
15
+ design: {
16
+ type: 'figma',
17
+ url: 'https://www.figma.com/design/duSsGnk84UMYav8mg8QNgR/%F0%9F%93%96-Shared-library?node-id=5236-98464&p=f&m=dev',
18
+ },
19
+ },
20
+ decorators: [
21
+ (Story) => (
22
+ <div style={{ margin: '3em', width: 'fit-content' }}>
23
+ <Story />
24
+ </div>
25
+ ),
26
+ ],
27
+ args: {
28
+ children: <Child />,
29
+ content: 'This is a tooltip',
30
+ },
31
+ };
32
+
33
+ const DefaultStoryWrapper = styled.div`
34
+ display: grid;
35
+ grid-template-columns: 1fr 1fr 1fr;
36
+ gap: 10em;
37
+ row-gap: 20em;
38
+ `;
39
+
40
+ export const Default = (args) => {
41
+ return (<DefaultStoryWrapper>
42
+ <div>
43
+ Default:
44
+ <Tooltip {...args}/>
45
+ </div>
46
+ <div>
47
+ With shortcuts:
48
+ <Tooltip shortcuts={['Ctrl / ⌘', 'F']} {...args}/>
49
+ </div>
50
+ <div>
51
+ With shortcuts and long text:
52
+ <Tooltip
53
+ persistent={{ isOpenOverride: true }}
54
+ shortcuts={['Ctrl / ⌘', 'F']}
55
+ content={longText}
56
+ ><Child /></Tooltip>
57
+ </div>
58
+ <div>
59
+ With image:
60
+ <Tooltip imageUrl='https://picsum.photos/id/1/1024' {...args} />
61
+ </div>
62
+ <div>
63
+ With subtleText:
64
+ <Tooltip subtleText="This is some subtle text" {...args} />
65
+ </div>
66
+ <div>
67
+ With everything:
68
+ <Tooltip
69
+ persistent={{ isOpenOverride: true }}
70
+ shortcuts={['Ctrl / ⌘', 'F']}
71
+ imageUrl='https://picsum.photos/id/1/1024'
72
+ subtleText="This is some subtle text"
73
+ placement={FLOATING_PLACEMENT.BOTTOM}
74
+ content={longText}
75
+ ><Child /></Tooltip>
76
+ </div>
77
+ </DefaultStoryWrapper>
78
+ );
79
+ };
80
+ Default.args = {
81
+ persistent: { isOpenOverride: true },
82
+ };
83
+
84
+ export const Sizes = () => {
85
+ return (<DefaultStoryWrapper>
86
+ <div>
87
+ Xsmall:
88
+ <Tooltip size="xsmall" content={longText}><Child /></Tooltip>
89
+ </div>
90
+ <div>
91
+ Small (default):
92
+ <Tooltip size="small" content={longText}><Child /></Tooltip>
93
+ </div>
94
+ <div>
95
+ Medium:
96
+ <Tooltip size="medium" content={longText}><Child /></Tooltip>
97
+ </div>
98
+ <div>
99
+ Large:
100
+ <Tooltip size="large" content={longText}><Child /></Tooltip>
101
+ </div>
102
+ <div>
103
+ Xlarge:
104
+ <Tooltip size="xlarge" content={longText}><Child /></Tooltip>
105
+ </div>
106
+ </DefaultStoryWrapper>
107
+ );
108
+ };
109
+
110
+ export const Playground = (args) => {
111
+ return (
112
+ <Tooltip {...args}/>
113
+ );
114
+ };
115
+ Playground.argTypes = {
116
+ content: { control: 'text' },
117
+ shortcuts: { control: 'array' },
118
+ imageUrl: { control: 'text' },
119
+ subtleText: { control: 'text' },
120
+ size: {
121
+ control: 'select',
122
+ options: ['xsmall', 'small', 'medium', 'large', 'xlarge'],
123
+ },
124
+ placement: {
125
+ control: 'select',
126
+ options: Object.values(FLOATING_PLACEMENT),
127
+ },
128
+ };
@@ -0,0 +1,120 @@
1
+ import {
2
+ useFloating,
3
+ useHover,
4
+ useInteractions,
5
+ } from '@floating-ui/react';
6
+ import { type ComponentType, forwardRef, useState } from 'react';
7
+ import styled from 'styled-components';
8
+
9
+ import { theme } from '../../design_system/theme.js';
10
+ import { FloatingComponentBase, type FloatingComponentBaseProps } from './floating_component_base.js';
11
+ import { TooltipContent } from './tooltip_content.js';
12
+
13
+ export const TOOLTIP_SIZES = {
14
+ XSMALL: 'xsmall',
15
+ SMALL: 'small',
16
+ MEDIUM: 'medium',
17
+ LARGE: 'large', /* Previously WIDE */
18
+ XLARGE: 'xlarge', /* Previously WIDER */
19
+ } as const;
20
+
21
+ export type TooltipSize = typeof TOOLTIP_SIZES[keyof typeof TOOLTIP_SIZES];
22
+
23
+ interface PersistentTooltipProps {
24
+ isOpenOverride?: boolean;
25
+ CloseButtonComponent?: ComponentType;
26
+ }
27
+
28
+ export interface TooltipProps extends Omit<FloatingComponentBaseProps, 'isOpen' | 'size'> {
29
+ as?: keyof JSX.IntrinsicElements | ComponentType<unknown>;
30
+ className?: string;
31
+ persistent?: PersistentTooltipProps;
32
+ delayShow?: number;
33
+ delayHide?: number;
34
+ shortcuts?: string[];
35
+ imageUrl?: string;
36
+ subtleText?: string;
37
+ size?: TooltipSize;
38
+ }
39
+
40
+ interface WithTooltipProps {
41
+ tooltipProps?: TooltipProps;
42
+ }
43
+
44
+ // Using a styled component to get access to the `as` prop
45
+ const TooltipFocusArea = styled.span``;
46
+
47
+ const StyledFloatingComponentBase = styled(FloatingComponentBase)`
48
+ color: ${theme.colorPalette.dark.neutral0};
49
+ background-color: ${theme.colorPalette.dark.neutral900};
50
+ border: 1px solid ${theme.color.neutral.smallTooltipBorder};
51
+ padding: ${theme.space.space8};
52
+ `;
53
+
54
+ /**
55
+ * Tooltip appears on hover, for onclick use Popover
56
+ */
57
+ export const Tooltip = ({
58
+ as,
59
+ className,
60
+ persistent,
61
+ delayShow = 500,
62
+ delayHide = 50,
63
+ shortcuts = [],
64
+ imageUrl,
65
+ subtleText,
66
+ size = TOOLTIP_SIZES.SMALL,
67
+ ...rest
68
+ }: TooltipProps) => {
69
+ const { isOpenOverride, CloseButtonComponent } = persistent || {};
70
+ const [open, setOpen] = useState(false);
71
+
72
+ const { refs, context } = useFloating({
73
+ open,
74
+ onOpenChange: setOpen,
75
+ });
76
+
77
+ const hover = useHover(context, {
78
+ delay: {
79
+ open: delayShow,
80
+ close: delayHide,
81
+ },
82
+ });
83
+
84
+ const { getReferenceProps, getFloatingProps } = useInteractions([hover]);
85
+
86
+ const tooltipProps = {
87
+ ...rest,
88
+ isOpen: isOpenOverride !== undefined ? isOpenOverride : open,
89
+ content: <TooltipContent content={rest.content} shortcuts={shortcuts} imageUrl={imageUrl} subtleText={subtleText} size={size} />,
90
+ };
91
+
92
+ return (
93
+ <TooltipFocusArea
94
+ as={as as keyof JSX.IntrinsicElements}
95
+ className={className}
96
+ ref={refs.setReference}
97
+ {...getReferenceProps()}
98
+ >
99
+ <div
100
+ ref={refs.setFloating}
101
+ {...getFloatingProps()}
102
+ >
103
+ <StyledFloatingComponentBase {...tooltipProps} CloseButtonComponent={CloseButtonComponent} />
104
+ </div>
105
+ </TooltipFocusArea>
106
+ );
107
+ };
108
+
109
+ export function withTooltip<T extends Record<string, unknown> = Record<string, unknown>>(Component: ComponentType<T>) {
110
+ const Enhanced = forwardRef<HTMLElement, T & WithTooltipProps>(({ tooltipProps, ...rest }, ref) => {
111
+ if (!tooltipProps) return <Component {...(rest as unknown as T)} ref={ref} />;
112
+ return <Tooltip {...tooltipProps}>
113
+ <Component {...(rest as unknown as T)} ref={ref} />
114
+ </Tooltip>;
115
+ });
116
+
117
+ Enhanced.displayName = `WithTooltip:${Component.displayName || Component.name}`;
118
+
119
+ return Enhanced;
120
+ }
@@ -0,0 +1,80 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ import { theme } from '../../design_system/theme.js';
4
+ import { Shortcut } from '../shortcut.js';
5
+ import { TOOLTIP_SIZES, type TooltipProps, type TooltipSize } from './tooltip.js';
6
+
7
+ type ContentProps = Pick<TooltipProps, 'content' | 'shortcuts' | 'imageUrl' | 'subtleText' | 'size'>;
8
+
9
+ const TOOLTIP_SIZES_VALUES: Record<TooltipSize, string> = {
10
+ xsmall: '24rem',
11
+ small: '32rem',
12
+ medium: '40rem',
13
+ large: '48rem',
14
+ xlarge: '64rem',
15
+ };
16
+
17
+ const StyledContent = styled.div<{ $size: TooltipSize }>`
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: ${theme.space.space8};
21
+ /* Size - (tooltip padding + border) * 2 */
22
+ max-width: ${({ $size }) => css`calc(${TOOLTIP_SIZES_VALUES[$size]} - (${theme.space.space8} + 1px) * 2)`};
23
+
24
+ .Tooltip-image {
25
+ max-width: 100%;
26
+ max-height: 150px;
27
+ border-radius: 4px;
28
+ object-fit: contain;
29
+ }
30
+
31
+ .Tooltip-textContent {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: ${theme.space.space8};
35
+
36
+ /* When there is no child with subtleText class */
37
+ &:not(:has(.Tooltip-subtleText)){
38
+ flex-direction: row;
39
+ flex-wrap: wrap;
40
+ }
41
+
42
+ .Tooltip-subtleText {
43
+ color: ${theme.colorPalette.dark.neutral500};
44
+ }
45
+
46
+ .Tooltip-shortcutContainer{
47
+ display: flex;
48
+ gap: ${theme.space.space4};
49
+ }
50
+ }
51
+ `;
52
+
53
+ export const TooltipContent = ({ content, shortcuts = [], imageUrl, subtleText, size = TOOLTIP_SIZES.SMALL }: ContentProps) => {
54
+ return (
55
+ <StyledContent $size={size}>
56
+ {imageUrl && (
57
+ <img
58
+ src={imageUrl}
59
+ alt=""
60
+ className="Tooltip-image"
61
+ />
62
+ )}
63
+ <div className="Tooltip-textContent">
64
+ {content}
65
+ {subtleText && (
66
+ <div className="Tooltip-subtleText">
67
+ {subtleText}
68
+ </div>
69
+ )}
70
+ {shortcuts.length > 0 && (
71
+ <div className='Tooltip-shortcutContainer'>
72
+ {shortcuts.map((shortcut, index) => (
73
+ <Shortcut key={`${shortcut}-${index}`} dark>{shortcut}</Shortcut>
74
+ ))}
75
+ </div>
76
+ )}
77
+ </div>
78
+ </StyledContent>
79
+ );
80
+ };