Skip to content

Commit 90dce6d

Browse files
Add exposure hooks with stay-time tracking, tests, and docs (#17)
* feat(add-exposure-hooks): add exposure hooks for nodes/pages and stay time utility * feat(add-exposure-hooks): Add exposure hooks for node/page/stay time with tests and bilingual docs * chore: add changeset --------- Co-authored-by: BitterGourd <91231822+gaoachao@users.noreply.github.com>
1 parent 98d8dc3 commit 90dce6d

18 files changed

+1314
-19
lines changed

.changeset/spotty-chefs-grow.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@lynx-js/react-use": patch
3+
---
4+
5+
Introduce exposure hooks:
6+
7+
- `useExposureForNode`: Node-level exposure hook with optional admission gating.
8+
- `useExposureForPage`: Page-level exposure hook handling multiple items via `GlobalEventEmitter`.
9+
- `useStayTime`: Tracks element visibility duration with optional manual control.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# useExposureForNode
2+
3+
Node-level exposure hook with optional admission gating.
4+
5+
## Usage
6+
7+
```tsx
8+
import { useExposureForNode } from "@lynx-js/react-use";
9+
10+
export function HeroCard() {
11+
const { isInView, exposureProps } = useExposureForNode({
12+
attrs: { "exposure-id": "hero-card", "exposure-scene": "home" },
13+
admissionTimeMs: 100,
14+
onAppear: (detail) => {
15+
console.log("appear", detail["exposure-id"]);
16+
},
17+
onDisappear: (detail) => {
18+
console.log("disappear", detail["exposure-id"]);
19+
},
20+
onChange: (_detail, { isInView }) => {
21+
console.log("isInView", isInView);
22+
},
23+
});
24+
25+
return (
26+
<view {...exposureProps}>
27+
<text>{isInView ? "Visible" : "Hidden"}</text>
28+
</view>
29+
);
30+
}
31+
```
32+
33+
## Type Declarations
34+
35+
```ts
36+
export interface IUseExposureForNodeOptions<
37+
EA extends Record<string, string | number | boolean | undefined>
38+
> {
39+
attrs?: TExposureAttrBag & EA;
40+
admissionTimeMs?: number;
41+
onAppear?: (e: UIAppearanceTargetDetail) => void;
42+
onDisappear?: (e: UIAppearanceTargetDetail) => void;
43+
onChange?: (
44+
e: UIAppearanceTargetDetail,
45+
info: { isInView: boolean }
46+
) => void;
47+
}
48+
49+
export interface IUseExposureForNodeReturn<
50+
EA extends Record<string, string | number | boolean | undefined>
51+
> {
52+
isInView: boolean;
53+
exposureProps: {
54+
binduiappear?: (e: UIAppearanceTargetDetail) => void;
55+
binduidisappear?: (e: UIAppearanceTargetDetail) => void;
56+
} & TExposureAttrBag &
57+
EA;
58+
}
59+
```
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# useExposureForPage
2+
3+
Page-level exposure hook that consumes `GlobalEventEmitter` events and manages multiple items with admission gating.
4+
5+
## Usage
6+
7+
```tsx
8+
import { useExposureForPage } from "@lynx-js/react-use";
9+
10+
const items = [
11+
{ id: "card-1", title: "A" },
12+
{ id: "card-2", title: "B" },
13+
];
14+
15+
export function Feed() {
16+
const { isInView, exposureProps } = useExposureForPage({
17+
attrs: { "exposure-scene": "feed" },
18+
admissionTimeMs: 80,
19+
onAppear: (_detail, info) => console.log("appear", info.exposureId),
20+
onDisappear: (_detail, info) => console.log("disappear", info.exposureId),
21+
});
22+
23+
return (
24+
<view>
25+
{items.map((item) => (
26+
<view key={item.id} {...exposureProps({ id: item.id })}>
27+
<text>
28+
{item.title} {isInView(item.id) ? "(visible)" : "(hidden)"}
29+
</text>
30+
</view>
31+
))}
32+
</view>
33+
);
34+
}
35+
```
36+
37+
## Type Declarations
38+
39+
```ts
40+
export interface IUseExposureForPageOptions<
41+
EA extends Record<string, string | number | boolean | undefined>
42+
> {
43+
attrs?: Omit<TExposureAttrBag, "exposure-id"> & EA;
44+
admissionMs?: number;
45+
admissionTimeMs?: number;
46+
onAppear?: (
47+
e: UIAppearanceTargetDetail,
48+
info: { exposureId?: string; exposureScene?: string }
49+
) => void;
50+
onDisappear?: (
51+
e: UIAppearanceTargetDetail,
52+
info: { exposureId?: string; exposureScene?: string }
53+
) => void;
54+
onChange?: (
55+
e: UIAppearanceTargetDetail,
56+
info: { isInView: boolean; exposureId?: string; exposureScene?: string }
57+
) => void;
58+
}
59+
60+
export interface IUseExposureForPageReturn<
61+
EA extends Record<string, string | number | boolean | undefined>
62+
> {
63+
isInView: (exposureId: string) => boolean;
64+
exposureProps: (p: { id: string; extra?: EA }) => TExposureAttrBag & EA;
65+
}
66+
```

docs/en/exposures/useStayTime.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# useStayTime
2+
3+
Track how long an element stays visible, with optional manual control.
4+
5+
## Usage
6+
7+
```tsx
8+
import { useStayTime } from "@lynx-js/react-use";
9+
10+
export function StayTimer() {
11+
const {
12+
stayTimeMs,
13+
isRunning,
14+
exposureProps,
15+
pause,
16+
resume,
17+
reset,
18+
} = useStayTime({
19+
admissionTimeMs: 60,
20+
onRunningChange: ({ isRunning, stayTimeMs }) => {
21+
console.log("running?", isRunning, "ms:", stayTimeMs);
22+
},
23+
});
24+
25+
return (
26+
<view>
27+
<view {...exposureProps}>
28+
<text>Stay time: {stayTimeMs}ms</text>
29+
<text>Status: {isRunning ? "running" : "stopped"}</text>
30+
</view>
31+
<button bindtap={pause}>Pause</button>
32+
<button bindtap={resume}>Resume</button>
33+
<button bindtap={reset}>Reset</button>
34+
</view>
35+
);
36+
}
37+
```
38+
39+
## Type Declarations
40+
41+
```ts
42+
export interface IUseStayTimeOptions<
43+
EA extends Record<string, string | number | boolean | undefined>
44+
> extends IUseExposureForNodeOptions<EA> {
45+
isStaying?: boolean;
46+
onRunningChange?: (info: { isRunning: boolean; stayTimeMs: number }) => void;
47+
}
48+
49+
export interface IUseStayTimeReturn<
50+
EA extends Record<string, string | number | boolean | undefined>
51+
> {
52+
stayTimeMs: number;
53+
isRunning: boolean;
54+
exposureProps:
55+
| {
56+
binduiappear?: (e: UIAppearanceTargetDetail) => void;
57+
binduidisappear?: (e: UIAppearanceTargetDetail) => void;
58+
} & TExposureAttrBag &
59+
EA;
60+
pause: () => void;
61+
resume: () => void;
62+
reset: () => void;
63+
}
64+
```
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# useExposureForNode
2+
3+
节点级曝光 Hook,可选曝光准入等待。
4+
5+
## 示例
6+
7+
```tsx
8+
import { useExposureForNode } from "@lynx-js/react-use";
9+
10+
export function HeroCard() {
11+
const { isInView, exposureProps } = useExposureForNode({
12+
attrs: { "exposure-id": "hero-card", "exposure-scene": "home" },
13+
admissionTimeMs: 100,
14+
onAppear: (detail) => {
15+
console.log("appear", detail["exposure-id"]);
16+
},
17+
onDisappear: (detail) => {
18+
console.log("disappear", detail["exposure-id"]);
19+
},
20+
onChange: (_detail, { isInView }) => {
21+
console.log("isInView", isInView);
22+
},
23+
});
24+
25+
return (
26+
<view {...exposureProps}>
27+
<text>{isInView ? "可见" : "不可见"}</text>
28+
</view>
29+
);
30+
}
31+
```
32+
33+
## 类型定义
34+
35+
```ts
36+
export interface IUseExposureForNodeOptions<
37+
EA extends Record<string, string | number | boolean | undefined>
38+
> {
39+
attrs?: TExposureAttrBag & EA;
40+
admissionTimeMs?: number;
41+
onAppear?: (e: UIAppearanceTargetDetail) => void;
42+
onDisappear?: (e: UIAppearanceTargetDetail) => void;
43+
onChange?: (
44+
e: UIAppearanceTargetDetail,
45+
info: { isInView: boolean }
46+
) => void;
47+
}
48+
49+
export interface IUseExposureForNodeReturn<
50+
EA extends Record<string, string | number | boolean | undefined>
51+
> {
52+
isInView: boolean;
53+
exposureProps: {
54+
binduiappear?: (e: UIAppearanceTargetDetail) => void;
55+
binduidisappear?: (e: UIAppearanceTargetDetail) => void;
56+
} & TExposureAttrBag &
57+
EA;
58+
}
59+
```
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# useExposureForPage
2+
3+
页面级曝光 Hook,监听 `GlobalEventEmitter` 的曝光/反曝光事件并支持准入等待。
4+
5+
## 示例
6+
7+
```tsx
8+
import { useExposureForPage } from "@lynx-js/react-use";
9+
10+
const items = [
11+
{ id: "card-1", title: "A" },
12+
{ id: "card-2", title: "B" },
13+
];
14+
15+
export function Feed() {
16+
const { isInView, exposureProps } = useExposureForPage({
17+
attrs: { "exposure-scene": "feed" },
18+
admissionTimeMs: 80,
19+
onAppear: (_detail, info) => console.log("appear", info.exposureId),
20+
onDisappear: (_detail, info) => console.log("disappear", info.exposureId),
21+
});
22+
23+
return (
24+
<view>
25+
{items.map((item) => (
26+
<view key={item.id} {...exposureProps({ id: item.id })}>
27+
<text>
28+
{item.title} {isInView(item.id) ? "(可见)" : "(不可见)"}
29+
</text>
30+
</view>
31+
))}
32+
</view>
33+
);
34+
}
35+
```
36+
37+
## 类型定义
38+
39+
```ts
40+
export interface IUseExposureForPageOptions<
41+
EA extends Record<string, string | number | boolean | undefined>
42+
> {
43+
attrs?: Omit<TExposureAttrBag, "exposure-id"> & EA;
44+
admissionMs?: number;
45+
admissionTimeMs?: number;
46+
onAppear?: (
47+
e: UIAppearanceTargetDetail,
48+
info: { exposureId?: string; exposureScene?: string }
49+
) => void;
50+
onDisappear?: (
51+
e: UIAppearanceTargetDetail,
52+
info: { exposureId?: string; exposureScene?: string }
53+
) => void;
54+
onChange?: (
55+
e: UIAppearanceTargetDetail,
56+
info: { isInView: boolean; exposureId?: string; exposureScene?: string }
57+
) => void;
58+
}
59+
60+
export interface IUseExposureForPageReturn<
61+
EA extends Record<string, string | number | boolean | undefined>
62+
> {
63+
isInView: (exposureId: string) => boolean;
64+
exposureProps: (p: { id: string; extra?: EA }) => TExposureAttrBag & EA;
65+
}
66+
```

docs/zh/exposures/useStayTime.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# useStayTime
2+
3+
用于统计元素可见时长的 Hook,支持手动控制。
4+
5+
## 示例
6+
7+
```tsx
8+
import { useStayTime } from "@lynx-js/react-use";
9+
10+
export function StayTimer() {
11+
const {
12+
stayTimeMs,
13+
isRunning,
14+
exposureProps,
15+
pause,
16+
resume,
17+
reset,
18+
} = useStayTime({
19+
admissionTimeMs: 60,
20+
onRunningChange: ({ isRunning, stayTimeMs }) => {
21+
console.log("running?", isRunning, "ms:", stayTimeMs);
22+
},
23+
});
24+
25+
return (
26+
<view>
27+
<view {...exposureProps}>
28+
<text>停留时长: {stayTimeMs}ms</text>
29+
<text>状态: {isRunning ? "计时中" : "已停止"}</text>
30+
</view>
31+
<button bindtap={pause}>暂停</button>
32+
<button bindtap={resume}>恢复</button>
33+
<button bindtap={reset}>重置</button>
34+
</view>
35+
);
36+
}
37+
```
38+
39+
## 类型定义
40+
41+
```ts
42+
export interface IUseStayTimeOptions<
43+
EA extends Record<string, string | number | boolean | undefined>
44+
> extends IUseExposureForNodeOptions<EA> {
45+
isStaying?: boolean;
46+
onRunningChange?: (info: { isRunning: boolean; stayTimeMs: number }) => void;
47+
}
48+
49+
export interface IUseStayTimeReturn<
50+
EA extends Record<string, string | number | boolean | undefined>
51+
> {
52+
stayTimeMs: number;
53+
isRunning: boolean;
54+
exposureProps:
55+
| {
56+
binduiappear?: (e: UIAppearanceTargetDetail) => void;
57+
binduidisappear?: (e: UIAppearanceTargetDetail) => void;
58+
} & TExposureAttrBag &
59+
EA;
60+
pause: () => void;
61+
resume: () => void;
62+
reset: () => void;
63+
}
64+
```

src/exposureBased/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { useExposureForNode } from './useExposureForNode.js';
2+
export { useExposureForPage } from './useExposureForPage.js';
3+
export { useStayTime } from './useStayTime.js';

0 commit comments

Comments
 (0)