@rootnative/impulse 0.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -0
  4. package/dist/chunk-5BMRKYVY.js +39 -0
  5. package/dist/chunk-F4RHM4ZK.js +77 -0
  6. package/dist/chunk-FR242SUF.js +174 -0
  7. package/dist/chunk-IG5RXCYR.js +74 -0
  8. package/dist/chunk-LM645QQT.js +37 -0
  9. package/dist/chunk-PMR25UCT.js +8 -0
  10. package/dist/chunk-ZX7WNICB.js +39 -0
  11. package/dist/compose/index.d.ts +80 -0
  12. package/dist/compose/index.js +2 -0
  13. package/dist/drag/index.d.ts +277 -0
  14. package/dist/drag/index.js +4 -0
  15. package/dist/gesture-handler/index.d.ts +1 -0
  16. package/dist/gesture-handler/index.js +1 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +8 -0
  19. package/dist/raw/index.d.ts +63 -0
  20. package/dist/raw/index.js +3 -0
  21. package/dist/tap/index.d.ts +150 -0
  22. package/dist/tap/index.js +4 -0
  23. package/dist/types-Ch2HM3aP.d.ts +142 -0
  24. package/dist/useGestureMemo-Ccv8rB0C.d.ts +38 -0
  25. package/jest-preset.cjs +60 -0
  26. package/jest-setup.cjs +56 -0
  27. package/package.json +115 -0
  28. package/src/compose/index.ts +7 -0
  29. package/src/compose/useGestures.ts +136 -0
  30. package/src/gesture-handler/index.ts +18 -0
  31. package/src/index.ts +53 -0
  32. package/src/intents/drag/index.ts +8 -0
  33. package/src/intents/tap/index.ts +2 -0
  34. package/src/intents/useDrag.ts +563 -0
  35. package/src/intents/useTap.ts +285 -0
  36. package/src/internal/useGestureMemo.ts +110 -0
  37. package/src/internal/useLatestCallback.ts +64 -0
  38. package/src/internal/useStableList.ts +62 -0
  39. package/src/internal/useStableRecord.ts +72 -0
  40. package/src/internal/warnOnce.ts +60 -0
  41. package/src/raw/index.ts +2 -0
  42. package/src/raw/useRawGesture.ts +71 -0
  43. package/src/relations/index.ts +120 -0
  44. package/src/types.ts +187 -0
@@ -0,0 +1,71 @@
1
+ import { type DependencyList } from 'react'
2
+ import { type GestureType } from 'react-native-gesture-handler'
3
+ import {
4
+ useGestureMemo,
5
+ type BuiltGesture,
6
+ type GestureMemoOptions,
7
+ } from '../internal/useGestureMemo'
8
+
9
+ /** Options for {@link useRawGesture}. */
10
+ export type UseRawGestureOptions = GestureMemoOptions
11
+
12
+ /** What {@link useRawGesture} returns. */
13
+ export type RawGestureResult<G extends GestureType> = BuiltGesture<G>
14
+
15
+ /**
16
+ * Build an RNGH gesture by hand, with Impulse's memoisation and coexistence
17
+ * handling applied to it.
18
+ *
19
+ * The mechanism-level escape hatch. Impulse's intent hooks cover the common
20
+ * cases; this covers a recognizer they do not model, or a configuration they
21
+ * do not expose, without giving up the two things that are tedious to get
22
+ * right by hand:
23
+ *
24
+ * ```tsx
25
+ * const fling = useRawGesture(
26
+ * () => Gesture.Fling().direction(Directions.RIGHT).onEnd(onFling),
27
+ * [onFling],
28
+ * { deferTo: scrollRef },
29
+ * )
30
+ *
31
+ * <GestureDetector gesture={fling.gesture}>…</GestureDetector>
32
+ * ```
33
+ *
34
+ * What you still own here, because you are building the gesture yourself:
35
+ *
36
+ * - **The dependency list.** `build` runs only when `deps` change, and a
37
+ * stale capture is a stale gesture. A worklet callback belongs in `deps`,
38
+ * because a worklet is captured as written. A JS-thread callback does not —
39
+ * put it through `useLatestCallback` and depend on the stable result.
40
+ * - **The thread.** RNGH decides per callback, by whether it carries the
41
+ * `'worklet'` directive, and warns in development when a gesture mixes the
42
+ * two. Impulse does not insert a `runOnJS` boundary for you here; that is
43
+ * something the intent hooks do because they know what each callback means.
44
+ * - **The payload.** You get RNGH's flat event, not an intent-shaped one.
45
+ *
46
+ * What Impulse still owns:
47
+ *
48
+ * - Gesture identity across renders, so an inline option cannot re-attach a
49
+ * gesture mid-drag.
50
+ * - `alongside` / `blocks` / `deferTo` resolution, so the three RNGH
51
+ * relations are chosen by outcome rather than by method name.
52
+ * - A `ref` other hooks can name in their own coexistence options.
53
+ *
54
+ * **Accessibility.** Nothing here is reachable by a screen reader or a
55
+ * keyboard, and Impulse cannot name a fallback for a gesture it did not
56
+ * design. Whatever this gesture does must also be doable another way — an
57
+ * `accessibilityActions` entry, or a visible control.
58
+ *
59
+ * @param build - Constructs the gesture. Do not call the relation methods
60
+ * here; pass `alongside` / `blocks` / `deferTo` in `options` instead, so
61
+ * the three-way choice stays in one place and is applied exactly once.
62
+ * @param deps - What the gesture depends on, in `useMemo` terms.
63
+ * @param options - Coexistence options and `testId`.
64
+ */
65
+ export function useRawGesture<G extends GestureType>(
66
+ build: () => G,
67
+ deps: DependencyList,
68
+ options?: UseRawGestureOptions,
69
+ ): RawGestureResult<G> {
70
+ return useGestureMemo('useRawGesture', build, deps, options)
71
+ }
@@ -0,0 +1,120 @@
1
+ import { type GestureType } from 'react-native-gesture-handler'
2
+ import { isDevBuild, warnOnce } from '../internal/warnOnce'
3
+ import { type GestureReference } from '../types'
4
+
5
+ /**
6
+ * `CoexistenceOptions` after normalization: every option is an array, and an
7
+ * option the consumer omitted is an empty one.
8
+ */
9
+ export interface ResolvedCoexistence {
10
+ readonly alongside: readonly GestureReference[]
11
+ readonly blocks: readonly GestureReference[]
12
+ readonly deferTo: readonly GestureReference[]
13
+ }
14
+
15
+ /**
16
+ * The three options, paired with the RNGH method each one means. This list is
17
+ * the mapping — it exists once, in one file, because choosing wrongly between
18
+ * the three is the single thing consumers get wrong most often and a mapping
19
+ * repeated per hook is a mapping that drifts.
20
+ *
21
+ * The direction of each relation is the part worth re-reading:
22
+ *
23
+ * - `alongside` → `simultaneousWithExternalGesture`. Symmetric. Both
24
+ * recognize; neither waits.
25
+ * - `blocks` → `blocksExternalGesture`. *This* gesture wins. The named
26
+ * gesture cannot activate until this one fails.
27
+ * - `deferTo` → `requireExternalGestureToFail`. The *named* gesture wins.
28
+ * This one activates only after that one fails.
29
+ *
30
+ * `blocks` and `deferTo` are the same relation read from opposite ends, which
31
+ * is exactly why RNGH's names for them are so easy to swap by accident.
32
+ */
33
+ const RELATIONS = [
34
+ ['alongside', 'simultaneousWithExternalGesture'],
35
+ ['blocks', 'blocksExternalGesture'],
36
+ ['deferTo', 'requireExternalGestureToFail'],
37
+ ] as const satisfies ReadonlyArray<readonly [keyof ResolvedCoexistence, string]>
38
+
39
+ /**
40
+ * Apply coexistence relations to a freshly built gesture.
41
+ *
42
+ * Call this exactly once per gesture object, at construction. RNGH's relation
43
+ * methods append to the gesture's config rather than replacing it, so calling
44
+ * them twice on one gesture adds the same reference twice. Impulse builds
45
+ * gestures inside a `useMemo` and configures them there, which makes "once
46
+ * per object" structural rather than a rule to remember.
47
+ *
48
+ * An empty option is skipped rather than passed as a zero-argument call, so
49
+ * a gesture with no coexistence options leaves RNGH's config keys absent
50
+ * instead of set to an empty array. The two are equivalent to RNGH, and the
51
+ * absent form is easier to read in a debugger.
52
+ *
53
+ * @param gesture - The gesture to configure. Mutated in place, and returned
54
+ * by nothing: the caller already holds it.
55
+ * @param relations - Normalized options. Use `useStableList` to produce them.
56
+ * @param hookName - The hook to name in a dev warning, e.g. `useRawGesture`.
57
+ */
58
+ export function applyRelations(
59
+ gesture: GestureType,
60
+ relations: ResolvedCoexistence,
61
+ hookName: string,
62
+ ): void {
63
+ warnOnConflictingRelations(relations, hookName)
64
+
65
+ for (const [option, method] of RELATIONS) {
66
+ const references = relations[option]
67
+ if (references.length > 0) {
68
+ gesture[method](...references)
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Warn when one gesture is named by more than one coexistence option.
75
+ *
76
+ * RNGH applies all three relations independently and never complains, so
77
+ * `{ alongside: ref, deferTo: ref }` installs two contradictory rules about
78
+ * the same pair and the result is whatever the platform's recognizer decides.
79
+ * There is no reading of the three names under which that is intentional,
80
+ * which makes it worth reporting rather than resolving silently.
81
+ *
82
+ * The warning key names the option pair rather than the reference, because a
83
+ * gesture reference has no stable string form — and because the message a
84
+ * reader needs is the same either way.
85
+ */
86
+ function warnOnConflictingRelations(
87
+ relations: ResolvedCoexistence,
88
+ hookName: string,
89
+ ): void {
90
+ if (!isDevBuild()) {
91
+ return
92
+ }
93
+
94
+ const namedBy = new Map<GestureReference, string[]>()
95
+ for (const [option] of RELATIONS) {
96
+ for (const reference of relations[option]) {
97
+ const options = namedBy.get(reference)
98
+ if (options) {
99
+ options.push(option)
100
+ } else {
101
+ namedBy.set(reference, [option])
102
+ }
103
+ }
104
+ }
105
+
106
+ for (const options of namedBy.values()) {
107
+ if (options.length > 1) {
108
+ const listed = options.map((option) => `\`${option}\``).join(' and ')
109
+ warnOnce(
110
+ `relations:conflict:${hookName}:${options.join('+')}`,
111
+ `${hookName} names the same gesture in ${listed}. Those are ` +
112
+ 'independent relations, not a choice of one, so both are applied ' +
113
+ 'and they contradict each other. Keep the one that describes the ' +
114
+ 'outcome you want: `alongside` for both recognizing at once, ' +
115
+ '`blocks` for this gesture winning, `deferTo` for the other one ' +
116
+ 'winning.',
117
+ )
118
+ }
119
+ }
120
+ }
package/src/types.ts ADDED
@@ -0,0 +1,187 @@
1
+ import { type ComponentProps, type ComponentType, type RefObject } from 'react'
2
+ import {
3
+ type ComposedGesture,
4
+ type GestureDetector,
5
+ type GestureType,
6
+ } from 'react-native-gesture-handler'
7
+ import { type SharedValue } from 'react-native-reanimated'
8
+
9
+ /**
10
+ * A gesture that an Impulse gesture can be placed in a relation with.
11
+ *
12
+ * The value is either a ref returned by another Impulse hook (`drag.ref`), a
13
+ * ref on a component that owns a gesture — a `ScrollView`, say — or a raw
14
+ * RNGH gesture object.
15
+ *
16
+ * RNGH models this as `GestureRef` and does **not** export it, so the union
17
+ * is written out here. It is RNGH's own minus the numeric form: RNGH accepts
18
+ * a handler tag as a number for its legacy API, and the three relation
19
+ * methods reject it. `AssertRelationArgument` below is what catches an
20
+ * upstream change to the shape.
21
+ */
22
+ export type GestureReference =
23
+ | GestureType
24
+ | RefObject<GestureType | undefined>
25
+ | RefObject<ComponentType | undefined | null>
26
+
27
+ /**
28
+ * Compile-time proof that `GestureReference` is exactly what RNGH's relation
29
+ * methods accept — assignable in both directions, so neither too wide nor too
30
+ * narrow. Nothing reads these types; they exist to fail the build if RNGH
31
+ * changes `GestureRef` under us, which a hand-copied union would otherwise
32
+ * absorb silently. They are type aliases rather than a checked `const` so the
33
+ * guard emits no runtime value and cannot defeat tree-shaking.
34
+ */
35
+ type RelationArgument = Parameters<
36
+ GestureType['simultaneousWithExternalGesture']
37
+ >[number]
38
+ type Assert<T extends true> = T
39
+ type _AssertReferenceIsAccepted = Assert<
40
+ GestureReference extends RelationArgument ? true : false
41
+ >
42
+ type _AssertReferenceIsComplete = Assert<
43
+ RelationArgument extends GestureReference ? true : false
44
+ >
45
+
46
+ /**
47
+ * A gesture that can be handed to `<GestureDetector>`: a single recognizer,
48
+ * or a composition of them.
49
+ *
50
+ * RNGH spells this inline in `GestureDetector`'s props and exports no name
51
+ * for it, so Impulse names it — every hook result's `gesture` has this type,
52
+ * and so does every member `useGestures` accepts.
53
+ */
54
+ export type AttachableGesture = GestureType | ComposedGesture
55
+
56
+ /**
57
+ * Compile-time proof that `AttachableGesture` is exactly what
58
+ * `<GestureDetector>` accepts. Same purpose as the assertions above: RNGH
59
+ * does not export the type, so this one is written out, and a hand-copied
60
+ * type is one that absorbs an upstream change without a word.
61
+ */
62
+ type DetectorGesture = ComponentProps<typeof GestureDetector>['gesture']
63
+ type _AssertAttachableIsAccepted = Assert<
64
+ AttachableGesture extends DetectorGesture ? true : false
65
+ >
66
+ type _AssertAttachableIsComplete = Assert<
67
+ DetectorGesture extends AttachableGesture ? true : false
68
+ >
69
+
70
+ /**
71
+ * Extra touchable area around a view, in points. A number widens every edge;
72
+ * an object widens the edges it names.
73
+ *
74
+ * Derived from RNGH's own method signature rather than copied, for the same
75
+ * reason `GestureReference` is: RNGH does not export the type from its package
76
+ * entry, and a hand-written copy is one that absorbs an upstream change
77
+ * without a word.
78
+ */
79
+ export type HitSlop = Parameters<GestureType['hitSlop']>[0]
80
+
81
+ /** One reference, or several. Every coexistence option accepts both. */
82
+ export type GestureReferences = GestureReference | GestureReference[]
83
+
84
+ /**
85
+ * How an Impulse gesture coexists with a gesture it does not own — most often
86
+ * a scroll view it lives inside.
87
+ *
88
+ * RNGH exposes this as three methods whose names describe the mechanism
89
+ * (`simultaneousWithExternalGesture`, `blocksExternalGesture`,
90
+ * `requireExternalGestureToFail`) and give no hint about which one a given
91
+ * case wants. These three name the outcome instead. Each maps to exactly one
92
+ * RNGH relation, and they are not interchangeable — picking the wrong one is
93
+ * the single thing consumers get wrong most often.
94
+ *
95
+ * All three may be set at once. They are independent relations, not a choice
96
+ * of one.
97
+ */
98
+ export interface CoexistenceOptions {
99
+ /**
100
+ * Both gestures recognize at the same time. Neither waits for the other.
101
+ * Maps to `simultaneousWithExternalGesture`.
102
+ *
103
+ * Use it when the two gestures read different things from the same touch —
104
+ * a pinch and a pan on one image, say.
105
+ */
106
+ alongside?: GestureReferences
107
+ /**
108
+ * This gesture wins. The named gesture cannot activate until this one has
109
+ * failed. Maps to `blocksExternalGesture`.
110
+ *
111
+ * Use it when this gesture is the foreground affordance — a bottom sheet
112
+ * that must take the drag before the list behind it does.
113
+ */
114
+ blocks?: GestureReferences
115
+ /**
116
+ * The named gesture wins. This one activates only after that one fails.
117
+ * Maps to `requireExternalGestureToFail`.
118
+ *
119
+ * Use it when this gesture is the fallback — a horizontal drag that should
120
+ * start only once the vertical scroll has declined the touch.
121
+ */
122
+ deferTo?: GestureReferences
123
+ }
124
+
125
+ /**
126
+ * How the members of a `useGestures` composition relate to one another.
127
+ *
128
+ * - `race` — the first to activate wins, and the rest are cancelled.
129
+ * - `simultaneous` — every member recognizes independently.
130
+ * - `exclusive` — members are tried in order, and a later one activates only
131
+ * after every earlier one has failed.
132
+ *
133
+ * Composition is a flat call carrying this mode rather than a nested builder
134
+ * chain, so precedence reads left to right instead of inside out.
135
+ */
136
+ export type ComposeMode = 'race' | 'simultaneous' | 'exclusive'
137
+
138
+ /**
139
+ * A point in a gesture payload, in points.
140
+ *
141
+ * Grouped rather than spelled as two flat fields, because every payload that
142
+ * carries more than one point — a drag's position and its origin, a pinch's
143
+ * focal point — would otherwise need a prefix per pair and the consumer would
144
+ * pick between `absoluteX` and `focalX` from memory. That flat union is
145
+ * exactly what the intent payloads exist to replace.
146
+ */
147
+ export interface Point {
148
+ readonly x: number
149
+ readonly y: number
150
+ }
151
+
152
+ /**
153
+ * What every intent hook returns: the gesture, a handle other hooks can name
154
+ * in their coexistence options, and whether the gesture is being recognized
155
+ * right now.
156
+ *
157
+ * Each hook extends this with the shared values its own intent produces —
158
+ * `drag.x`, `pinch.scale`, `rotate.angle`. The three members here are the
159
+ * part that is the same whatever the intent.
160
+ */
161
+ export interface IntentResult<G extends GestureType> {
162
+ /** The configured gesture. Hand it to `<GestureDetector>`. */
163
+ readonly gesture: G
164
+ /**
165
+ * A handle on this gesture for another hook's `alongside` / `blocks` /
166
+ * `deferTo`.
167
+ *
168
+ * Prefer it over passing `other.gesture`: a gesture object is replaced when
169
+ * its dependencies change, and this ref is created once and read by RNGH at
170
+ * the moment it resolves relations, so a relation written against it keeps
171
+ * pointing at the live gesture.
172
+ *
173
+ * It is populated when `<GestureDetector>` mounts the gesture, not when the
174
+ * hook runs, so reading `.current` during the first render gives
175
+ * `undefined`.
176
+ */
177
+ readonly ref: RefObject<GestureType | undefined>
178
+ /**
179
+ * `true` while the gesture is active — for a tap, while the finger is down;
180
+ * for a drag, while it is being dragged.
181
+ *
182
+ * A shared value, so a pressed or grabbed state can be driven on the UI
183
+ * thread through `useAnimatedStyle` without a re-render. Reading `.value`
184
+ * during render works but tells you only what was true at the last commit.
185
+ */
186
+ readonly isActive: SharedValue<boolean>
187
+ }