@apify/ui-library 1.92.1-featimprovetooltip-7e1224.32 → 1.92.1-featimprovetooltip-7e1224.84

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apify/ui-library",
3
- "version": "1.92.1-featimprovetooltip-7e1224.32+2b6834918f7",
3
+ "version": "1.92.1-featimprovetooltip-7e1224.84+e6dc055477c",
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": "2b6834918f728c0d3d078ce95e5e3cbb007f9a7d"
69
+ "gitHead": "e6dc055477cf2b4762ebfd2e7bb377abdc437c1d"
70
70
  }
@@ -1,5 +1,4 @@
1
1
  import {
2
- arrow,
3
2
  autoPlacement,
4
3
  autoUpdate,
5
4
  flip,
@@ -9,15 +8,15 @@ import {
9
8
  shift,
10
9
  type Strategy,
11
10
  useFloating,
11
+ useTransitionStyles,
12
12
  } from '@floating-ui/react';
13
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';
14
+ import { type ComponentType, type CSSProperties, forwardRef, type MouseEvent, type ReactNode, type RefObject } from 'react';
15
+ import styled from 'styled-components';
16
16
 
17
17
  import { theme } from '../../design_system/theme.js';
18
18
 
19
19
  export const classNames = {
20
- ARROW: 'FloatingComponent-arrow',
21
20
  CHILDREN: 'FloatingComponent-children',
22
21
  };
23
22
 
@@ -38,18 +37,7 @@ export const FLOATING_PLACEMENT = {
38
37
 
39
38
  export type FloatingPlacement = typeof FLOATING_PLACEMENT[keyof typeof FLOATING_PLACEMENT];
40
39
 
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 {
40
+ interface FloatingComponentWrapProps {
53
41
  showInPortal?: boolean;
54
42
  className?: string;
55
43
  style?: CSSProperties;
@@ -72,28 +60,7 @@ export interface FloatingComponentBaseProps {
72
60
  showInPortal?: boolean;
73
61
  }
74
62
 
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};
63
+ const FloatingComponentWrapStyled = styled.span`
97
64
  padding: ${theme.space.space16};
98
65
  ${theme.typography.shared.mobile.bodyM};
99
66
  border-radius: 0.8rem;
@@ -101,17 +68,6 @@ const FloatingComponentWrapStyled = styled.span<FloatingComponentWrapStyledProps
101
68
  white-space: normal;
102
69
  word-break: break-word;
103
70
  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
71
 
116
72
  @media ${theme.device.tablet} {
117
73
  ${theme.typography.shared.tablet.bodyM};
@@ -123,9 +79,7 @@ const FloatingComponentWrapStyled = styled.span<FloatingComponentWrapStyledProps
123
79
  `;
124
80
 
125
81
  const ChildrenWrap = styled.div`
126
- .${classNames.CHILDREN} {
127
- width: fit-content;
128
- }
82
+ width: fit-content;
129
83
  `;
130
84
 
131
85
  const StyledPopoverBox = styled.div`
@@ -166,19 +120,17 @@ export const FloatingComponentBase = ({
166
120
  CloseButtonComponent,
167
121
  showInPortal = false,
168
122
  }: FloatingComponentBaseProps) => {
169
- const arrowRef = useRef<HTMLDivElement>(null);
170
-
171
123
  const {
172
124
  x,
173
125
  y,
174
126
  refs: { setReference, setFloating },
175
- placement: effectivePlacement,
176
127
  strategy: effectiveStrategy,
177
128
  middlewareData: {
178
- arrow: { x: arrowX, y: arrowY } = {},
179
129
  hide: refHidden,
180
130
  },
131
+ context,
181
132
  } = useFloating({
133
+ open: isOpen,
182
134
  placement,
183
135
  strategy,
184
136
  whileElementsMounted: autoUpdate,
@@ -186,40 +138,28 @@ export const FloatingComponentBase = ({
186
138
  offset(offsetPx),
187
139
  autoPlacements?.length ? autoPlacement({ allowedPlacements: autoPlacements }) : flip(),
188
140
  shift({ padding: 5 }),
189
- arrow({ element: arrowRef, padding: 9 }),
190
141
  hide({
191
142
  strategy: 'referenceHidden',
192
143
  }),
193
144
  ],
194
145
  });
195
146
 
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;
147
+ const { isMounted, styles } = useTransitionStyles(context, {
148
+ initial: ({ side }) => {
149
+ switch (side) {
150
+ case 'top':
151
+ return { opacity: 0, scale: '0.9', transform: 'translateY(10px)' };
152
+ case 'bottom':
153
+ return { opacity: 0, scale: '0.9', transform: 'translateY(-10px)' };
154
+ case 'left':
155
+ return { opacity: 0, scale: '0.9', transform: 'translateX(10px)' };
156
+ case 'right':
157
+ return { opacity: 0, scale: '0.9', transform: 'translateX(-10px)' };
158
+ default:
159
+ return {};
160
+ }
161
+ },
162
+ });
223
163
 
224
164
  if (!content) return <span>{children}</span>;
225
165
 
@@ -229,8 +169,8 @@ export const FloatingComponentBase = ({
229
169
  <ChildrenWrap className={clsx(classNames.CHILDREN, contentWrapClassName)} ref={setReference}>
230
170
  {children}
231
171
  </ChildrenWrap>
232
- {isOpen && (
233
- <FloatingComponentWrap arrowRotationDegs={arrowRotationDegs}
172
+ { isMounted && (
173
+ <FloatingComponentWrap
234
174
  className={className}
235
175
  ref={setFloating}
236
176
  style={{
@@ -239,23 +179,19 @@ export const FloatingComponentBase = ({
239
179
  left: x ?? 0,
240
180
  width: 'max-content',
241
181
  visibility: refHidden?.referenceHidden ? 'hidden' : 'visible',
182
+ ...styles,
242
183
  }}
243
184
  onClick={(e) => e.stopPropagation()}
244
185
  showInPortal={showInPortal}
245
186
  >
246
- <FadeIn delay={60} ref={triggerRef}>
187
+ <div ref={triggerRef}>
247
188
  {CloseButtonComponent
248
189
  ? <StyledPopoverBox>
249
190
  {content}
250
191
  <CloseButtonComponent />
251
192
  </StyledPopoverBox>
252
193
  : content}
253
- </FadeIn>
254
- <div
255
- ref={arrowRef}
256
- className={classNames.ARROW}
257
- style={arrowStyle}
258
- />
194
+ </div>
259
195
  </FloatingComponentWrap>
260
196
  )}
261
197
  </>
@@ -19,7 +19,7 @@ export default {
19
19
  },
20
20
  decorators: [
21
21
  (Story) => (
22
- <div style={{ margin: '3em', width: 'fit-content' }}>
22
+ <div style={{ margin: '5em', width: 'fit-content' }}>
23
23
  <Story />
24
24
  </div>
25
25
  ),
@@ -27,6 +27,7 @@ export default {
27
27
  args: {
28
28
  children: <Child />,
29
29
  content: 'This is a tooltip',
30
+ isDarkTheme: false,
30
31
  },
31
32
  };
32
33
 
@@ -40,31 +41,31 @@ const DefaultStoryWrapper = styled.div`
40
41
  export const Default = (args) => {
41
42
  return (<DefaultStoryWrapper>
42
43
  <div>
43
- Default:
44
44
  <Tooltip {...args}/>
45
+ Default
45
46
  </div>
46
47
  <div>
47
- With shortcuts:
48
48
  <Tooltip shortcuts={['Ctrl / ⌘', 'F']} {...args}/>
49
+ With shortcuts
49
50
  </div>
50
51
  <div>
51
- With shortcuts and long text:
52
- <Tooltip
53
- persistent={{ isOpenOverride: true }}
54
- shortcuts={['Ctrl / ⌘', 'F']}
55
- content={longText}
56
- ><Child /></Tooltip>
52
+ <Tooltip subtleText="This is some subtle text" {...args} />
53
+ With subtleText
57
54
  </div>
58
55
  <div>
59
- With image:
60
56
  <Tooltip imageUrl='https://picsum.photos/id/1/1024' {...args} />
57
+ With image
61
58
  </div>
62
59
  <div>
63
- With subtleText:
64
- <Tooltip subtleText="This is some subtle text" {...args} />
60
+ <Tooltip
61
+ persistent={{ isOpenOverride: true }}
62
+ shortcuts={['Ctrl / ⌘', 'F']}
63
+ content={longText}
64
+ textAlign='center'
65
+ ><Child /></Tooltip>
66
+ With shortcuts and long centered text
65
67
  </div>
66
68
  <div>
67
- With everything:
68
69
  <Tooltip
69
70
  persistent={{ isOpenOverride: true }}
70
71
  shortcuts={['Ctrl / ⌘', 'F']}
@@ -73,6 +74,7 @@ export const Default = (args) => {
73
74
  placement={FLOATING_PLACEMENT.BOTTOM}
74
75
  content={longText}
75
76
  ><Child /></Tooltip>
77
+ With everything
76
78
  </div>
77
79
  </DefaultStoryWrapper>
78
80
  );
@@ -4,12 +4,20 @@ import {
4
4
  useInteractions,
5
5
  } from '@floating-ui/react';
6
6
  import { type ComponentType, forwardRef, useState } from 'react';
7
- import styled from 'styled-components';
7
+ import styled, { css } from 'styled-components';
8
8
 
9
9
  import { theme } from '../../design_system/theme.js';
10
+ import { useSharedUiDependencies } from '../../ui_dependency_provider.js';
10
11
  import { FloatingComponentBase, type FloatingComponentBaseProps } from './floating_component_base.js';
11
12
  import { TooltipContent } from './tooltip_content.js';
12
13
 
14
+ export const TOOLTIP_TEXT_ALIGNS = {
15
+ LEFT: 'left',
16
+ CENTER: 'center',
17
+ } as const;
18
+
19
+ export type TooltipTextAlign = typeof TOOLTIP_TEXT_ALIGNS[keyof typeof TOOLTIP_TEXT_ALIGNS];
20
+
13
21
  export const TOOLTIP_SIZES = {
14
22
  XSMALL: 'xsmall',
15
23
  SMALL: 'small',
@@ -35,6 +43,7 @@ export interface TooltipProps extends Omit<FloatingComponentBaseProps, 'isOpen'
35
43
  imageUrl?: string;
36
44
  subtleText?: string;
37
45
  size?: TooltipSize;
46
+ textAlign?: TooltipTextAlign;
38
47
  }
39
48
 
40
49
  interface WithTooltipProps {
@@ -44,11 +53,15 @@ interface WithTooltipProps {
44
53
  // Using a styled component to get access to the `as` prop
45
54
  const TooltipFocusArea = styled.span``;
46
55
 
47
- const StyledFloatingComponentBase = styled(FloatingComponentBase)`
56
+ const StyledFloatingComponentBase = styled(FloatingComponentBase)<{ $isDarkTheme?: boolean }>`
48
57
  color: ${theme.colorPalette.dark.neutral0};
49
58
  background-color: ${theme.colorPalette.dark.neutral900};
50
- border: 1px solid ${theme.color.neutral.smallTooltipBorder};
51
59
  padding: ${theme.space.space8};
60
+
61
+ ${({ $isDarkTheme }) => $isDarkTheme && css`
62
+ box-shadow: ${theme.shadow.shadow2};
63
+ border: 1px solid ${theme.color.neutral.smallTooltipBorder};
64
+ `}
52
65
  `;
53
66
 
54
67
  /**
@@ -64,8 +77,10 @@ export const Tooltip = ({
64
77
  imageUrl,
65
78
  subtleText,
66
79
  size = TOOLTIP_SIZES.SMALL,
80
+ textAlign = TOOLTIP_TEXT_ALIGNS.LEFT,
67
81
  ...rest
68
82
  }: TooltipProps) => {
83
+ const { uiTheme } = useSharedUiDependencies();
69
84
  const { isOpenOverride, CloseButtonComponent } = persistent || {};
70
85
  const [open, setOpen] = useState(false);
71
86
 
@@ -86,7 +101,14 @@ export const Tooltip = ({
86
101
  const tooltipProps = {
87
102
  ...rest,
88
103
  isOpen: isOpenOverride !== undefined ? isOpenOverride : open,
89
- content: <TooltipContent content={rest.content} shortcuts={shortcuts} imageUrl={imageUrl} subtleText={subtleText} size={size} />,
104
+ content: <TooltipContent
105
+ content={rest.content}
106
+ shortcuts={shortcuts}
107
+ imageUrl={imageUrl}
108
+ subtleText={subtleText}
109
+ size={size}
110
+ textAlign={textAlign}
111
+ />,
90
112
  };
91
113
 
92
114
  return (
@@ -100,17 +122,21 @@ export const Tooltip = ({
100
122
  ref={refs.setFloating}
101
123
  {...getFloatingProps()}
102
124
  >
103
- <StyledFloatingComponentBase {...tooltipProps} CloseButtonComponent={CloseButtonComponent} />
125
+ <StyledFloatingComponentBase
126
+ {...tooltipProps}
127
+ CloseButtonComponent={CloseButtonComponent}
128
+ $isDarkTheme={uiTheme === 'DARK'}
129
+ />
104
130
  </div>
105
131
  </TooltipFocusArea>
106
132
  );
107
133
  };
108
134
 
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} />;
135
+ export function withTooltip<TProps extends Record<string, unknown> = Record<string, unknown>>(Component: ComponentType<TProps>) {
136
+ const Enhanced = forwardRef<HTMLElement, TProps & WithTooltipProps>(({ tooltipProps, ...rest }, ref) => {
137
+ if (!tooltipProps) return <Component {...(rest as unknown as TProps)} ref={ref} />;
112
138
  return <Tooltip {...tooltipProps}>
113
- <Component {...(rest as unknown as T)} ref={ref} />
139
+ <Component {...(rest as unknown as TProps)} ref={ref} />
114
140
  </Tooltip>;
115
141
  });
116
142
 
@@ -2,9 +2,10 @@ import styled, { css } from 'styled-components';
2
2
 
3
3
  import { theme } from '../../design_system/theme.js';
4
4
  import { Shortcut } from '../shortcut.js';
5
- import { TOOLTIP_SIZES, type TooltipProps, type TooltipSize } from './tooltip.js';
5
+ import type { TooltipProps, TooltipSize, TooltipTextAlign } from './tooltip.js';
6
+ import { TOOLTIP_SIZES, TOOLTIP_TEXT_ALIGNS } from './tooltip.js';
6
7
 
7
- type ContentProps = Pick<TooltipProps, 'content' | 'shortcuts' | 'imageUrl' | 'subtleText' | 'size'>;
8
+ type ContentProps = Pick<TooltipProps, 'content' | 'shortcuts' | 'imageUrl' | 'subtleText' | 'size' | 'textAlign'>;
8
9
 
9
10
  const TOOLTIP_SIZES_VALUES: Record<TooltipSize, string> = {
10
11
  xsmall: '24rem',
@@ -14,61 +15,83 @@ const TOOLTIP_SIZES_VALUES: Record<TooltipSize, string> = {
14
15
  xlarge: '64rem',
15
16
  };
16
17
 
17
- const StyledContent = styled.div<{ $size: TooltipSize }>`
18
+ const TOOLTIP_CLASSNAMES = {
19
+ IMAGE: 'Tooltip-image',
20
+ TEXT_CONTENT: 'Tooltip-textContent',
21
+ SUBTLE_TEXT: 'Tooltip-subtleText',
22
+ SHORTCUT_CONTAINER: 'Tooltip-shortcutContainer',
23
+ } as const;
24
+
25
+ const StyledContent = styled.div<{ $size: TooltipSize, $textAlign: TooltipTextAlign }>`
18
26
  display: flex;
19
27
  flex-direction: column;
20
28
  gap: ${theme.space.space8};
21
29
  /* Size - (tooltip padding + border) * 2 */
22
30
  max-width: ${({ $size }) => css`calc(${TOOLTIP_SIZES_VALUES[$size]} - (${theme.space.space8} + 1px) * 2)`};
31
+ text-align: ${({ $textAlign }) => $textAlign || 'left'};
32
+ align-items: ${({ $textAlign }) => ($textAlign === 'center' ? 'center' : 'flex-start')};
23
33
 
24
- .Tooltip-image {
34
+ .${TOOLTIP_CLASSNAMES.IMAGE} {
25
35
  max-width: 100%;
26
36
  max-height: 150px;
27
37
  border-radius: 4px;
28
38
  object-fit: contain;
29
39
  }
30
40
 
31
- .Tooltip-textContent {
41
+ .${TOOLTIP_CLASSNAMES.TEXT_CONTENT} {
32
42
  display: flex;
33
43
  flex-direction: column;
34
44
  gap: ${theme.space.space8};
45
+ align-items: ${({ $textAlign }) => ($textAlign === 'center' ? 'center' : 'flex-start')};
35
46
 
36
47
  /* When there is no child with subtleText class */
37
- &:not(:has(.Tooltip-subtleText)){
48
+ &:not(:has(.${TOOLTIP_CLASSNAMES.SUBTLE_TEXT})){
38
49
  flex-direction: row;
39
50
  flex-wrap: wrap;
40
51
  }
41
52
 
42
- .Tooltip-subtleText {
53
+ /* When there is a child with subtleText class */
54
+ &:has(.${TOOLTIP_CLASSNAMES.SUBTLE_TEXT}){
55
+ text-align: left;
56
+ }
57
+
58
+ .${TOOLTIP_CLASSNAMES.SUBTLE_TEXT} {
43
59
  color: ${theme.colorPalette.dark.neutral500};
44
60
  }
45
61
 
46
- .Tooltip-shortcutContainer{
62
+ .${TOOLTIP_CLASSNAMES.SHORTCUT_CONTAINER} {
47
63
  display: flex;
48
64
  gap: ${theme.space.space4};
49
65
  }
50
66
  }
51
67
  `;
52
68
 
53
- export const TooltipContent = ({ content, shortcuts = [], imageUrl, subtleText, size = TOOLTIP_SIZES.SMALL }: ContentProps) => {
69
+ export const TooltipContent = ({
70
+ content,
71
+ shortcuts = [],
72
+ imageUrl,
73
+ subtleText,
74
+ size = TOOLTIP_SIZES.SMALL,
75
+ textAlign = TOOLTIP_TEXT_ALIGNS.LEFT,
76
+ }: ContentProps) => {
54
77
  return (
55
- <StyledContent $size={size}>
78
+ <StyledContent $size={size} $textAlign={textAlign}>
56
79
  {imageUrl && (
57
80
  <img
58
81
  src={imageUrl}
59
82
  alt=""
60
- className="Tooltip-image"
83
+ className={TOOLTIP_CLASSNAMES.IMAGE}
61
84
  />
62
85
  )}
63
- <div className="Tooltip-textContent">
86
+ <div className={TOOLTIP_CLASSNAMES.TEXT_CONTENT}>
64
87
  {content}
65
88
  {subtleText && (
66
- <div className="Tooltip-subtleText">
89
+ <div className={TOOLTIP_CLASSNAMES.SUBTLE_TEXT}>
67
90
  {subtleText}
68
91
  </div>
69
92
  )}
70
93
  {shortcuts.length > 0 && (
71
- <div className='Tooltip-shortcutContainer'>
94
+ <div className={TOOLTIP_CLASSNAMES.SHORTCUT_CONTAINER}>
72
95
  {shortcuts.map((shortcut, index) => (
73
96
  <Shortcut key={`${shortcut}-${index}`} dark>{shortcut}</Shortcut>
74
97
  ))}
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
 
3
+ import { theme } from '../design_system/theme.ts';
3
4
  import { Shortcut } from './shortcut.tsx';
4
5
 
5
6
  export default {
@@ -16,13 +17,13 @@ export default {
16
17
  export const ShortcutComponent = () => {
17
18
  return (<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
18
19
  Default:
19
- <div style={{ display: 'flex', gap: '4px' }}>
20
+ <div style={{ display: 'flex', gap: '4px', padding: '16px' }}>
20
21
  <Shortcut>F</Shortcut>
21
22
  <Shortcut>Ctrl / ⌘</Shortcut>
22
23
  <Shortcut>Shift</Shortcut>
23
24
  </div>
24
25
  Dark:
25
- <div style={{ display: 'flex', gap: '4px' }}>
26
+ <div style={{ display: 'flex', gap: '4px', backgroundColor: theme.colorPalette.dark.neutral700, padding: '16px' }}>
26
27
  <Shortcut dark>F</Shortcut>
27
28
  <Shortcut dark>Ctrl / ⌘</Shortcut>
28
29
  <Shortcut dark>Shift</Shortcut>