Skip to content

Commit 9ba29dd

Browse files
authored
feat: disabled also can hover (#415)
* feat: disabled also can hover * test: add test case
1 parent b482c5c commit 9ba29dd

File tree

3 files changed

+77
-32
lines changed

3 files changed

+77
-32
lines changed

docs/examples/body-overflow.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,20 @@ export default () => {
8080
htmlRegion: 'scroll',
8181
}}
8282
>
83-
<span
83+
<button
84+
disabled
8485
style={{
85-
background: 'green',
86-
color: '#FFF',
86+
// background: 'green',
87+
// color: '#FFF',
8788
paddingBlock: 30,
8889
paddingInline: 70,
8990
opacity: 0.9,
9091
transform: 'scale(0.6)',
9192
display: 'inline-block',
9293
}}
9394
>
94-
Target
95-
</span>
95+
Button Target
96+
</button>
9697
</Trigger>
9798

9899
<Trigger

src/index.tsx

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import useId from 'rc-util/lib/hooks/useId';
99
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
1010
import isMobile from 'rc-util/lib/isMobile';
1111
import * as React from 'react';
12+
import { flushSync } from 'react-dom';
1213
import type { TriggerContextProps } from './context';
1314
import TriggerContext from './context';
1415
import useAction from './hooks/useAction';
@@ -307,10 +308,14 @@ export function generateTrigger(
307308
openRef.current = mergedOpen;
308309

309310
const internalTriggerOpen = useEvent((nextOpen: boolean) => {
310-
if (mergedOpen !== nextOpen) {
311-
setMergedOpen(nextOpen);
312-
onPopupVisibleChange?.(nextOpen);
313-
}
311+
// Enter or Pointer will both trigger open state change
312+
// We only need take one to avoid duplicated change event trigger
313+
flushSync(() => {
314+
if (mergedOpen !== nextOpen) {
315+
setMergedOpen(nextOpen);
316+
onPopupVisibleChange?.(nextOpen);
317+
}
318+
});
314319
});
315320

316321
// Trigger for delay
@@ -354,7 +359,9 @@ export function generateTrigger(
354359
0, 0,
355360
]);
356361

357-
const setMousePosByEvent = (event: React.MouseEvent) => {
362+
const setMousePosByEvent = (
363+
event: Pick<React.MouseEvent, 'clientX' | 'clientY'>,
364+
) => {
358365
setMousePos([event.clientX, event.clientY]);
359366
};
360367

@@ -463,21 +470,23 @@ export function generateTrigger(
463470
hideAction,
464471
);
465472

466-
// Util wrapper for trigger action
467-
const wrapperAction = (
473+
/**
474+
* Util wrapper for trigger action
475+
*/
476+
function wrapperAction<Event extends React.SyntheticEvent>(
468477
eventName: string,
469478
nextOpen: boolean,
470479
delay?: number,
471-
preEvent?: (event: any) => void,
472-
) => {
480+
preEvent?: (event: Event) => void,
481+
) {
473482
cloneProps[eventName] = (event: any, ...args: any[]) => {
474483
preEvent?.(event);
475484
triggerOpen(nextOpen, delay);
476485

477486
// Pass to origin
478487
originChildProps[eventName]?.(event, ...args);
479488
};
480-
};
489+
}
481490

482491
// ======================= Action: Click ========================
483492
const clickToShow = showActions.has('click');
@@ -521,9 +530,23 @@ export function generateTrigger(
521530
let onPopupMouseLeave: VoidFunction;
522531

523532
if (hoverToShow) {
524-
wrapperAction('onMouseEnter', true, mouseEnterDelay, (event) => {
525-
setMousePosByEvent(event);
526-
});
533+
// Compatible with old browser which not support pointer event
534+
wrapperAction<React.MouseEvent>(
535+
'onMouseEnter',
536+
true,
537+
mouseEnterDelay,
538+
(event) => {
539+
setMousePosByEvent(event);
540+
},
541+
);
542+
wrapperAction<React.PointerEvent>(
543+
'onPointerEnter',
544+
true,
545+
mouseEnterDelay,
546+
(event) => {
547+
setMousePosByEvent(event);
548+
},
549+
);
527550
onPopupMouseEnter = () => {
528551
// Only trigger re-open when popup is visible
529552
if (mergedOpen || inMotion) {
@@ -542,6 +565,7 @@ export function generateTrigger(
542565

543566
if (hoverToHide) {
544567
wrapperAction('onMouseLeave', false, mouseLeaveDelay);
568+
wrapperAction('onPointerLeave', false, mouseLeaveDelay);
545569
onPopupMouseLeave = () => {
546570
triggerOpen(false, mouseLeaveDelay);
547571
};

tests/basic.test.jsx

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -131,22 +131,42 @@ describe('Trigger.Basic', () => {
131131
expect(isPopupHidden()).toBeTruthy();
132132
});
133133

134-
it('hover works', () => {
135-
const { container } = render(
136-
<Trigger
137-
action={['hover']}
138-
popupAlign={placementAlignMap.left}
139-
popup={<strong>trigger</strong>}
140-
>
141-
<div className="target">click</div>
142-
</Trigger>,
143-
);
134+
describe('hover works', () => {
135+
it('mouse event', () => {
136+
const { container } = render(
137+
<Trigger
138+
action={['hover']}
139+
popupAlign={placementAlignMap.left}
140+
popup={<strong>trigger</strong>}
141+
>
142+
<div className="target">click</div>
143+
</Trigger>,
144+
);
144145

145-
trigger(container, '.target', 'mouseEnter');
146-
expect(isPopupHidden()).toBeFalsy();
146+
trigger(container, '.target', 'mouseEnter');
147+
expect(isPopupHidden()).toBeFalsy();
147148

148-
trigger(container, '.target', 'mouseLeave');
149-
expect(isPopupHidden()).toBeTruthy();
149+
trigger(container, '.target', 'mouseLeave');
150+
expect(isPopupHidden()).toBeTruthy();
151+
});
152+
153+
it('pointer event', () => {
154+
const { container } = render(
155+
<Trigger
156+
action={['hover']}
157+
popupAlign={placementAlignMap.left}
158+
popup={<strong>trigger</strong>}
159+
>
160+
<div className="target">click</div>
161+
</Trigger>,
162+
);
163+
164+
trigger(container, '.target', 'pointerEnter');
165+
expect(isPopupHidden()).toBeFalsy();
166+
167+
trigger(container, '.target', 'pointerLeave');
168+
expect(isPopupHidden()).toBeTruthy();
169+
});
150170
});
151171

152172
it('contextMenu works', () => {

0 commit comments

Comments
 (0)