Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add useMouseInElement hook #2616

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const menus = [
'useKeyPress',
'useLongPress',
'useMouse',
'useMouseInElement',
'useResponsive',
'useScroll',
'useSize',
Expand Down
2 changes: 2 additions & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import useLongPress from './useLongPress';
import useMap from './useMap';
import useMemoizedFn from './useMemoizedFn';
import useMount from './useMount';
import useMouseInElement from './useMouseInElement';
import useMouse from './useMouse';
import useNetwork from './useNetwork';
import usePagination from './usePagination';
Expand Down Expand Up @@ -101,6 +102,7 @@ export {
useDebounceEffect,
usePrevious,
useMouse,
useMouseInElement,
useScroll,
useClickAway,
useFullscreen,
Expand Down
49 changes: 49 additions & 0 deletions packages/hooks/src/useMouseInElement/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { renderHook, waitFor } from '@testing-library/react';
import useMouseInElement, { type Result } from '../index';

describe('useMouseInElement', () => {
function moveMouse(x: number, y: number) {
document.dispatchEvent(
new MouseEvent('mousemove', {
clientX: x,
clientY: y,
}),
);
}
const targetEl = document.createElement('div');
const getBoundingClientRectMock = jest.spyOn(HTMLElement.prototype, 'getBoundingClientRect');
getBoundingClientRectMock.mockReturnValue({
left: 100,
top: 100,
width: 200,
height: 300,
} as DOMRect);

it('mouse in element', async () => {
const inCb = jest.fn((result: Result) => result);
const { result } = renderHook(() => useMouseInElement(() => targetEl, inCb));
moveMouse(110, 110);
await waitFor(() => expect(result.current.clientX).toBe(110));
expect(result.current.clientY).toBe(110);
expect(inCb).toBeCalled();
expect(result.current.elementW).toBe(200);
expect(result.current.elementH).toBe(300);
expect(result.current.elementPosX).toBe(100);
expect(result.current.elementPosY).toBe(100);
expect(result.current.isInside).toBeTruthy();
});

it('mouse out element', async () => {
const outCb = jest.fn((result: Result) => result);
const { result } = renderHook(() => useMouseInElement(() => targetEl, undefined, outCb));
moveMouse(80, 80);
await waitFor(() => expect(result.current.clientX).toBe(80));
expect(outCb).toBeCalled();
expect(result.current.clientY).toBe(80);
expect(result.current.elementW).toBe(200);
expect(result.current.elementH).toBe(300);
expect(result.current.elementPosX).toBe(100);
expect(result.current.elementPosY).toBe(100);
expect(result.current.isInside).toBeFalsy();
});
});
34 changes: 34 additions & 0 deletions packages/hooks/src/useMouseInElement/demo/demo1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* title: Basic usage
*
* title.zh-CN: 基础用法
*/

import { useMouseInElement } from 'ahooks';
import React, { useRef } from 'react';

const App: React.FC = () => {
const ref = useRef<HTMLDivElement>(null);
const { isInside } = useMouseInElement(
ref.current,
(res) => {
console.log('inside');
},
() => {
console.log('outside');
},
);

return (
<div>
<div
ref={ref}
style={{ width: 200, height: 200, padding: 12, border: '1px solid #000', marginBottom: 8 }}
>
isInside:{String(isInside)}
</div>
</div>
);
};

export default App;
40 changes: 40 additions & 0 deletions packages/hooks/src/useMouseInElement/index.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
nav:
path: /hooks
---

# useMouseInElement

A Hook that listens to whether the current mouse is on the specified DOM。

## Examples

### Default Usage

<code src="./demo/demo1.tsx" />

## API

```typescript
const { isInside } = useMouseInElement(target: Target);
```

### Params

| Property | Description | Type | Default |
| ----------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------- |
| target | DOM element or ref | `Element` \| `() => Element` \| `MutableRefObject<Element>` | Document.body |
| inCallback | When the state changes, trigger a callback function within DOM | (result: Result) => void | undefined |
| outCallback | When the state changes, it is not within the DOM and triggers a callback function once | (result: Result) => void | undefined |

### Result

| Property | Description | Type |
| ----------- | ------------------------------------------------------------------------------------------------------------------ | --------- |
| clientX | Position left relative to the upper left edge of the content area | `number` |
| clientY | Position top relative to the upper left edge of the content area | `number` |
| elementH | Target element height | `number` |
| elementW | Target element width | `number` |
| elementPosX | The position of the target element left relative to the top left of the fully rendered content area in the browser | `number` |
| elementPosY | The position of the target element top relative to the top left of the fully rendered content area in the browser | `number` |
| isInside | Is the mouse on the current element | `boolean` |
83 changes: 83 additions & 0 deletions packages/hooks/src/useMouseInElement/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { getTargetElement, type BasicTarget } from '../utils/domTarget';
import { useState, useEffect, useRef } from 'react';
import useMouse, { type CursorState } from '../useMouse';
import useEventListener from '../useEventListener';

type ElementStateKeys = 'elementH' | 'elementW' | 'elementPosX' | 'elementPosY';

export type ElementState = Pick<CursorState, ElementStateKeys>;

export type Result = {
clientX: number;
clientY: number;
isInside: boolean;
} & ElementState;

const useMouseInElement = (
target?: BasicTarget,
inCallback?: (result: Result) => void,
outCallback?: (result: Result) => void,
) => {
const { clientX, clientY } = useMouse();
const elementStatus = useRef<ElementState>({
elementH: 0,
elementW: 0,
elementPosX: 0,
elementPosY: 0,
});

const [isInside, setIsInside] = useState(false);

const [el, setEl] = useState<HTMLElement>(window?.document.body);

useEffect(() => {
const targetElement = getTargetElement(target);
setEl((targetElement as HTMLElement) ?? window?.document.body);
}, [target]);

useEffect(() => {
if (!el || !(el instanceof HTMLElement)) return;

const { left, top, width, height } = el.getBoundingClientRect();
elementStatus.current.elementPosX = left;
elementStatus.current.elementPosY = top;
elementStatus.current.elementW = width;
elementStatus.current.elementH = height;

const elX = clientX - elementStatus.current.elementPosX;
const elY = clientY - elementStatus.current.elementPosY;
const isOutside =
width === 0 || height === 0 || elX < 0 || elY < 0 || elX > width || elY > height;
setIsInside(!isOutside);
}, [el, clientX, clientY]);

useEventListener(
'mouseleave',
() => {
setIsInside(false);
},
{ target: document },
);

const result: Result = {
clientX,
clientY,
elementW: elementStatus.current.elementW,
elementH: elementStatus.current.elementH,
elementPosX: elementStatus.current.elementPosX,
elementPosY: elementStatus.current.elementPosY,
isInside,
};

useEffect(() => {
if (!isInside) {
outCallback?.(result);
} else {
inCallback?.(result);
}
}, [isInside]);

return result;
};

export default useMouseInElement;
40 changes: 40 additions & 0 deletions packages/hooks/src/useMouseInElement/index.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
nav:
path: /hooks
---

# useMouseInElement

一个监听当前鼠标是不是在指定的 DOM 上的 Hook。

## 代码演示

### 基础用法

<code src="./demo/demo1.tsx" />

## API

```typescript
const { isInside } = useMouseInElement(target: Target);
```

### Params

| 参数 | 说明 | 类型 | 默认值 |
| ----------- | --------------------------------------- | ----------------------------------------------------------- | --------- |
| target | DOM 节点或者 Ref | `Element` \| `() => Element` \| `MutableRefObject<Element>` | - |
| callback | 状态变更时处于dom内部触发一次回调函数 | (result: Result) => {} | undefined |
| outCallback | 状态变更时不处于dom内部触发一次回调函数 | (result: Result) => void | undefined |

### Result

| 参数 | 说明 | 类型 |
| ----------- | ------------------------------ | --------- |
| clientX | 距离当前视窗左侧的距离 | `number` |
| clientY | 距离当前视窗顶部的距离 | `number` |
| elementH | 指定元素的高 | `number` |
| elementW | 指定元素的宽 | `number` |
| elementPosX | 指定元素距离完整页面左侧的距离 | `number` |
| elementPosY | 指定元素距离完整页面顶部的距离 | `number` |
| isInside | 鼠标是否在当前元素上 | `boolean` |
Loading