Skip to content

Commit cb05ce1

Browse files
committed
feat: add factory for recognizing hover gesture
1 parent cc47840 commit cb05ce1

File tree

14 files changed

+357
-59
lines changed

14 files changed

+357
-59
lines changed

src/extracted/storePointerMoveMetadata.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ export function storePointerMoveMetadata<
6565
| 'touchstart'
6666
| 'touchmove'
6767
| 'touchend',
68-
Metadata extends PointerMoveMetadata & PointerStartMetadata & PointerTimeMetadata
69-
> (
68+
Metadata extends PointerMoveMetadata & PointerStartMetadata & PointerTimeMetadata<true>
69+
> ({ event, api }: {
7070
event: MouseEvent | TouchEvent,
7171
api: Parameters<RecognizeableEffect<Type, Metadata>>[1]
72-
): void {
72+
}): void {
7373
const { getMetadata } = api,
7474
metadata = getMetadata()
7575

src/extracted/storePointerStartMetadata.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ const initialMetadata: PointerStartMetadata = {
1717
}
1818

1919
export function storePointerStartMetadata<
20-
Type extends 'mousedown' | 'touchstart',
20+
Type extends 'mousedown' | 'touchstart' | 'mouseenter',
2121
Metadata extends PointerStartMetadata
22-
> (
22+
> ({ event, api }: {
2323
event: MouseEvent | TouchEvent,
2424
api: Parameters<RecognizeableEffect<Type, Metadata>>[1]
25-
): void {
25+
}): void {
2626
const { getMetadata } = api,
2727
metadata = getMetadata()
2828

src/extracted/storePointerTimeMetadata.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ import type { RecognizeableEffect } from '../classes'
33
import { createClone } from '../pipes/any'
44
import type { PointerMoveMetadata } from './storePointerMoveMetadata'
55

6-
export type PointerTimeMetadata = {
7-
times: {
8-
start: number,
9-
end: number,
10-
},
11-
duration: number,
12-
velocity: number,
13-
}
6+
export type PointerTimeMetadata<Moves extends boolean> = (
7+
& {
8+
times: {
9+
start: number,
10+
end: number,
11+
},
12+
duration: number,
13+
}
14+
& (Moves extends true ? { velocity: number } : {})
15+
)
1416

15-
const initialMetadata: PointerTimeMetadata = {
17+
const initialMetadata: PointerTimeMetadata<true> = {
1618
times: {
1719
start: 0,
1820
end: 0,
@@ -22,18 +24,27 @@ const initialMetadata: PointerTimeMetadata = {
2224
}
2325

2426
export function storePointerTimeMetadata<
25-
Type extends 'mousedown' | 'touchstart',
26-
Metadata extends PointerTimeMetadata & PointerMoveMetadata
27-
> (
27+
Type extends 'mousedown' | 'touchstart' | 'mouseenter',
28+
Moves extends boolean,
29+
Metadata extends (Moves extends true ? (PointerTimeMetadata<Moves> & PointerMoveMetadata) : PointerTimeMetadata<Moves>)
30+
> ({
31+
event,
32+
moves,
33+
api,
34+
getShouldStore,
35+
setRequest,
36+
recognize,
37+
}: {
2838
event: MouseEvent | TouchEvent,
39+
moves: Moves,
2940
api: Parameters<RecognizeableEffect<Type, Metadata>>[1],
3041
getShouldStore: () => boolean,
3142
setRequest: (request: number) => void,
3243
recognize?: RecognizeableEffect<
33-
'mousedown' | 'mousemove' | 'touchstart' | 'touchmove',
34-
PointerTimeMetadata & PointerMoveMetadata
44+
'mousedown' | 'mousemove' | 'touchstart' | 'touchmove' | 'mouseenter',
45+
Metadata
3546
>,
36-
): void {
47+
}): void {
3748
const { getSequence, getMetadata, getStatus, listenInjection: { effect } } = api,
3849
metadata = getMetadata()
3950

@@ -51,7 +62,12 @@ export function storePointerTimeMetadata<
5162
metadata.times.end = Math.round(timestamp)
5263
metadata.duration = Math.max(0, metadata.times.end - metadata.times.start)
5364
const durationFromPrevious = Math.max(0, metadata.times.end - previousEndTime)
54-
metadata.velocity = (metadata.distance.straight.fromPrevious / durationFromPrevious) || 0
65+
if (moves) {
66+
(metadata as PointerMoveMetadata & PointerTimeMetadata<true>).velocity = (
67+
((metadata as PointerMoveMetadata & PointerTimeMetadata<true>).distance.straight.fromPrevious / durationFromPrevious)
68+
|| 0
69+
)
70+
}
5571

5672
const event = getSequence().at(-1)
5773

src/factories/createHover.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Listenable } from '../classes/Listenable'
2+
import type { RecognizeableEffect } from '../classes'
3+
import { toHookApi, storePointerStartMetadata, storePointerTimeMetadata } from '../extracted'
4+
import type { PointerStartMetadata, PointerTimeMetadata, HookApi } from '../extracted'
5+
import { } from '../classes/Recognizeable'
6+
7+
export type HoverType = 'mouseenter' | 'mouseleave' | 'touchstart'
8+
9+
export type HoverMetadata = (
10+
& PointerStartMetadata
11+
& PointerTimeMetadata<false>
12+
)
13+
14+
export type HoverOptions = {
15+
minDuration?: number,
16+
onEnter?: HoverHook,
17+
onLeave?: HoverHook,
18+
}
19+
20+
export type HoverHook = (api: HoverHookApi) => any
21+
22+
export type HoverHookApi = HookApi<HoverType, HoverMetadata>
23+
24+
const defaultOptions: HoverOptions = {
25+
minDuration: 0,
26+
}
27+
28+
/**
29+
* [Docs](https://baleada.dev/docs/logic/factories/hover)
30+
*/
31+
export function createHover (options: HoverOptions = {}) {
32+
const {
33+
minDuration,
34+
onEnter,
35+
onLeave,
36+
} = { ...defaultOptions, ...options },
37+
stop = () => {
38+
window.cancelAnimationFrame(request)
39+
}
40+
41+
let request: number
42+
let mouseStatus: 'entered' | 'exited'
43+
44+
const mouseenter: RecognizeableEffect<'mouseenter', HoverMetadata> = (event, api) => {
45+
mouseStatus = 'entered'
46+
47+
storePointerStartMetadata({ event, api })
48+
storePointerTimeMetadata({
49+
event,
50+
moves: false,
51+
api,
52+
getShouldStore: () => mouseStatus === 'entered',
53+
setRequest: newRequest => request = newRequest,
54+
// @ts-expect-error
55+
recognize,
56+
})
57+
58+
onEnter?.(toHookApi(api))
59+
}
60+
61+
const recognize: RecognizeableEffect<'mouseenter', HoverMetadata> = (event, api) => {
62+
const { getMetadata, recognized } = api,
63+
metadata = getMetadata()
64+
65+
if (metadata.duration >= minDuration) {
66+
recognized()
67+
}
68+
}
69+
70+
const mouseleave: RecognizeableEffect<'mouseleave', HoverMetadata> = (event, api) => {
71+
const { denied } = api
72+
73+
if (mouseStatus === 'entered') {
74+
denied()
75+
mouseStatus = 'exited'
76+
}
77+
78+
onLeave?.(toHookApi(api))
79+
}
80+
81+
const touchstart: RecognizeableEffect<'touchstart', HoverMetadata> = event => {
82+
event.preventDefault()
83+
}
84+
85+
return {
86+
mouseenter: {
87+
effect: mouseenter,
88+
stop,
89+
},
90+
mouseleave,
91+
touchstart,
92+
}
93+
}
94+
95+
export class Hover extends Listenable<HoverType, HoverMetadata> {
96+
constructor (options?: HoverOptions) {
97+
super(
98+
'recognizeable' as HoverType,
99+
{
100+
recognizeable: {
101+
effects: createHover(options),
102+
},
103+
}
104+
)
105+
}
106+
107+
get metadata () {
108+
return this.recognizeable.metadata
109+
}
110+
}

src/factories/createMousepress.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type MousepressType = 'mousedown' | 'mouseleave' | 'mouseup'
1212

1313
export type MousepressMetadata = PointerStartMetadata
1414
& PointerMoveMetadata
15-
& PointerTimeMetadata
15+
& PointerTimeMetadata<true>
1616

1717
export type MousepressOptions = {
1818
minDuration?: number,
@@ -58,16 +58,17 @@ export function createMousepress (options: MousepressOptions = {}) {
5858
// @ts-expect-error
5959
mousemoveEffect = event => mousemove(event, api)
6060

61-
storePointerStartMetadata(event, api)
62-
storePointerMoveMetadata(event, api)
63-
storePointerTimeMetadata(
61+
storePointerStartMetadata({ event, api })
62+
storePointerMoveMetadata({ event, api })
63+
storePointerTimeMetadata({
6464
event,
65+
moves: true,
6566
api,
66-
() => mouseStatus === 'down',
67-
newRequest => request = newRequest,
67+
getShouldStore: () => mouseStatus === 'down',
68+
setRequest: newRequest => request = newRequest,
6869
// @ts-expect-error
6970
recognize,
70-
)
71+
})
7172

7273
const { listenInjection: { optionsByType: { mousedown: { target } } } } = api
7374
target.addEventListener('mousemove', mousemoveEffect)
@@ -78,7 +79,7 @@ export function createMousepress (options: MousepressOptions = {}) {
7879
const mousemove: RecognizeableEffect<'mousemove', MousepressMetadata> = (event, api) => {
7980
const { pushSequence } = api
8081
pushSequence(event)
81-
storePointerMoveMetadata(event, api)
82+
storePointerMoveMetadata({ event, api })
8283
// @ts-expect-error
8384
recognize(event, api)
8485

src/factories/createMouserelease.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type MousereleaseType = 'mousedown' | 'mouseleave' | 'mouseup'
1111

1212
export type MousereleaseMetadata = PointerStartMetadata
1313
& PointerMoveMetadata
14-
& PointerTimeMetadata
14+
& PointerTimeMetadata<true>
1515

1616
export type MousereleaseOptions = {
1717
minDuration?: number,
@@ -60,14 +60,15 @@ export function createMouserelease (options: MousereleaseOptions = {}) {
6060
// @ts-expect-error
6161
mousemoveEffect = event => mousemove(event, api)
6262

63-
storePointerStartMetadata(event, api)
64-
storePointerMoveMetadata(event, api)
65-
storePointerTimeMetadata(
63+
storePointerStartMetadata({ event, api })
64+
storePointerMoveMetadata({ event, api })
65+
storePointerTimeMetadata({
6666
event,
67+
moves: true,
6768
api,
68-
() => mouseStatus === 'down',
69-
newRequest => request = newRequest,
70-
)
69+
getShouldStore: () => mouseStatus === 'down',
70+
setRequest: newRequest => request = newRequest,
71+
})
7172

7273
const { listenInjection: { optionsByType: { mousedown: { target } } } } = api
7374
target.addEventListener('mousemove', mousemoveEffect)
@@ -76,7 +77,7 @@ export function createMouserelease (options: MousereleaseOptions = {}) {
7677
}
7778

7879
const mousemove: RecognizeableEffect<'mousemove', MousereleaseMetadata> = (event, api) => {
79-
storePointerMoveMetadata(event, api)
80+
storePointerMoveMetadata({ event, api })
8081

8182
onMove?.(toHookApi(api))
8283
}
@@ -96,7 +97,7 @@ export function createMouserelease (options: MousereleaseOptions = {}) {
9697
const mouseup: RecognizeableEffect<'mouseup', MousereleaseMetadata> = (event, api) => {
9798
if (mouseStatus !== 'down') return
9899

99-
storePointerMoveMetadata(event, api)
100+
storePointerMoveMetadata({ event, api })
100101

101102
const { listenInjection: { optionsByType: { mouseup: { target } } } } = api
102103
stop(target)

src/factories/createTouchpress.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type TouchpressType = 'touchstart' | 'touchmove' | 'touchcancel' | 'touch
1414

1515
export type TouchpressMetadata = PointerStartMetadata
1616
& PointerMoveMetadata
17-
& PointerTimeMetadata
17+
& PointerTimeMetadata<true>
1818

1919
export type TouchpressOptions = {
2020
minDuration?: number,
@@ -62,16 +62,17 @@ export function createTouchpress (options: TouchpressOptions = {}) {
6262
return
6363
}
6464

65-
storePointerStartMetadata(event, api)
66-
storePointerMoveMetadata(event, api)
67-
storePointerTimeMetadata(
65+
storePointerStartMetadata({ event, api })
66+
storePointerMoveMetadata({ event, api })
67+
storePointerTimeMetadata({
6868
event,
69+
moves: true,
6970
api,
70-
() => totalTouches === 1,
71-
newRequest => request = newRequest,
71+
getShouldStore: () => totalTouches === 1,
72+
setRequest: newRequest => request = newRequest,
7273
// @ts-expect-error
7374
recognize,
74-
)
75+
})
7576

7677
onStart?.(toHookApi(api))
7778
}
@@ -80,7 +81,7 @@ export function createTouchpress (options: TouchpressOptions = {}) {
8081
const { getStatus } = api
8182

8283
if (getStatus() !== 'denied') {
83-
storePointerMoveMetadata(event, api)
84+
storePointerMoveMetadata({ event, api })
8485
// @ts-expect-error
8586
recognize(event, api)
8687
}

src/factories/createTouchrelease.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type TouchreleaseType = 'touchstart' | 'touchmove' | 'touchcancel' | 'tou
1616

1717
export type TouchreleaseMetadata = PointerStartMetadata
1818
& PointerMoveMetadata
19-
& PointerTimeMetadata
19+
& PointerTimeMetadata<true>
2020

2121
export type TouchreleaseOptions = {
2222
minDuration?: number,
@@ -67,22 +67,23 @@ export function createTouchrelease (options: TouchreleaseOptions = {}) {
6767
return
6868
}
6969

70-
storePointerStartMetadata(event, api)
71-
storePointerMoveMetadata(event, api)
72-
storePointerTimeMetadata(
70+
storePointerStartMetadata({ event, api })
71+
storePointerMoveMetadata({ event, api })
72+
storePointerTimeMetadata({
7373
event,
74+
moves: true,
7475
api,
75-
() => totalTouches === 1,
76-
newRequest => request = newRequest,
77-
)
76+
getShouldStore: () => totalTouches === 1,
77+
setRequest: newRequest => request = newRequest,
78+
})
7879

7980
onStart?.(toHookApi(api))
8081
}
8182

8283
const touchmove: RecognizeableEffect<'touchmove', TouchreleaseMetadata> = (event, api) => {
8384
const { getStatus } = api
8485

85-
if (getStatus() !== 'denied') storePointerMoveMetadata(event, api)
86+
if (getStatus() !== 'denied') storePointerMoveMetadata({ event, api })
8687

8788
onMove?.(toHookApi(api))
8889
}
@@ -107,7 +108,7 @@ export function createTouchrelease (options: TouchreleaseOptions = {}) {
107108
return
108109
}
109110

110-
storePointerMoveMetadata(event, api)
111+
storePointerMoveMetadata({ event, api })
111112

112113
stop()
113114
totalTouches--

0 commit comments

Comments
 (0)