@apify/ui-library 1.92.0 → 1.92.1-featimprovetooltip-7e1224.32
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/src/components/floating/floating_component_base.d.ts +41 -0
- package/dist/src/components/floating/floating_component_base.d.ts.map +1 -0
- package/dist/src/components/floating/floating_component_base.js +151 -0
- package/dist/src/components/floating/floating_component_base.js.map +1 -0
- package/dist/src/components/floating/index.d.ts +2 -0
- package/dist/src/components/floating/index.d.ts.map +1 -1
- package/dist/src/components/floating/index.js +2 -0
- package/dist/src/components/floating/index.js.map +1 -1
- package/dist/src/components/floating/tooltip.d.ts +35 -0
- package/dist/src/components/floating/tooltip.d.ts.map +1 -0
- package/dist/src/components/floating/tooltip.js +56 -0
- package/dist/src/components/floating/tooltip.js.map +1 -0
- package/dist/src/components/floating/tooltip_content.d.ts +5 -0
- package/dist/src/components/floating/tooltip_content.d.ts.map +1 -0
- package/dist/src/components/floating/tooltip_content.js +51 -0
- package/dist/src/components/floating/tooltip_content.js.map +1 -0
- package/dist/src/components/index.d.ts +1 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/components/index.js +1 -0
- package/dist/src/components/index.js.map +1 -1
- package/dist/src/components/shortcut.d.ts +9 -0
- package/dist/src/components/shortcut.d.ts.map +1 -0
- package/dist/src/components/shortcut.js +28 -0
- package/dist/src/components/shortcut.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/floating/floating_component_base.tsx +263 -0
- package/src/components/floating/index.ts +2 -0
- package/src/components/floating/tooltip.stories.jsx +128 -0
- package/src/components/floating/tooltip.tsx +120 -0
- package/src/components/floating/tooltip_content.tsx +80 -0
- package/src/components/index.ts +1 -0
- package/src/components/shortcut.stories.jsx +57 -0
- 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.92.
|
|
3
|
+
"version": "1.92.1-featimprovetooltip-7e1224.32+2b6834918f7",
|
|
4
4
|
"description": "React UI library used by apify.com",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"src",
|
|
67
67
|
"style"
|
|
68
68
|
],
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "2b6834918f728c0d3d078ce95e5e3cbb007f9a7d"
|
|
70
70
|
}
|
|
@@ -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
|
+
};
|
|
@@ -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
|
+
};
|
package/src/components/index.ts
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Shortcut } from './shortcut.tsx';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'UI-Library/Shortcut',
|
|
7
|
+
component: Shortcut,
|
|
8
|
+
parameters: {
|
|
9
|
+
design: {
|
|
10
|
+
type: 'figma',
|
|
11
|
+
url: 'https://www.figma.com/design/duSsGnk84UMYav8mg8QNgR/%F0%9F%93%96-Shared-library?node-id=5236-100844&m=dev',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ShortcutComponent = () => {
|
|
17
|
+
return (<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
18
|
+
Default:
|
|
19
|
+
<div style={{ display: 'flex', gap: '4px' }}>
|
|
20
|
+
<Shortcut>F</Shortcut>
|
|
21
|
+
<Shortcut>Ctrl / ⌘</Shortcut>
|
|
22
|
+
<Shortcut>Shift</Shortcut>
|
|
23
|
+
</div>
|
|
24
|
+
Dark:
|
|
25
|
+
<div style={{ display: 'flex', gap: '4px' }}>
|
|
26
|
+
<Shortcut dark>F</Shortcut>
|
|
27
|
+
<Shortcut dark>Ctrl / ⌘</Shortcut>
|
|
28
|
+
<Shortcut dark>Shift</Shortcut>
|
|
29
|
+
</div>
|
|
30
|
+
</div>);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Playground = (args) => {
|
|
34
|
+
return (<Shortcut {...args}/>);
|
|
35
|
+
};
|
|
36
|
+
Playground.args = {
|
|
37
|
+
children: 'F',
|
|
38
|
+
dark: false,
|
|
39
|
+
as: 'div',
|
|
40
|
+
};
|
|
41
|
+
Playground.argTypes = {
|
|
42
|
+
dark: {
|
|
43
|
+
control: 'boolean',
|
|
44
|
+
defaultValue: false,
|
|
45
|
+
},
|
|
46
|
+
as: {
|
|
47
|
+
control: 'text',
|
|
48
|
+
defaultValue: 'div',
|
|
49
|
+
},
|
|
50
|
+
children: {
|
|
51
|
+
control: 'text',
|
|
52
|
+
defaultValue: 'F',
|
|
53
|
+
},
|
|
54
|
+
className: {
|
|
55
|
+
control: 'text',
|
|
56
|
+
},
|
|
57
|
+
};
|