From 3dec947474479ecda3d6ebe3608f971ea15bd28d Mon Sep 17 00:00:00 2001 From: zhuyue Date: Tue, 30 Jul 2024 21:55:25 +0800 Subject: [PATCH] feat: add useMouseInElement hook --- config/hooks.ts | 1 + packages/hooks/src/index.ts | 2 + .../useMouseInElement/__tests__/index.test.ts | 49 +++++++++++ .../src/useMouseInElement/demo/demo1.tsx | 34 ++++++++ .../src/useMouseInElement/index.en-US.md | 40 +++++++++ packages/hooks/src/useMouseInElement/index.ts | 83 +++++++++++++++++++ .../src/useMouseInElement/index.zh-CN.md | 40 +++++++++ 7 files changed, 249 insertions(+) create mode 100644 packages/hooks/src/useMouseInElement/__tests__/index.test.ts create mode 100644 packages/hooks/src/useMouseInElement/demo/demo1.tsx create mode 100644 packages/hooks/src/useMouseInElement/index.en-US.md create mode 100644 packages/hooks/src/useMouseInElement/index.ts create mode 100644 packages/hooks/src/useMouseInElement/index.zh-CN.md diff --git a/config/hooks.ts b/config/hooks.ts index fa74274cf2..64bccbbaf8 100644 --- a/config/hooks.ts +++ b/config/hooks.ts @@ -96,6 +96,7 @@ export const menus = [ 'useKeyPress', 'useLongPress', 'useMouse', + 'useMouseInElement', 'useResponsive', 'useScroll', 'useSize', diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index f977e5ff86..2351c9ed3e 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -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'; @@ -101,6 +102,7 @@ export { useDebounceEffect, usePrevious, useMouse, + useMouseInElement, useScroll, useClickAway, useFullscreen, diff --git a/packages/hooks/src/useMouseInElement/__tests__/index.test.ts b/packages/hooks/src/useMouseInElement/__tests__/index.test.ts new file mode 100644 index 0000000000..37b91e272b --- /dev/null +++ b/packages/hooks/src/useMouseInElement/__tests__/index.test.ts @@ -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(); + }); +}); diff --git a/packages/hooks/src/useMouseInElement/demo/demo1.tsx b/packages/hooks/src/useMouseInElement/demo/demo1.tsx new file mode 100644 index 0000000000..5f88f9d6c6 --- /dev/null +++ b/packages/hooks/src/useMouseInElement/demo/demo1.tsx @@ -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(null); + const { isInside } = useMouseInElement( + ref.current, + (res) => { + console.log('inside'); + }, + () => { + console.log('outside'); + }, + ); + + return ( +
+
+ isInside:{String(isInside)} +
+
+ ); +}; + +export default App; diff --git a/packages/hooks/src/useMouseInElement/index.en-US.md b/packages/hooks/src/useMouseInElement/index.en-US.md new file mode 100644 index 0000000000..44129c8cac --- /dev/null +++ b/packages/hooks/src/useMouseInElement/index.en-US.md @@ -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 + + + +## API + +```typescript +const { isInside } = useMouseInElement(target: Target); +``` + +### Params + +| Property | Description | Type | Default | +| ----------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------- | +| target | DOM element or ref | `Element` \| `() => Element` \| `MutableRefObject` | 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` | diff --git a/packages/hooks/src/useMouseInElement/index.ts b/packages/hooks/src/useMouseInElement/index.ts new file mode 100644 index 0000000000..c3086dbdd8 --- /dev/null +++ b/packages/hooks/src/useMouseInElement/index.ts @@ -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; + +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({ + elementH: 0, + elementW: 0, + elementPosX: 0, + elementPosY: 0, + }); + + const [isInside, setIsInside] = useState(false); + + const [el, setEl] = useState(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; diff --git a/packages/hooks/src/useMouseInElement/index.zh-CN.md b/packages/hooks/src/useMouseInElement/index.zh-CN.md new file mode 100644 index 0000000000..291dc2b8d8 --- /dev/null +++ b/packages/hooks/src/useMouseInElement/index.zh-CN.md @@ -0,0 +1,40 @@ +--- +nav: + path: /hooks +--- + +# useMouseInElement + +一个监听当前鼠标是不是在指定的 DOM 上的 Hook。 + +## 代码演示 + +### 基础用法 + + + +## API + +```typescript +const { isInside } = useMouseInElement(target: Target); +``` + +### Params + +| 参数 | 说明 | 类型 | 默认值 | +| ----------- | --------------------------------------- | ----------------------------------------------------------- | --------- | +| target | DOM 节点或者 Ref | `Element` \| `() => Element` \| `MutableRefObject` | - | +| 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` |