@apify/ui-library 1.92.1-featimprovetooltip-7e1224.41 → 1.92.1-featimprovetooltip-7e1224.86

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.41+cf501777327",
3
+ "version": "1.92.1-featimprovetooltip-7e1224.86+42ab7a23a31",
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": "cf501777327ae7affeb4a51e284e50558ad5b55e"
69
+ "gitHead": "42ab7a23a314c6d9052bbeee53f667f127fa017f"
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};
@@ -164,19 +120,17 @@ export const FloatingComponentBase = ({
164
120
  CloseButtonComponent,
165
121
  showInPortal = false,
166
122
  }: FloatingComponentBaseProps) => {
167
- const arrowRef = useRef<HTMLDivElement>(null);
168
-
169
123
  const {
170
124
  x,
171
125
  y,
172
126
  refs: { setReference, setFloating },
173
- placement: effectivePlacement,
174
127
  strategy: effectiveStrategy,
175
128
  middlewareData: {
176
- arrow: { x: arrowX, y: arrowY } = {},
177
129
  hide: refHidden,
178
130
  },
131
+ context,
179
132
  } = useFloating({
133
+ open: isOpen,
180
134
  placement,
181
135
  strategy,
182
136
  whileElementsMounted: autoUpdate,
@@ -184,40 +138,28 @@ export const FloatingComponentBase = ({
184
138
  offset(offsetPx),
185
139
  autoPlacements?.length ? autoPlacement({ allowedPlacements: autoPlacements }) : flip(),
186
140
  shift({ padding: 5 }),
187
- arrow({ element: arrowRef, padding: 9 }),
188
141
  hide({
189
142
  strategy: 'referenceHidden',
190
143
  }),
191
144
  ],
192
145
  });
193
146
 
194
- const arrowStyle = useMemo(() => {
195
- const staticSide = {
196
- top: 'bottom',
197
- right: 'left',
198
- bottom: 'top',
199
- left: 'right',
200
- }[effectivePlacement.split('-')[0] as 'top' | 'right' | 'bottom' | 'left'];
201
-
202
- let borderNone: CSSProperties = { borderLeft: 0, borderTop: 0 };
203
- if (staticSide === 'bottom') borderNone = { borderRight: 0, borderBottom: 0 };
204
- if (staticSide === 'right') borderNone = { borderLeft: 0, borderBottom: 0 };
205
- if (staticSide === 'left') borderNone = { borderRight: 0, borderTop: 0 };
206
-
207
- const style: CSSProperties = {
208
- left: arrowX != null ? `${arrowX}px` : '',
209
- top: arrowY != null ? `${arrowY}px` : '',
210
- ...borderNone,
211
- };
212
-
213
- if (staticSide) {
214
- (style as Record<string, string | number>)[staticSide] = '-6px';
215
- }
216
-
217
- return style;
218
- }, [arrowX, arrowY, effectivePlacement]);
219
-
220
- 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
+ });
221
163
 
222
164
  if (!content) return <span>{children}</span>;
223
165
 
@@ -227,8 +169,8 @@ export const FloatingComponentBase = ({
227
169
  <ChildrenWrap className={clsx(classNames.CHILDREN, contentWrapClassName)} ref={setReference}>
228
170
  {children}
229
171
  </ChildrenWrap>
230
- {isOpen && (
231
- <FloatingComponentWrap arrowRotationDegs={arrowRotationDegs}
172
+ { isMounted && (
173
+ <FloatingComponentWrap
232
174
  className={className}
233
175
  ref={setFloating}
234
176
  style={{
@@ -237,23 +179,19 @@ export const FloatingComponentBase = ({
237
179
  left: x ?? 0,
238
180
  width: 'max-content',
239
181
  visibility: refHidden?.referenceHidden ? 'hidden' : 'visible',
182
+ ...styles,
240
183
  }}
241
184
  onClick={(e) => e.stopPropagation()}
242
185
  showInPortal={showInPortal}
243
186
  >
244
- <FadeIn delay={60} ref={triggerRef}>
187
+ <div ref={triggerRef}>
245
188
  {CloseButtonComponent
246
189
  ? <StyledPopoverBox>
247
190
  {content}
248
191
  <CloseButtonComponent />
249
192
  </StyledPopoverBox>
250
193
  : content}
251
- </FadeIn>
252
- <div
253
- ref={arrowRef}
254
- className={classNames.ARROW}
255
- style={arrowStyle}
256
- />
194
+ </div>
257
195
  </FloatingComponentWrap>
258
196
  )}
259
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>
@@ -17,6 +17,7 @@ const adaptiveColors = css`
17
17
  color: ${theme.color.neutral.textSubtle};
18
18
  `;
19
19
 
20
+ /* Tooltips with shortcuts use dark mode styles */
20
21
  const darkColors = css`
21
22
  background-color: ${theme.colorPalette.dark.neutral800};
22
23
  border: 1px solid ${theme.colorPalette.dark.neutral700};