Skip to content

Commit

Permalink
fix(components/popover): 修复 destroyOnHide 为 true 时 hover 离开窗体有可能不会关闭的问题
Browse files Browse the repository at this point in the history
Closes #63
  • Loading branch information
mengxinssfd committed Dec 1, 2023
1 parent 81b8134 commit 109cafc
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 21 deletions.
11 changes: 10 additions & 1 deletion packages/components/src/popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ export const Popover: React.FC<PopoverProps> = React.forwardRef<
offset,
},
);
const show = useShowController(

const [show, enterBalloonSubject, leaveBalloonSubject] = useShowController(
childrenRef,
balloonRef,
refreshPosition,
Expand All @@ -105,6 +106,14 @@ export const Popover: React.FC<PopoverProps> = React.forwardRef<
<WordBalloon
attrs={{
...attrs,
onMouseEnter(e) {
attrs.onMouseEnter?.(e);
enterBalloonSubject.current.next();
},
onMouseLeave(e) {
attrs.onMouseLeave?.(e);
leaveBalloonSubject.current.next();
},
className: getClassNames(rootName, attrs.className),
}}
ref={balloonRef as React.Ref<HTMLDivElement>}
Expand Down
80 changes: 77 additions & 3 deletions packages/components/src/popover/__tests__/Popover.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { Button } from '~/button';
import { Popover } from '..';

describe('Popover', () => {
function getBalloon() {
return document.querySelector('.t-word-balloon') as HTMLElement;
}

jest.useFakeTimers();
test('attrs', () => {
const onClick = jest.fn();
Expand Down Expand Up @@ -398,9 +402,79 @@ describe('Popover', () => {
act(() => jest.advanceTimersByTime(0));
act(() => jest.advanceTimersByTime(300));
expect(getBalloon()).toMatchSnapshot();
});

describe('hovers', () => {
const getBtn = () => document.querySelector('button')!;
const Actions = {
enterBalloon: () => fireEvent.mouseEnter(getBalloon()),
leaveBalloon: () => fireEvent.mouseLeave(getBalloon()),
enterTrigger: () => fireEvent.mouseEnter(getBtn()),
leaveTrigger: () => fireEvent.mouseLeave(getBtn()),
};

it('destroyOnHide 快速 hover', () => {
jest.useFakeTimers();
const App = () => {
const visibleRef = useRef(false);
return (
<Popover
onVisibleChange={(visible) => (visibleRef.current = visible)}
destroyOnHide
content="1"
>
<button>hover</button>
</Popover>
);
};
render(<App />);

expect(getBalloon()).toBeNull();
Actions.enterTrigger();
expect(getBalloon()).not.toBeNull();

Actions.leaveTrigger();
expect(getBalloon()).not.toBeNull();
Actions.enterBalloon();
expect(getBalloon()).not.toBeNull();
Actions.leaveBalloon();
Actions.enterTrigger();
Actions.leaveTrigger();
Actions.enterBalloon();
Actions.leaveBalloon();

function getBalloon() {
return document.querySelector('.t-word-balloon') as HTMLElement;
}
act(() => jest.advanceTimersByTime(500));
expect(getBalloon()).toBeNull();
});
it('balloon 快速 hover', () => {
jest.useFakeTimers();
render(
<Popover content="1">
<button>hover</button>
</Popover>,
);

expect(getBalloon()).toBeNull();
Actions.enterTrigger();
expect(getBalloon()).not.toBeNull();

act(() => jest.advanceTimersByTime(500));

Actions.leaveTrigger();
expect(getBalloon()).not.toHaveClass('t-popover-leave-active');
Actions.enterBalloon();
Actions.leaveBalloon();
expect(getBalloon()).not.toHaveClass('t-popover-leave-active');
Actions.enterBalloon();
Actions.leaveBalloon();

// 第一次是退出延时
act(() => jest.advanceTimersByTime(200));
expect(getBalloon()).not.toHaveClass('t-transition--invisible');
// 第二次是动画延时
// 需要两次才会有 t-transition--invisible
act(() => jest.advanceTimersByTime(500));
expect(getBalloon()).toHaveClass('t-transition--invisible');
});
});
});
35 changes: 18 additions & 17 deletions packages/components/src/popover/hooks/useShowController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,43 @@ import {
switchMap,
takeUntil,
takeWhile,
defer,
Subject,
delay,
merge,
retry,
take,
tap,
of,
} from 'rxjs';
import { useBeforeDestroy, fromOuterEvent, useNextEffect } from '@pkg/shared';
import { PopoverRequiredPartProps } from '~/popover/Popover';
import { fromOuterEvent, useNextEffect } from '@pkg/shared';
import React, { useEffect, useState, useRef } from 'react';
import { castArray, emptyFn } from '@tool-pack/basic';
import { collectScroller } from '@tool-pack/dom';

function hoverTriggerHandler(
triggerEl: HTMLElement,
balloonElRef: React.MutableRefObject<HTMLElement | undefined>,
open: () => void,
close: () => void,
enterDelay: number,
leaveDelay: number,
show: boolean,
enterBalloonSubject: React.MutableRefObject<Subject<void>>,
leaveBalloonSubject: React.MutableRefObject<Subject<void>>,
) {
const triggerEnterEvent = fromEvent(triggerEl, 'mouseenter');
const triggerMoveEvent = fromEvent(triggerEl, 'mousemove');
const triggerLeaveEvent = fromEvent(triggerEl, 'mouseleave');

// setShow(true) 之后是异步显示窗体的,此时无法获取窗体dom,所以需要延时一下
const balloonLeaveEvent = defer(() =>
fromEvent(balloonElRef.current!, 'mouseleave'),
).pipe(retry({ count: 5, delay: 2 }));

const balloonEnterEvent = defer(() =>
fromEvent(balloonElRef.current!, 'mouseenter'),
).pipe(retry({ count: 5, delay: 2 }));

const leaveEvent = merge(triggerLeaveEvent, balloonLeaveEvent)
const leaveEvent = merge(
triggerLeaveEvent,
leaveBalloonSubject.current.asObservable(),
)
.pipe(
switchMap(() =>
of(null).pipe(
delay(leaveDelay),
takeUntil(triggerMoveEvent),
takeUntil(balloonEnterEvent),
takeUntil(enterBalloonSubject.current.asObservable()),
),
),
takeUntil(triggerEnterEvent),
Expand Down Expand Up @@ -94,6 +88,12 @@ export function useShowController(
) {
const [show, _setShow] = useState(false);
const nextEffect = useNextEffect();
const leaveBalloonSubject = useRef(new Subject<void>());
const enterBalloonSubject = useRef(new Subject<void>());
useBeforeDestroy(() => {
leaveBalloonSubject.current.unsubscribe();
enterBalloonSubject.current.unsubscribe();
});

// 事件触发启动
useEffect(() => {
Expand All @@ -108,12 +108,13 @@ export function useShowController(
case 'hover':
return hoverTriggerHandler(
el,
balloonElRef,
open,
close,
enterDelay,
leaveDelay,
show,
enterBalloonSubject,
leaveBalloonSubject,
);
case 'click':
const triggerClick$ = fromEvent(el, 'click').pipe(
Expand Down Expand Up @@ -182,7 +183,7 @@ export function useShowController(
_setShow(visible);
}, [visible, disabled]);

return show;
return [show, enterBalloonSubject, leaveBalloonSubject] as const;

function setShow(value: ((prevValue: boolean) => boolean) | boolean): void {
const onChange: Exclude<typeof onVisibleChange, undefined> = onVisibleChange
Expand Down

0 comments on commit 109cafc

Please sign in to comment.