Skip to content

Commit 76e2ca6

Browse files
committed
feat: add open/close visibility delays to Tooltip
1 parent 4d20882 commit 76e2ca6

File tree

19 files changed

+394
-44
lines changed

19 files changed

+394
-44
lines changed

apps/docs/docs/components/overlay/Tooltip/_mobileExamples.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,25 @@ function TooltipColorSchemeOptOut() {
4040
);
4141
}
4242
```
43+
44+
### Visibility delay (press)
45+
46+
Use `openDelay` and `closeDelay` to slow down activation/dismissal when users tap through dense surfaces.
47+
48+
```jsx
49+
function TooltipVisibilityDelay() {
50+
return (
51+
<HStack spacingHorizontal={2} gap={2} justifyContent="space-around">
52+
<Tooltip content="Opens after 400ms" openDelay={400}>
53+
<Button>Open delay 400ms</Button>
54+
</Tooltip>
55+
<Tooltip content="Closes after 150ms" closeDelay={150}>
56+
<Button>Close delay 150ms</Button>
57+
</Tooltip>
58+
<Tooltip content="Open 400 / Close 150" openDelay={400} closeDelay={150}>
59+
<Button>Open 400 / Close 150</Button>
60+
</Tooltip>
61+
</HStack>
62+
);
63+
}
64+
```

apps/docs/docs/components/overlay/Tooltip/_webExamples.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,25 @@ You can use tooltips within `TextInput` to provide more context.
9090
placeholder="Satoshi Nakamoto"
9191
/>
9292
```
93+
94+
### Visibility delay (hover)
95+
96+
Use `openDelay` and `closeDelay` to slow down hover activation and reduce accidental opens on dense UI. Keyboard focus still opens immediately.
97+
98+
```jsx live
99+
function TooltipVisibilityDelay() {
100+
return (
101+
<HStack spacingHorizontal={2} gap={2} justifyContent="space-around">
102+
<Tooltip content="Opens after 400ms" openDelay={400}>
103+
<Button>Open delay 400ms</Button>
104+
</Tooltip>
105+
<Tooltip content="Closes after 150ms" closeDelay={150}>
106+
<Button>Close delay 150ms</Button>
107+
</Tooltip>
108+
<Tooltip content="Open 400 / Close 150" openDelay={400} closeDelay={150}>
109+
<Button>Open 400 / Close 150</Button>
110+
</Tooltip>
111+
</HStack>
112+
);
113+
}
114+
```

packages/common/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
88

99
<!-- template-start -->
1010

11+
## 8.29.0 ((12/11/2025, 02:43 PM PST))
12+
13+
This is an artificial version bump with no new change.
14+
1115
## 8.28.1 ((12/10/2025, 04:33 PM PST))
1216

1317
This is an artificial version bump with no new change.

packages/common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coinbase/cds-common",
3-
"version": "8.28.1",
3+
"version": "8.29.0",
44
"description": "Coinbase Design System - Common",
55
"repository": {
66
"type": "git",

packages/mcp-server/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
88

99
<!-- template-start -->
1010

11+
## 8.29.0 ((12/11/2025, 02:43 PM PST))
12+
13+
This is an artificial version bump with no new change.
14+
1115
## 8.28.1 ((12/10/2025, 04:33 PM PST))
1216

1317
This is an artificial version bump with no new change.

packages/mcp-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coinbase/cds-mcp-server",
3-
"version": "8.28.1",
3+
"version": "8.29.0",
44
"description": "Coinbase Design System - MCP Server",
55
"repository": {
66
"type": "git",

packages/mobile/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
88

99
<!-- template-start -->
1010

11+
## 8.29.0 (12/11/2025 PST)
12+
13+
#### 🚀 Updates
14+
15+
- Add open/close visibility delays to Tooltip. [[#234](https://github.com/coinbase/cds/pull/234)] [DX-5010]
16+
1117
## 8.28.1 (12/10/2025 PST)
1218

1319
#### 🐞 Fixes

packages/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coinbase/cds-mobile",
3-
"version": "8.28.1",
3+
"version": "8.29.0",
44
"description": "Coinbase Design System - Mobile",
55
"repository": {
66
"type": "git",

packages/mobile/src/overlays/__stories__/TooltipV2.stories.tsx

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ const shortText = 'This is the short text.';
2424
const longText =
2525
'This is the really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really long text.';
2626

27+
const DelayVariations = () => {
28+
return (
29+
<Example title="Delay Variations">
30+
<VStack background="bgAlternate" gap={4} paddingY={2}>
31+
<HStack justifyContent="space-evenly">
32+
<Tooltip closeDelay={0} content="Opens after 400ms" openDelay={400}>
33+
<Text font="label2">Open delay 400ms</Text>
34+
</Tooltip>
35+
<Tooltip closeDelay={200} content="Closes after 200ms" openDelay={0}>
36+
<Text font="label2">Close delay 200ms</Text>
37+
</Tooltip>
38+
</HStack>
39+
<HStack justifyContent="space-evenly">
40+
<Tooltip closeDelay={150} content="Open 300 / Close 150" openDelay={300}>
41+
<Text font="label2">Open 300 / Close 150</Text>
42+
</Tooltip>
43+
<Tooltip closeDelay={300} content="Open 500 / Close 300" openDelay={500}>
44+
<Text font="label2">Open 500 / Close 300</Text>
45+
</Tooltip>
46+
</HStack>
47+
</VStack>
48+
</Example>
49+
);
50+
};
51+
2752
const ToolTipWithA11y = ({ tooltipText, yShiftByStatusBarHeight }: Omit<ContentTypes, 'title'>) => {
2853
const triggerRef = useRef(null);
2954
const { setA11yFocus } = useA11y();
@@ -206,7 +231,7 @@ const RNModalTest = () => {
206231
);
207232

208233
return (
209-
<>
234+
<Example>
210235
<Button onPress={setVisibleToOn}>Open RN Modal Test 2</Button>
211236
<RNModal statusBarTranslucent={statusBarTranslucent} visible={visible}>
212237
<VStack paddingTop={6} width="100%">
@@ -232,7 +257,7 @@ const RNModalTest = () => {
232257
yShiftByStatusBarHeight={yShiftByStatusBarHeight}
233258
/>
234259
</RNModal>
235-
</>
260+
</Example>
236261
);
237262
};
238263

@@ -249,11 +274,14 @@ const DisabledTest = () => {
249274
const TooltipV2Screen = () => {
250275
return (
251276
<ExampleScreen>
252-
<CDSModalTest />
253-
<RNModalTest />
254-
<Content title="Short Text Tooltip" tooltipText={shortText} />
255-
<Content title="Long Text Tooltip" tooltipText={longText} />
256-
<DisabledTest />
277+
<VStack gap={2}>
278+
<CDSModalTest />
279+
<RNModalTest />
280+
<DelayVariations />
281+
<Content title="Short Text Tooltip" tooltipText={shortText} />
282+
<Content title="Long Text Tooltip" tooltipText={longText} />
283+
<DisabledTest />
284+
</VStack>
257285
</ExampleScreen>
258286
);
259287
};

packages/mobile/src/overlays/tooltip/Tooltip.tsx

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
1+
import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import { Modal as RNModal, TouchableOpacity, View } from 'react-native';
33

44
import { InvertedThemeProvider } from '../../system/ThemeProvider';
@@ -24,24 +24,55 @@ export const Tooltip = memo(
2424
visible,
2525
invertColorScheme = true,
2626
elevation,
27+
openDelay = 0,
28+
closeDelay = 0,
2729
}: TooltipProps) => {
2830
const subjectRef = useRef<View | null>(null);
2931
const [isOpen, setIsOpen] = useState(false);
3032
const isVisible = visible !== false && isOpen;
3133
const [subjectLayout, setSubjectLayout] = useState<SubjectLayout>();
34+
const openTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
35+
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
3236

3337
const WrapperComponent = invertColorScheme ? InvertedThemeProvider : Fragment;
3438

3539
const { opacity, translateY, animateIn, animateOut } = useTooltipAnimation(placement);
3640

41+
const clearOpenTimeout = useCallback(() => {
42+
if (openTimeoutRef.current) {
43+
clearTimeout(openTimeoutRef.current);
44+
openTimeoutRef.current = null;
45+
}
46+
}, []);
47+
48+
const clearCloseTimeout = useCallback(() => {
49+
if (closeTimeoutRef.current) {
50+
clearTimeout(closeTimeoutRef.current);
51+
closeTimeoutRef.current = null;
52+
}
53+
}, []);
54+
3755
const handleRequestClose = useCallback(() => {
38-
animateOut.start(() => {
39-
setIsOpen(false);
40-
onCloseTooltip?.();
41-
});
42-
}, [animateOut, onCloseTooltip]);
56+
clearOpenTimeout();
57+
clearCloseTimeout();
58+
59+
const closeTooltip = () => {
60+
animateOut.start(() => {
61+
setIsOpen(false);
62+
onCloseTooltip?.();
63+
});
64+
};
65+
66+
if (closeDelay > 0) {
67+
closeTimeoutRef.current = setTimeout(closeTooltip, closeDelay);
68+
return;
69+
}
70+
71+
closeTooltip();
72+
}, [animateOut, clearCloseTimeout, clearOpenTimeout, closeDelay, onCloseTooltip]);
4373

4474
const handlePressSubject = useCallback(() => {
75+
clearCloseTimeout();
4576
subjectRef.current?.measure((x, y, width, height, pageOffsetX, pageOffsetY) => {
4677
setSubjectLayout({
4778
width,
@@ -50,9 +81,19 @@ export const Tooltip = memo(
5081
pageOffsetY,
5182
});
5283
});
53-
setIsOpen(true);
54-
onOpenTooltip?.();
55-
}, [onOpenTooltip]);
84+
const openTooltip = () => {
85+
setIsOpen(true);
86+
onOpenTooltip?.();
87+
};
88+
89+
clearOpenTimeout();
90+
if (openDelay > 0) {
91+
openTimeoutRef.current = setTimeout(openTooltip, openDelay);
92+
return;
93+
}
94+
95+
openTooltip();
96+
}, [clearCloseTimeout, clearOpenTimeout, onOpenTooltip, openDelay]);
5697

5798
// The accessibility props for the trigger component. Trigger component
5899
// equals the component where when you click on it, it will show the tooltip
@@ -86,6 +127,13 @@ export const Tooltip = memo(
86127
[content, accessibilityLabelForContent, accessibilityHintForContent, handleRequestClose],
87128
);
88129

130+
useEffect(() => {
131+
return () => {
132+
clearOpenTimeout();
133+
clearCloseTimeout();
134+
};
135+
}, [clearCloseTimeout, clearOpenTimeout]);
136+
89137
return (
90138
<View ref={subjectRef} collapsable={false}>
91139
<TouchableOpacity

0 commit comments

Comments
 (0)