@anyknown/axis 0.1.0 → 0.2.1

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 (141) hide show
  1. package/README.md +1 -0
  2. package/dist/axis/constants.d.ts +12 -0
  3. package/dist/axis/constants.js +27 -0
  4. package/dist/axis/create-axis.d.ts +10 -0
  5. package/dist/axis/create-axis.js +212 -0
  6. package/dist/axis/create-drag-client.d.ts +6 -0
  7. package/dist/axis/create-drag-client.js +95 -0
  8. package/dist/axis/create-geometry.d.ts +2 -0
  9. package/dist/axis/create-geometry.js +11 -0
  10. package/dist/axis/create-hold.d.ts +2 -0
  11. package/dist/axis/create-hold.js +38 -0
  12. package/dist/axis/create-scroll-driver.d.ts +2 -0
  13. package/dist/axis/create-scroll-driver.js +139 -0
  14. package/dist/axis/create-snap-suspension.d.ts +2 -0
  15. package/dist/axis/create-snap-suspension.js +23 -0
  16. package/dist/axis/create-style-overrides.d.ts +2 -0
  17. package/dist/axis/create-style-overrides.js +35 -0
  18. package/dist/axis/create-wheel.d.ts +5 -0
  19. package/dist/axis/create-wheel.js +57 -0
  20. package/dist/axis/drags-along.d.ts +2 -0
  21. package/dist/axis/drags-along.js +1 -0
  22. package/dist/axis/index.d.ts +2 -0
  23. package/dist/axis/index.js +1 -0
  24. package/dist/axis/merge-options.d.ts +2 -0
  25. package/dist/axis/merge-options.js +9 -0
  26. package/dist/axis/next-axis-state.d.ts +2 -0
  27. package/dist/axis/next-axis-state.js +14 -0
  28. package/dist/axis/resolve-axis.d.ts +2 -0
  29. package/dist/axis/resolve-axis.js +8 -0
  30. package/dist/{axis.d.ts → axis/types.d.ts} +57 -9
  31. package/dist/axis/types.js +1 -0
  32. package/dist/coordinator/constants.d.ts +5 -0
  33. package/dist/coordinator/constants.js +5 -0
  34. package/dist/coordinator/create-coordinator.d.ts +2 -0
  35. package/dist/coordinator/create-coordinator.js +191 -0
  36. package/dist/coordinator/index.d.ts +2 -0
  37. package/dist/coordinator/index.js +1 -0
  38. package/dist/coordinator/pick-owner.d.ts +3 -0
  39. package/dist/coordinator/pick-owner.js +7 -0
  40. package/dist/coordinator/register.d.ts +2 -0
  41. package/dist/coordinator/register.js +11 -0
  42. package/dist/coordinator/types.d.ts +24 -0
  43. package/dist/coordinator/types.js +1 -0
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +1 -1
  46. package/dist/react/constants.d.ts +3 -0
  47. package/dist/react/constants.js +8 -0
  48. package/dist/react/index.d.ts +3 -0
  49. package/dist/react/index.js +1 -0
  50. package/dist/react/types.d.ts +12 -0
  51. package/dist/react/types.js +1 -0
  52. package/dist/react/use-applied-options.d.ts +3 -0
  53. package/dist/react/use-applied-options.js +11 -0
  54. package/dist/react/use-axis-instance.d.ts +3 -0
  55. package/dist/react/use-axis-instance.js +19 -0
  56. package/dist/react/use-axis.d.ts +11 -0
  57. package/dist/react/use-axis.js +14 -0
  58. package/dist/react/use-isomorphic-layout-effect.d.ts +2 -0
  59. package/dist/react/use-isomorphic-layout-effect.js +2 -0
  60. package/dist/react/use-selected-state.d.ts +4 -0
  61. package/dist/react/use-selected-state.js +14 -0
  62. package/dist/react/use-stable-api.d.ts +4 -0
  63. package/dist/react/use-stable-api.js +7 -0
  64. package/dist/utils/adjacent-snap.d.ts +1 -0
  65. package/dist/utils/adjacent-snap.js +8 -0
  66. package/dist/utils/cap-velocity.d.ts +2 -0
  67. package/dist/utils/cap-velocity.js +6 -0
  68. package/dist/utils/clamp.d.ts +1 -0
  69. package/dist/utils/clamp.js +1 -0
  70. package/dist/utils/composed-path-matches.d.ts +1 -0
  71. package/dist/utils/composed-path-matches.js +9 -0
  72. package/dist/utils/constants.d.ts +9 -0
  73. package/dist/utils/constants.js +19 -0
  74. package/dist/utils/css-length-to-px.d.ts +1 -0
  75. package/dist/utils/css-length-to-px.js +6 -0
  76. package/dist/utils/dominant-axis.d.ts +2 -0
  77. package/dist/utils/dominant-axis.js +3 -0
  78. package/dist/utils/ease-out-cubic.d.ts +1 -0
  79. package/dist/utils/ease-out-cubic.js +1 -0
  80. package/dist/utils/glide-duration.d.ts +1 -0
  81. package/dist/utils/glide-duration.js +9 -0
  82. package/dist/utils/index.d.ts +28 -0
  83. package/dist/utils/index.js +27 -0
  84. package/dist/utils/is-inside-editable.d.ts +1 -0
  85. package/dist/utils/is-inside-editable.js +2 -0
  86. package/dist/utils/is-on-scrollbar.d.ts +1 -0
  87. package/dist/utils/is-on-scrollbar.js +6 -0
  88. package/dist/utils/is-rtl.d.ts +1 -0
  89. package/dist/utils/is-rtl.js +1 -0
  90. package/dist/utils/is-scroll-key.d.ts +1 -0
  91. package/dist/utils/is-scroll-key.js +7 -0
  92. package/dist/utils/max-scroll.d.ts +2 -0
  93. package/dist/utils/max-scroll.js +4 -0
  94. package/dist/utils/measure-snaps.d.ts +1 -0
  95. package/dist/utils/measure-snaps.js +31 -0
  96. package/dist/utils/momentum-distance.d.ts +1 -0
  97. package/dist/utils/momentum-distance.js +4 -0
  98. package/dist/utils/momentum-step.d.ts +4 -0
  99. package/dist/utils/momentum-step.js +4 -0
  100. package/dist/utils/nearest-snap.d.ts +1 -0
  101. package/dist/utils/nearest-snap.js +12 -0
  102. package/dist/utils/parse-snap-align.d.ts +5 -0
  103. package/dist/utils/parse-snap-align.js +5 -0
  104. package/dist/utils/parse-snap-type.d.ts +4 -0
  105. package/dist/utils/parse-snap-type.js +7 -0
  106. package/dist/utils/pointer-sample.d.ts +2 -0
  107. package/dist/utils/pointer-sample.js +5 -0
  108. package/dist/utils/prefers-reduced-motion.d.ts +1 -0
  109. package/dist/utils/prefers-reduced-motion.js +1 -0
  110. package/dist/utils/read-scroll.d.ts +2 -0
  111. package/dist/utils/read-scroll.js +6 -0
  112. package/dist/utils/snap-items.d.ts +2 -0
  113. package/dist/utils/snap-items.js +18 -0
  114. package/dist/utils/snap-positions.d.ts +2 -0
  115. package/dist/utils/snap-positions.js +28 -0
  116. package/dist/utils/supported-user-select.d.ts +1 -0
  117. package/dist/utils/supported-user-select.js +1 -0
  118. package/dist/utils/to-logical.d.ts +1 -0
  119. package/dist/utils/to-logical.js +3 -0
  120. package/dist/utils/to-physical.d.ts +1 -0
  121. package/dist/utils/to-physical.js +3 -0
  122. package/dist/utils/try-capture-pointer.d.ts +1 -0
  123. package/dist/utils/try-capture-pointer.js +6 -0
  124. package/dist/utils/try-release-pointer.d.ts +1 -0
  125. package/dist/utils/try-release-pointer.js +7 -0
  126. package/dist/utils/types.d.ts +22 -0
  127. package/dist/utils/types.js +1 -0
  128. package/dist/utils/velocity-from-samples.d.ts +2 -0
  129. package/dist/utils/velocity-from-samples.js +21 -0
  130. package/dist/utils/wheel-delta-y-px.d.ts +1 -0
  131. package/dist/utils/wheel-delta-y-px.js +5 -0
  132. package/dist/utils/write-scroll.d.ts +2 -0
  133. package/dist/utils/write-scroll.js +7 -0
  134. package/package.json +6 -5
  135. package/dist/axis.js +0 -655
  136. package/dist/coordinator.d.ts +0 -36
  137. package/dist/coordinator.js +0 -247
  138. package/dist/math.d.ts +0 -90
  139. package/dist/math.js +0 -139
  140. package/dist/react.d.ts +0 -22
  141. package/dist/react.js +0 -49
@@ -0,0 +1,57 @@
1
+ import { adjacentSnap, clamp, nearestSnap, wheelDeltaYPx } from "../utils/index.js";
2
+ import { EDGE, ZERO } from "./constants.js";
3
+ export function createWheel(core, snap, driver) {
4
+ const { el, rtl, scrollRange, position, writePosition, options, motion, setMotion, isDestroyed } = core;
5
+ let direction = 1;
6
+ let attached = false;
7
+ const settleOnNextSnapInWheelDirection = () => {
8
+ if (isDestroyed())
9
+ return;
10
+ if (!snap.snapAxes().x) {
11
+ driver.settle();
12
+ return;
13
+ }
14
+ const current = position().x;
15
+ const positions = core.snapPoints().x;
16
+ const target = adjacentSnap(positions, current - 2 * direction, direction) ?? nearestSnap(positions, current);
17
+ driver.glideTo({ x: target }, ZERO, "glide");
18
+ };
19
+ const onWheel = (event) => {
20
+ if (options().axis !== "x" || event.ctrlKey || event.shiftKey)
21
+ return;
22
+ if (Math.abs(event.deltaY) <= Math.abs(event.deltaX))
23
+ return;
24
+ if (motion() === "pressed" || motion() === "dragging")
25
+ return;
26
+ const delta = wheelDeltaYPx(event, el.clientWidth);
27
+ const isRtlNow = rtl();
28
+ const max = scrollRange();
29
+ const current = position(isRtlNow, max).x;
30
+ const leavesPageScrollingAtEdge = delta > 0 ? current >= max.x - EDGE : current <= EDGE;
31
+ if (leavesPageScrollingAtEdge)
32
+ return;
33
+ event.preventDefault();
34
+ driver.stop();
35
+ snap.suspend();
36
+ direction = delta > 0 ? 1 : -1;
37
+ writePosition({ x: clamp(current + delta, 0, max.x) }, isRtlNow);
38
+ if (motion() !== "wheel")
39
+ setMotion("wheel");
40
+ if (isDestroyed())
41
+ return;
42
+ driver.afterWheelPause(settleOnNextSnapInWheelDirection);
43
+ };
44
+ const sync = () => {
45
+ const { wheel, axis } = options();
46
+ const wanted = wheel && axis === "x";
47
+ if (wanted === attached)
48
+ return;
49
+ attached = wanted;
50
+ if (wanted)
51
+ el.addEventListener("wheel", onWheel, { passive: false });
52
+ else
53
+ el.removeEventListener("wheel", onWheel);
54
+ };
55
+ const remove = () => el.removeEventListener("wheel", onWheel);
56
+ return { sync, remove };
57
+ }
@@ -0,0 +1,2 @@
1
+ import type { AxisMode, AxisName } from "./types.js";
2
+ export declare const dragsAlong: (mode: AxisMode, axis: AxisName) => boolean;
@@ -0,0 +1 @@
1
+ export const dragsAlong = (mode, axis) => mode === "both" || mode === axis;
@@ -0,0 +1,2 @@
1
+ export { createAxis } from "./create-axis.js";
2
+ export type { AxisInstance, AxisMode, AxisName, AxisOptions, AxisPosition, AxisScrollState, AxisScrollToOptions, AxisState, } from "./types.js";
@@ -0,0 +1 @@
1
+ export { createAxis } from "./create-axis.js";
@@ -0,0 +1,2 @@
1
+ import type { AxisOptions } from "./types.js";
2
+ export declare function mergeOptions(base: Required<AxisOptions>, patch: AxisOptions): Required<AxisOptions>;
@@ -0,0 +1,9 @@
1
+ import { DEFAULTS } from "./constants.js";
2
+ export function mergeOptions(base, patch) {
3
+ const next = { ...base };
4
+ for (const key of Object.keys(DEFAULTS)) {
5
+ if (patch[key] !== undefined)
6
+ next[key] = patch[key];
7
+ }
8
+ return next;
9
+ }
@@ -0,0 +1,2 @@
1
+ import type { AxisState } from "./types.js";
2
+ export declare function nextAxisState(scroll: number, max: number, previous: AxisState): AxisState;
@@ -0,0 +1,14 @@
1
+ import { EDGE } from "./constants.js";
2
+ export function nextAxisState(scroll, max, previous) {
3
+ const next = {
4
+ scroll,
5
+ maxScroll: max,
6
+ progress: max > 0 ? scroll / max : 0,
7
+ canScrollPrev: scroll > EDGE,
8
+ canScrollNext: scroll < max - EDGE,
9
+ };
10
+ for (const key of Object.keys(next))
11
+ if (next[key] !== previous[key])
12
+ return next;
13
+ return previous;
14
+ }
@@ -0,0 +1,2 @@
1
+ import type { AxisMode, AxisName } from "./types.js";
2
+ export declare function resolveAxis(method: string, axis: AxisName | undefined, mode: AxisMode): AxisName;
@@ -0,0 +1,8 @@
1
+ export function resolveAxis(method, axis, mode) {
2
+ if (axis)
3
+ return axis;
4
+ if (mode === "both") {
5
+ throw new Error(`axis: ${method}() needs an axis ("x" or "y") when the instance uses axis: "both"`);
6
+ }
7
+ return mode;
8
+ }
@@ -1,3 +1,4 @@
1
+ import type { Vector } from "../utils/index.js";
1
2
  /** The direction(s) a mouse drag scrolls: horizontal, vertical, or free 2D panning. */
2
3
  export type AxisMode = "x" | "y" | "both";
3
4
  /** A single scroll axis. */
@@ -15,6 +16,12 @@ export interface AxisOptions {
15
16
  * @default true
16
17
  */
17
18
  momentum?: boolean;
19
+ /**
20
+ * The farthest a momentum glide may travel after one release, in px. A faster fling is slowed down to land
21
+ * within it, so one gesture never moves more than this.
22
+ * @default Infinity
23
+ */
24
+ maxGlide?: number;
18
25
  /**
19
26
  * Translate vertical mouse-wheel scrolling into horizontal scrolling. Only used with `axis: "x"`; at either
20
27
  * end the wheel is left alone, so the page keeps scrolling.
@@ -87,12 +94,53 @@ export interface AxisInstance {
87
94
  /** Removes every listener and observer and restores every inline style that axis changed. */
88
95
  destroy(): void;
89
96
  }
90
- /**
91
- * Enhances a scroll container (`overflow: auto`) with mouse drag-scrolling, momentum, snap-aware release,
92
- * optional wheel translation and an observable scroll state.
93
- *
94
- * Native scrolling stays the source of truth: touch, trackpad, keyboard and scrollbar input are untouched,
95
- * and only a primary-button mouse drag is taken over (by writing `scrollLeft` / `scrollTop`). Nested instances
96
- * share the drag: the innermost instance that can scroll in the drag's direction takes it.
97
- */
98
- export declare function createAxis(el: HTMLElement, options?: AxisOptions): AxisInstance;
97
+ export type Motion = "idle" | "pressed" | "dragging" | "momentum" | "glide" | "wheel";
98
+ export type Glide = "momentum" | "glide";
99
+ export interface Geometry {
100
+ rtl(): boolean;
101
+ scrollRange(): Vector;
102
+ position(rtl?: boolean, range?: Vector): Vector;
103
+ writePosition(position: AxisPosition, rtl?: boolean): void;
104
+ }
105
+ export interface AxisCore extends Geometry {
106
+ el: HTMLElement;
107
+ win: Window;
108
+ options(): Required<AxisOptions>;
109
+ motion(): Motion;
110
+ setMotion(next: Motion): void;
111
+ isDestroyed(): boolean;
112
+ snapPoints(): Record<AxisName, number[]>;
113
+ }
114
+ export interface StyleOverrides {
115
+ override(prop: string, value: string, important?: boolean): void;
116
+ restore(prop: string): void;
117
+ restoreAll(): void;
118
+ }
119
+ export interface SnapSuspension {
120
+ suspend(): void;
121
+ resume(): void;
122
+ snapAxes(): {
123
+ x: boolean;
124
+ y: boolean;
125
+ };
126
+ isSuspended(): boolean;
127
+ }
128
+ export interface Hold {
129
+ holdCurrent(): void;
130
+ release(): void;
131
+ reset(): void;
132
+ yieldDrag(glideHoldsAtItsEnd: boolean): void;
133
+ holdIfYielded(): void;
134
+ follow(axis: AxisName): void;
135
+ undoAutoscroll(): void;
136
+ }
137
+ export interface ScrollDriver {
138
+ stop(): void;
139
+ settle(): void;
140
+ interrupt(): void;
141
+ jump(target: AxisPosition): void;
142
+ glideTo(target: AxisPosition, velocity: Vector, kind: Glide): void;
143
+ fling(scrollVelocity: Vector): void;
144
+ afterWheelPause(settle: () => void): void;
145
+ glideTarget(): Vector;
146
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { Coordinator } from "./types.js";
2
+ export declare const DRAG_THRESHOLD = 5;
3
+ export declare const CLICK_SUPPRESS_MS = 100;
4
+ export declare const NO_DRAG = "input, textarea, select, [contenteditable]:not([contenteditable='false']), [data-axis-no-drag]";
5
+ export declare const coordinators: WeakMap<Window, Coordinator>;
@@ -0,0 +1,5 @@
1
+ import { EDITABLE } from "../utils/index.js";
2
+ export const DRAG_THRESHOLD = 5;
3
+ export const CLICK_SUPPRESS_MS = 100;
4
+ export const NO_DRAG = `${EDITABLE}, [data-axis-no-drag]`;
5
+ export const coordinators = new WeakMap();
@@ -0,0 +1,2 @@
1
+ import type { Coordinator } from "./types.js";
2
+ export declare function createCoordinator(win: Window): Coordinator;
@@ -0,0 +1,191 @@
1
+ import { composedPathMatches, isScrollKey, pointerSample, VELOCITY_WINDOW, velocityFromSamples, } from "../utils/index.js";
2
+ import { CLICK_SUPPRESS_MS, coordinators, DRAG_THRESHOLD, NO_DRAG } from "./constants.js";
3
+ import { pickOwner } from "./pick-owner.js";
4
+ export function createCoordinator(win) {
5
+ const clients = new Map();
6
+ let session = null;
7
+ let suppressClick = false;
8
+ let suppressTimer = 0;
9
+ const capture = { capture: true };
10
+ const passiveCapture = { capture: true, passive: true };
11
+ const stillRegistered = (client) => clients.get(client.el) === client;
12
+ const forgetSuppressedClick = () => {
13
+ suppressClick = false;
14
+ win.clearTimeout(suppressTimer);
15
+ };
16
+ const detach = () => {
17
+ win.removeEventListener("pointermove", onPointerMove);
18
+ win.removeEventListener("pointerup", onPointerUp);
19
+ win.removeEventListener("pointercancel", onPointerCancel);
20
+ win.removeEventListener("wheel", releaseHoldsOnUserScroll, passiveCapture);
21
+ win.removeEventListener("keydown", releaseHoldsOnUserScroll, capture);
22
+ win.removeEventListener("contextmenu", cancelPress, capture);
23
+ win.removeEventListener("blur", cancelPress);
24
+ win.document.removeEventListener("visibilitychange", cancelPressWhenHidden);
25
+ session = null;
26
+ };
27
+ const endSession = (ended, pointerVelocity) => {
28
+ detach();
29
+ if (ended.owner && stillRegistered(ended.owner))
30
+ ended.owner.end(pointerVelocity);
31
+ for (const client of ended.chain) {
32
+ if (client !== ended.owner && stillRegistered(client))
33
+ client.pass();
34
+ }
35
+ };
36
+ const cancelPress = () => {
37
+ if (session)
38
+ endSession(session, null);
39
+ };
40
+ const cancelPressWhenHidden = () => {
41
+ if (win.document.visibilityState === "hidden")
42
+ cancelPress();
43
+ };
44
+ const releaseHoldsOnUserScroll = (event) => {
45
+ if (event.type === "keydown" && !isScrollKey(event))
46
+ return;
47
+ if (session)
48
+ for (const client of session.chain)
49
+ client.interrupt();
50
+ };
51
+ const attach = () => {
52
+ win.addEventListener("pointermove", onPointerMove);
53
+ win.addEventListener("pointerup", onPointerUp);
54
+ win.addEventListener("pointercancel", onPointerCancel);
55
+ win.addEventListener("wheel", releaseHoldsOnUserScroll, passiveCapture);
56
+ win.addEventListener("keydown", releaseHoldsOnUserScroll, capture);
57
+ win.addEventListener("contextmenu", cancelPress, capture);
58
+ win.addEventListener("blur", cancelPress);
59
+ win.document.addEventListener("visibilitychange", cancelPressWhenHidden);
60
+ };
61
+ const onPointerDown = (event) => {
62
+ forgetSuppressedClick();
63
+ if (session)
64
+ return;
65
+ const path = event.composedPath();
66
+ const chain = [];
67
+ for (const node of path) {
68
+ const client = clients.get(node);
69
+ if (client)
70
+ chain.push(client);
71
+ }
72
+ if (chain.length === 0)
73
+ return;
74
+ const draggable = event.pointerType === "mouse" &&
75
+ event.button === 0 &&
76
+ !composedPathMatches(path, NO_DRAG, chain.at(-1).el) &&
77
+ !chain.some((client) => client.onScrollbar(event));
78
+ if (!draggable) {
79
+ for (const client of chain)
80
+ client.interrupt();
81
+ return;
82
+ }
83
+ for (const client of chain)
84
+ client.press();
85
+ const pressed = chain.filter(stillRegistered);
86
+ if (pressed.length === 0)
87
+ return;
88
+ session = {
89
+ pointerId: event.pointerId,
90
+ start: { x: event.clientX, y: event.clientY },
91
+ chain: pressed,
92
+ owner: null,
93
+ samples: [pointerSample(event)],
94
+ };
95
+ attach();
96
+ };
97
+ const handOver = (current, delta) => {
98
+ const owner = pickOwner(current.chain, delta);
99
+ if (!owner) {
100
+ endSession(current, null);
101
+ return;
102
+ }
103
+ current.owner = owner;
104
+ for (const client of current.chain)
105
+ if (client !== owner)
106
+ client.yield();
107
+ owner.start(current.pointerId);
108
+ };
109
+ const onPointerMove = (event) => {
110
+ const current = session;
111
+ if (!current || event.pointerId !== current.pointerId)
112
+ return;
113
+ const releasedOutsideWindow = !(event.buttons & 1);
114
+ if (releasedOutsideWindow) {
115
+ cancelPress();
116
+ return;
117
+ }
118
+ const { samples } = current;
119
+ samples.push(pointerSample(event));
120
+ while (samples.length > 2 && event.timeStamp - samples[0].t > VELOCITY_WINDOW * 2)
121
+ samples.shift();
122
+ const delta = { x: event.clientX - current.start.x, y: event.clientY - current.start.y };
123
+ if (!current.owner) {
124
+ if (Math.hypot(delta.x, delta.y) <= DRAG_THRESHOLD)
125
+ return;
126
+ handOver(current, delta);
127
+ }
128
+ const endedByListener = session !== current;
129
+ if (endedByListener || !current.owner)
130
+ return;
131
+ current.owner.move(delta);
132
+ };
133
+ const onPointerUp = (event) => {
134
+ const current = session;
135
+ if (!current || event.pointerId !== current.pointerId)
136
+ return;
137
+ current.samples.push(pointerSample(event));
138
+ if (current.owner) {
139
+ suppressClick = true;
140
+ win.clearTimeout(suppressTimer);
141
+ suppressTimer = win.setTimeout(() => (suppressClick = false), CLICK_SUPPRESS_MS);
142
+ }
143
+ endSession(current, current.owner ? velocityFromSamples(current.samples) : null);
144
+ };
145
+ const onPointerCancel = (event) => {
146
+ if (session && event.pointerId === session.pointerId)
147
+ cancelPress();
148
+ };
149
+ const swallowClickAfterDrag = (event) => {
150
+ if (!suppressClick)
151
+ return;
152
+ forgetSuppressedClick();
153
+ event.preventDefault();
154
+ event.stopPropagation();
155
+ };
156
+ const preventNativeDragAndDrop = (event) => {
157
+ if (session)
158
+ event.preventDefault();
159
+ };
160
+ const remove = (client) => {
161
+ if (clients.get(client.el) !== client)
162
+ return;
163
+ clients.delete(client.el);
164
+ if (session) {
165
+ const current = session;
166
+ current.chain = current.chain.filter((other) => other !== client);
167
+ if (current.owner === client || current.chain.length === 0) {
168
+ current.owner = null;
169
+ endSession(current, null);
170
+ }
171
+ }
172
+ if (clients.size === 0) {
173
+ win.removeEventListener("pointerdown", onPointerDown, capture);
174
+ win.removeEventListener("click", swallowClickAfterDrag, capture);
175
+ win.removeEventListener("dragstart", preventNativeDragAndDrop, capture);
176
+ forgetSuppressedClick();
177
+ coordinators.delete(win);
178
+ }
179
+ };
180
+ return {
181
+ add(client) {
182
+ if (clients.size === 0) {
183
+ win.addEventListener("pointerdown", onPointerDown, capture);
184
+ win.addEventListener("click", swallowClickAfterDrag, capture);
185
+ win.addEventListener("dragstart", preventNativeDragAndDrop, capture);
186
+ }
187
+ clients.set(client.el, client);
188
+ return () => remove(client);
189
+ },
190
+ };
191
+ }
@@ -0,0 +1,2 @@
1
+ export { register } from "./register.js";
2
+ export type { DragClient } from "./types.js";
@@ -0,0 +1 @@
1
+ export { register } from "./register.js";
@@ -0,0 +1,3 @@
1
+ import { type Vector } from "../utils/index.js";
2
+ import type { DragClient } from "./types.js";
3
+ export declare function pickOwner(innermostFirst: readonly DragClient[], delta: Vector): DragClient | null;
@@ -0,0 +1,7 @@
1
+ import { dominantAxis } from "../utils/index.js";
2
+ export function pickOwner(innermostFirst, delta) {
3
+ const axis = dominantAxis(delta);
4
+ const dragsAlongAxis = innermostFirst.filter((client) => client.accepts(axis));
5
+ const canStillScroll = dragsAlongAxis.find((client) => client.canScroll(delta));
6
+ return canStillScroll ?? dragsAlongAxis[0] ?? null;
7
+ }
@@ -0,0 +1,2 @@
1
+ import type { DragClient } from "./types.js";
2
+ export declare function register(client: DragClient): () => void;
@@ -0,0 +1,11 @@
1
+ import { coordinators } from "./constants.js";
2
+ import { createCoordinator } from "./create-coordinator.js";
3
+ export function register(client) {
4
+ const win = client.el.ownerDocument.defaultView ?? window;
5
+ let coordinator = coordinators.get(win);
6
+ if (!coordinator) {
7
+ coordinator = createCoordinator(win);
8
+ coordinators.set(win, coordinator);
9
+ }
10
+ return coordinator.add(client);
11
+ }
@@ -0,0 +1,24 @@
1
+ import type { Sample, Vector } from "../utils/index.js";
2
+ export interface DragClient {
3
+ readonly el: HTMLElement;
4
+ onScrollbar(event: PointerEvent): boolean;
5
+ accepts(axis: "x" | "y"): boolean;
6
+ canScroll(delta: Vector): boolean;
7
+ press(): void;
8
+ yield(): void;
9
+ start(pointerId: number): void;
10
+ move(delta: Vector): void;
11
+ pass(): void;
12
+ end(pointerVelocity: Vector | null): void;
13
+ interrupt(): void;
14
+ }
15
+ export interface Session {
16
+ pointerId: number;
17
+ start: Vector;
18
+ chain: DragClient[];
19
+ owner: DragClient | null;
20
+ samples: Sample[];
21
+ }
22
+ export interface Coordinator {
23
+ add(client: DragClient): () => void;
24
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createAxis } from "./axis.js";
2
- export type { AxisInstance, AxisMode, AxisName, AxisOptions, AxisPosition, AxisScrollState, AxisScrollToOptions, AxisState, } from "./axis.js";
1
+ export { createAxis } from "./axis/index.js";
2
+ export type { AxisInstance, AxisMode, AxisName, AxisOptions, AxisPosition, AxisScrollState, AxisScrollToOptions, AxisState, } from "./axis/index.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { createAxis } from "./axis.js";
1
+ export { createAxis } from "./axis/index.js";
@@ -0,0 +1,3 @@
1
+ import type { AxisScrollState } from "../axis/index.js";
2
+ export declare const EMPTY_STATE: AxisScrollState;
3
+ export declare const NOOP: () => void;
@@ -0,0 +1,8 @@
1
+ const EMPTY_AXIS = { scroll: 0, maxScroll: 0, progress: 0, canScrollPrev: false, canScrollNext: false };
2
+ export const EMPTY_STATE = {
3
+ x: EMPTY_AXIS,
4
+ y: EMPTY_AXIS,
5
+ isDragging: false,
6
+ isMomentum: false,
7
+ };
8
+ export const NOOP = () => { };
@@ -0,0 +1,3 @@
1
+ export type { AxisApi, UseAxisResult } from "./types.js";
2
+ export { useAxis } from "./use-axis.js";
3
+ export type { AxisMode, AxisName, AxisOptions, AxisPosition, AxisScrollState, AxisScrollToOptions, AxisState, } from "../axis/index.js";
@@ -0,0 +1 @@
1
+ export { useAxis } from "./use-axis.js";
@@ -0,0 +1,12 @@
1
+ import type { AxisInstance, AxisScrollState } from "../axis/index.js";
2
+ /** Imperative scroll controls returned by {@link useAxis}. Safe to call before the element mounts (no-op). */
3
+ export type AxisApi = Pick<AxisInstance, "scrollPrev" | "scrollNext" | "scrollTo" | "refresh">;
4
+ /** What {@link useAxis} returns. */
5
+ export interface UseAxisResult<T extends HTMLElement = HTMLElement, S = AxisScrollState> {
6
+ /** Callback ref for the scroll container. Stable. */
7
+ ref: (element: T | null) => void;
8
+ /** The scroll state of both axes, or what the selector picked from it. Re-renders only when it changes. */
9
+ state: S;
10
+ /** Stable scroll controls. */
11
+ api: AxisApi;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { type RefObject } from "react";
2
+ import type { AxisInstance, AxisOptions } from "../axis/index.js";
3
+ export declare function useAppliedOptions(options: AxisOptions, instanceRef: RefObject<AxisInstance | null>): RefObject<AxisOptions>;
@@ -0,0 +1,11 @@
1
+ import { useRef } from "react";
2
+ import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
3
+ export function useAppliedOptions(options, instanceRef) {
4
+ const optionsRef = useRef(options);
5
+ const { axis, momentum, wheel, cursor } = options;
6
+ useIsomorphicLayoutEffect(() => {
7
+ optionsRef.current = { axis, momentum, wheel, cursor };
8
+ instanceRef.current?.setOptions(optionsRef.current);
9
+ }, [axis, momentum, wheel, cursor]);
10
+ return optionsRef;
11
+ }
@@ -0,0 +1,3 @@
1
+ import { type RefObject } from "react";
2
+ import { type AxisInstance, type AxisOptions } from "../axis/index.js";
3
+ export declare function useAxisInstance(element: HTMLElement | null, optionsRef: RefObject<AxisOptions>, instanceRef: RefObject<AxisInstance | null>): AxisInstance | null;
@@ -0,0 +1,19 @@
1
+ import { useState } from "react";
2
+ import { createAxis } from "../axis/index.js";
3
+ import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
4
+ export function useAxisInstance(element, optionsRef, instanceRef) {
5
+ const [instance, setInstance] = useState(null);
6
+ useIsomorphicLayoutEffect(() => {
7
+ if (!element)
8
+ return;
9
+ const created = createAxis(element, optionsRef.current);
10
+ instanceRef.current = created;
11
+ setInstance(created);
12
+ return () => {
13
+ created.destroy();
14
+ instanceRef.current = null;
15
+ setInstance(null);
16
+ };
17
+ }, [element]);
18
+ return instance;
19
+ }
@@ -0,0 +1,11 @@
1
+ import type { AxisOptions, AxisScrollState } from "../axis/index.js";
2
+ import type { UseAxisResult } from "./types.js";
3
+ /**
4
+ * Binds axis to an element through a callback ref. The instance is created when the element mounts and
5
+ * destroyed when it unmounts or changes. Option changes are applied without re-creating the instance.
6
+ *
7
+ * With a `selector`, `state` is `selector(state)` and the component re-renders only when that result changes
8
+ * (compared with `Object.is`).
9
+ */
10
+ export declare function useAxis<T extends HTMLElement = HTMLElement>(options?: AxisOptions): UseAxisResult<T>;
11
+ export declare function useAxis<S>(options: AxisOptions | undefined, selector: (state: AxisScrollState) => S): UseAxisResult<HTMLElement, S>;
@@ -0,0 +1,14 @@
1
+ import { useRef, useState } from "react";
2
+ import { useAppliedOptions } from "./use-applied-options.js";
3
+ import { useAxisInstance } from "./use-axis-instance.js";
4
+ import { useSelectedState } from "./use-selected-state.js";
5
+ import { useStableApi } from "./use-stable-api.js";
6
+ export function useAxis(options = {}, selector) {
7
+ const [element, setElement] = useState(null);
8
+ const instanceRef = useRef(null);
9
+ const optionsRef = useAppliedOptions(options, instanceRef);
10
+ const instance = useAxisInstance(element, optionsRef, instanceRef);
11
+ const state = useSelectedState(instance, selector);
12
+ const api = useStableApi(instanceRef);
13
+ return { ref: setElement, state, api };
14
+ }
@@ -0,0 +1,2 @@
1
+ import { useEffect } from "react";
2
+ export declare const useIsomorphicLayoutEffect: typeof useEffect;
@@ -0,0 +1,2 @@
1
+ import { useEffect, useLayoutEffect } from "react";
2
+ export const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
@@ -0,0 +1,4 @@
1
+ import type { AxisInstance, AxisScrollState } from "../axis/index.js";
2
+ type Selector = ((state: AxisScrollState) => unknown) | undefined;
3
+ export declare function useSelectedState(instance: AxisInstance | null, selector: Selector): unknown;
4
+ export {};
@@ -0,0 +1,14 @@
1
+ import { useCallback, useRef, useSyncExternalStore } from "react";
2
+ import { EMPTY_STATE, NOOP } from "./constants.js";
3
+ export function useSelectedState(instance, selector) {
4
+ const subscribe = useCallback((listener) => instance?.subscribe(listener) ?? NOOP, [instance]);
5
+ const snapshotMemo = useRef(null);
6
+ const getSnapshot = useCallback(() => {
7
+ const current = instance?.getState() ?? EMPTY_STATE;
8
+ if (snapshotMemo.current?.state !== current || snapshotMemo.current.selector !== selector) {
9
+ snapshotMemo.current = { state: current, selector, selected: selector ? selector(current) : current };
10
+ }
11
+ return snapshotMemo.current.selected;
12
+ }, [instance, selector]);
13
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
14
+ }
@@ -0,0 +1,4 @@
1
+ import { type RefObject } from "react";
2
+ import type { AxisInstance } from "../axis/index.js";
3
+ import type { AxisApi } from "./types.js";
4
+ export declare const useStableApi: (instanceRef: RefObject<AxisInstance | null>) => AxisApi;