From 827464341619c31ed5916a896115002269f1555d Mon Sep 17 00:00:00 2001 From: admin-zlj <1759629281@qq.com> Date: Sat, 8 Feb 2025 11:43:48 +0800 Subject: [PATCH 1/2] feat: add useStickyFixed hook --- config/hooks.ts | 1 + packages/hooks/src/index.ts | 2 + .../hooks/src/useStickyFixed/demo/demo1.tsx | 47 ++++++++++++++++++ .../hooks/src/useStickyFixed/demo/demo2.tsx | 46 ++++++++++++++++++ .../hooks/src/useStickyFixed/index.en-US.md | 47 ++++++++++++++++++ packages/hooks/src/useStickyFixed/index.ts | 48 +++++++++++++++++++ .../hooks/src/useStickyFixed/index.zh-CN.md | 43 +++++++++++++++++ 7 files changed, 234 insertions(+) create mode 100644 packages/hooks/src/useStickyFixed/demo/demo1.tsx create mode 100644 packages/hooks/src/useStickyFixed/demo/demo2.tsx create mode 100644 packages/hooks/src/useStickyFixed/index.en-US.md create mode 100644 packages/hooks/src/useStickyFixed/index.ts create mode 100644 packages/hooks/src/useStickyFixed/index.zh-CN.md diff --git a/config/hooks.ts b/config/hooks.ts index 26ffff428a..04f70b18ef 100644 --- a/config/hooks.ts +++ b/config/hooks.ts @@ -101,6 +101,7 @@ export const menus = [ 'useScroll', 'useSize', 'useFocusWithin', + 'useStickyFixed', ], }, { diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 55c7232b0d..c84d76f81b 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -76,6 +76,7 @@ import useWebSocket from './useWebSocket'; import useWhyDidYouUpdate from './useWhyDidYouUpdate'; import useMutationObserver from './useMutationObserver'; import useTheme from './useTheme'; +import useStickyFixed from './useStickyFixed'; export { useRequest, @@ -158,4 +159,5 @@ export { useResetState, useMutationObserver, useTheme, + useStickyFixed, }; diff --git a/packages/hooks/src/useStickyFixed/demo/demo1.tsx b/packages/hooks/src/useStickyFixed/demo/demo1.tsx new file mode 100644 index 0000000000..048f193c38 --- /dev/null +++ b/packages/hooks/src/useStickyFixed/demo/demo1.tsx @@ -0,0 +1,47 @@ +/** + * title: Basic usage + * desc: Need to input the sticky positioning element target and the rolling container scrollTarget, ScrollTarget defaults to document + * + * title.zh-CN: 基础用法 + * desc.zh-CN: 需要传入粘性定位元素target和滚动容器scrollTarget, scrollTarget默认为document + * + */ + +import React, { useRef, useState } from 'react'; +import { useStickyFixed } from 'ahooks'; + +export default () => { + const [topV, setTopV] = useState(0); + + const targetRef = useRef(null); + const scrollTargetRef = useRef(null); + + const [isFixed] = useStickyFixed(targetRef, { scrollTarget: scrollTargetRef }); + + const fixedStyle = { background: 'pink' }; + + return ( + <> +
+ top: setTopV(Number(e.target.value))} /> +
+ +
+
top content
+
+ sticky dom +
+
bottom content
+
+ +
isFixed :{`${isFixed}`}
+ + ); +}; diff --git a/packages/hooks/src/useStickyFixed/demo/demo2.tsx b/packages/hooks/src/useStickyFixed/demo/demo2.tsx new file mode 100644 index 0000000000..9b11821d74 --- /dev/null +++ b/packages/hooks/src/useStickyFixed/demo/demo2.tsx @@ -0,0 +1,46 @@ +/** + * title: Pass in DOM element + * desc: Pass in a function that returns the DOM element. + * + * title.zh-CN: 传入 DOM 元素 + * desc.zh-CN: 传入 function 并返回一个 dom 元素。 + * + */ + +import React, { useState } from 'react'; +import { useStickyFixed } from 'ahooks'; + +export default () => { + const [topV, setTopV] = useState(0); + + const [isFixed] = useStickyFixed(() => document.getElementById('target'), { + scrollTarget: () => document.getElementById('scrollTarget'), + }); + + const fixedStyle = { background: 'pink' }; + + return ( + <> +
+ top: setTopV(Number(e.target.value))} /> +
+ +
+
top content
+
+ sticky dom +
+
bottom content
+
+ +
isFixed :{`${isFixed}`}
+ + ); +}; diff --git a/packages/hooks/src/useStickyFixed/index.en-US.md b/packages/hooks/src/useStickyFixed/index.en-US.md new file mode 100644 index 0000000000..6c1dc711d4 --- /dev/null +++ b/packages/hooks/src/useStickyFixed/index.en-US.md @@ -0,0 +1,47 @@ +--- +nav: + path: /hooks +--- + +# useStickyFixed + +Observe whether the element of 'position: sticky' is in a fixed suction state + +## Examples + +### Basic usage + + + +### Pass in DOM element + + + +## API + +```typescript +const [isFixed] = useStickyFixed(targetRef, { scrollTarget }); +``` + +### Params + +| Property | Description | Type | Default | +| -------- | ---------------------------------------- | ----------------------------------------------------------- | ------- | +| target | `position: sticky` 's Dom element or ref | `() => Element` \| `Element` \| `MutableRefObject` | - | +| options | More config | `Options` | - | + +### Options + +| Property | Description | Type | Default | +| ------------- | --------------------------------------------------- | ----------------------------------------------------------- | ------- | +| scrollTarget | The element or ref of the DOM in the scrolling area | `() => Element` \| `Element` \| `MutableRefObject` | document| + + +### Result + +| Property | Description | Type | +| ------------- | ------------------------------------------------------ | --------- | +| isFixed | Is the `position: sticky` 's Dom in the 'fixed' state | `boolean` | + + + diff --git a/packages/hooks/src/useStickyFixed/index.ts b/packages/hooks/src/useStickyFixed/index.ts new file mode 100644 index 0000000000..e690ae1a98 --- /dev/null +++ b/packages/hooks/src/useStickyFixed/index.ts @@ -0,0 +1,48 @@ +import { useRef } from 'react'; +import { getTargetElement, type BasicTarget } from '../utils/domTarget'; +import useEffectWithTarget from '../utils/useEffectWithTarget'; +import useRafState from '../useRafState'; + +function useStickyFixed( + target: BasicTarget, + options?: { + scrollTarget?: BasicTarget; + }, +): [boolean] { + const { scrollTarget } = options || {}; + + const [state, setState] = useRafState(false); + const lastTopRef = useRef(0); + + useEffectWithTarget( + () => { + const scrollElement = getTargetElement(scrollTarget, document); + if (!scrollElement) { + return; + } + + const stickyElement = getTargetElement(target); + if (!stickyElement) { + return; + } + + const handleScroll = () => { + const rect = stickyElement.getBoundingClientRect(); + const currentTop = rect.top; + const lastTop = lastTopRef.current; + setState(currentTop === lastTop); + lastTopRef.current = currentTop; + }; + + scrollElement.addEventListener('scroll', handleScroll); + return () => { + scrollElement.removeEventListener('scroll', handleScroll); + }; + }, + [], + target, + ); + + return [state]; +} +export default useStickyFixed; diff --git a/packages/hooks/src/useStickyFixed/index.zh-CN.md b/packages/hooks/src/useStickyFixed/index.zh-CN.md new file mode 100644 index 0000000000..c80f7672e9 --- /dev/null +++ b/packages/hooks/src/useStickyFixed/index.zh-CN.md @@ -0,0 +1,43 @@ +--- +nav: + path: /hooks +--- + +# useStickyFixed + +观察粘性定位(`position: sticky`)的元素,是否处于吸顶固定状态 + +## 代码演示 + +### 基础用法 + + + +### 传入 DOM 元素 + + + +## API + +```typescript +const [isFixed] = useStickyFixed(targetRef, { scrollTarget }); +``` + +### Params + +| 参数 | 说明 | 类型 | 默认值 | +| ------- | -------------------------------- | ---------------------------------------------------------- | ------ | +| target | 粘性定位的 DOM 节点或者 Ref 对象 | `Element` \|`() => Element` \| `MutableRefObject` | - | +| options | 额外的配置项 | `Options` | - | + +### Options + +| 参数 | 说明 | 类型 | 默认值 | +| ------------ | -------------------------------- | ------------------------------------------------------------------------ | -------- | +| scrollTarget | 滚动区域的 DOM 节点或者 Ref 对象 | `Element` \| `Document` \|`() => Element` \| `MutableRefObject` | document | + +### Result + +| 参数 | 说明 | 类型 | +| ------- | --------------------------------- | --------- | +| isFixed | 粘性定位元素是否处于 `fixed` 状态 | `boolean` | From 6a19aeb6166c1d6f47c9564c1311a7f46b1f61cc Mon Sep 17 00:00:00 2001 From: admin-zlj <1759629281@qq.com> Date: Sat, 8 Feb 2025 15:08:09 +0800 Subject: [PATCH 2/2] feat: add tests for useStickyFixed hook --- .../useStickyFixed/__tests__/index.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/hooks/src/useStickyFixed/__tests__/index.test.ts diff --git a/packages/hooks/src/useStickyFixed/__tests__/index.test.ts b/packages/hooks/src/useStickyFixed/__tests__/index.test.ts new file mode 100644 index 0000000000..7f6bd2ccd1 --- /dev/null +++ b/packages/hooks/src/useStickyFixed/__tests__/index.test.ts @@ -0,0 +1,42 @@ +import { act, renderHook } from '@testing-library/react'; +import useStickyFixed from '../index'; + +const scrollElement = document.createElement('div'); +scrollElement.style.overflowY = 'scroll'; +scrollElement.style.height = '200px'; // 设置高度以允许滚动 +scrollElement.style.width = '200px'; // 设置高度以允许滚动 +document.body.appendChild(scrollElement); + +const topElement = document.createElement('div'); //top元素用于填充 +topElement.style.height = '100px'; +scrollElement.appendChild(topElement); + +const targetElement = document.createElement('div'); // 模拟 sticky 的元素 +targetElement.style.position = 'sticky'; +targetElement.style.top = '0'; +targetElement.style.height = '20px'; //其他元素用于填充 +scrollElement.appendChild(targetElement); + +const bottomElement = document.createElement('div'); //bottom元素用于填充 +bottomElement.style.height = '200px'; +scrollElement.appendChild(bottomElement); + +describe('useStickyFixed', () => { + it('should set state to false when not scrolling', () => { + const { result } = renderHook(() => useStickyFixed(targetElement, { scrollTarget: scrollElement })); + + // 开始未滚动 返回false + expect(result.current[0]).toBe(false); + }); + + it('should not throw if target is not found', () => { + const { result } = renderHook(() => useStickyFixed(null, { scrollTarget: scrollElement })); + + act(() => { + scrollElement.scrollTop = 0; + scrollElement.dispatchEvent(new Event('scroll')); + }); + + expect(result.current[0]).toBe(false); // 没有抛出错误 + }); +});