Skip to content

Commit 4a69bd2

Browse files
committed
chore: update deps
1 parent 67829e5 commit 4a69bd2

2 files changed

Lines changed: 116 additions & 15 deletions

File tree

docs/examples/safe-hover.tsx

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
1-
import Trigger from '@rc-component/trigger';
1+
import Trigger, { type TriggerRef } from '@rc-component/trigger';
22
import React from 'react';
3+
import {
4+
getSafeHoverAreaPolygons,
5+
type SafeHoverPoint,
6+
} from '../../src/util/safeHover';
37
import '../../assets/index.less';
48

9+
type SafeHoverPolygon = {
10+
points: SafeHoverPoint[];
11+
fill: string;
12+
stroke: string;
13+
};
14+
15+
const safeHoverPolygonStyles = [
16+
{
17+
fill: 'rgba(255, 176, 32, 0.22)',
18+
stroke: 'rgba(222, 121, 0, 0.6)',
19+
},
20+
{
21+
fill: 'rgba(22, 119, 255, 0.16)',
22+
stroke: 'rgba(22, 119, 255, 0.55)',
23+
},
24+
];
25+
526
const builtinPlacements = {
627
top: {
728
points: ['bc', 'tc'],
@@ -18,16 +39,77 @@ const popupStyle: React.CSSProperties = {
1839
};
1940

2041
const SafeHoverDemo = () => {
42+
const triggerRef = React.useRef<TriggerRef>(null);
43+
44+
const [safeHoverPolygons, setSafeHoverPolygons] = React.useState<
45+
SafeHoverPolygon[]
46+
>([]);
47+
48+
const updateSafeHoverPolygons = (
49+
event: React.MouseEvent<HTMLElement> | React.PointerEvent<HTMLElement>,
50+
) => {
51+
const target = triggerRef.current?.nativeElement;
52+
const popup = triggerRef.current?.popupElement;
53+
54+
if (!target || !popup) {
55+
setSafeHoverPolygons([]);
56+
return;
57+
}
58+
59+
const leavePoint: SafeHoverPoint = [event.clientX, event.clientY];
60+
setSafeHoverPolygons(
61+
getSafeHoverAreaPolygons(
62+
leavePoint,
63+
target.getBoundingClientRect(),
64+
popup.getBoundingClientRect(),
65+
).map((points, index) => ({
66+
points,
67+
...safeHoverPolygonStyles[index],
68+
})),
69+
);
70+
};
71+
2172
return (
2273
<div style={{ minHeight: 320, padding: '160px 80px 80px' }}>
74+
{safeHoverPolygons.length > 0 && (
75+
<svg
76+
aria-hidden
77+
style={{
78+
position: 'fixed',
79+
inset: 0,
80+
width: '100vw',
81+
height: '100vh',
82+
pointerEvents: 'none',
83+
zIndex: 999,
84+
}}
85+
>
86+
{safeHoverPolygons.map(({ points, fill, stroke }, index) => (
87+
<polygon
88+
// eslint-disable-next-line react/no-array-index-key
89+
key={index}
90+
points={points.map((point) => point.join(',')).join(' ')}
91+
fill={fill}
92+
stroke={stroke}
93+
strokeDasharray="4 3"
94+
strokeWidth={1}
95+
/>
96+
))}
97+
</svg>
98+
)}
2399
<Trigger
100+
ref={triggerRef}
24101
action={['hover']}
25102
mouseLeaveDelay={0.12}
26103
popupPlacement="top"
27104
builtinPlacements={builtinPlacements}
28105
popupStyle={popupStyle}
106+
onOpenChange={(nextOpen) => {
107+
if (!nextOpen) {
108+
setSafeHoverPolygons([]);
109+
}
110+
}}
29111
popup={
30-
<div>
112+
<div onMouseEnter={() => setSafeHoverPolygons([])}>
31113
<strong>Safe hover popup</strong>
32114
<div style={{ marginTop: 8 }}>
33115
Move through the gap to reach me.
@@ -38,7 +120,12 @@ const SafeHoverDemo = () => {
38120
</div>
39121
}
40122
>
41-
<button style={{ padding: '8px 16px' }} type="button">
123+
<button
124+
style={{ padding: '8px 16px' }}
125+
type="button"
126+
onMouseLeave={updateSafeHoverPolygons}
127+
onPointerLeave={updateSafeHoverPolygons}
128+
>
42129
Hover target
43130
</button>
44131
</Trigger>

src/util/safeHover.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,7 @@ function getSafeHoverIntentPolygon(
161161
}
162162
}
163163

164-
export function isPointInSafeHoverArea(
165-
point: SafeHoverPoint,
164+
export function getSafeHoverAreaPolygons(
166165
leavePoint: SafeHoverPoint,
167166
targetRect: SafeHoverRect,
168167
popupRect: SafeHoverRect,
@@ -171,6 +170,30 @@ export function isPointInSafeHoverArea(
171170
const side = getSafeHoverSide(targetRect, popupRect);
172171

173172
if (!side || !isLeavePointTowardsPopup(side, leavePoint, targetRect)) {
173+
return [];
174+
}
175+
176+
return [
177+
getSafeHoverGapPolygon(side, targetRect, popupRect, buffer),
178+
getSafeHoverIntentPolygon(side, leavePoint, popupRect, buffer),
179+
];
180+
}
181+
182+
export function isPointInSafeHoverArea(
183+
point: SafeHoverPoint,
184+
leavePoint: SafeHoverPoint,
185+
targetRect: SafeHoverRect,
186+
popupRect: SafeHoverRect,
187+
buffer = 0.5,
188+
) {
189+
const safeHoverPolygons = getSafeHoverAreaPolygons(
190+
leavePoint,
191+
targetRect,
192+
popupRect,
193+
buffer,
194+
);
195+
196+
if (!safeHoverPolygons.length) {
174197
return false;
175198
}
176199

@@ -180,14 +203,5 @@ export function isPointInSafeHoverArea(
180203

181204
// The gap polygon keeps the straight corridor open; the intent polygon
182205
// catches diagonal movement toward the popup edge.
183-
return (
184-
isPointInPolygon(
185-
point,
186-
getSafeHoverGapPolygon(side, targetRect, popupRect, buffer),
187-
) ||
188-
isPointInPolygon(
189-
point,
190-
getSafeHoverIntentPolygon(side, leavePoint, popupRect, buffer),
191-
)
192-
);
206+
return safeHoverPolygons.some((polygon) => isPointInPolygon(point, polygon));
193207
}

0 commit comments

Comments
 (0)