@enigmax/primitives 0.1.0 → 0.1.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.
- package/README.md +1 -1
- package/dist/{chunk-5Q5TAW2N.js → chunk-QDGZANPT.js} +18 -3
- package/dist/index.js +1 -1
- package/dist/react/index.js +1 -1
- package/package.json +1 -1
- package/src/core/marquee.ts +38 -4
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ Three of these cost a production bug each. They are the reason this package exis
|
|
|
69
69
|
|
|
70
70
|
**`touch-action` decides whether a phone can scroll your page.** Without it the browser owns the horizontal swipe and the drag handlers never see it. With `none`, a visitor can no longer scroll the page by starting the gesture on the row, which on a phone is most of the width of the screen. The lane gets `pan-y` (or `pan-x` when vertical).
|
|
71
71
|
|
|
72
|
-
**A hover effect that ignores `pointerType` sticks forever on touch.** A tap fires `pointerenter` too, and on touch nothing ever fires the `pointerleave` that undoes it, so a "slow down on hover" leaves the row at 15% speed for good after the first tap. Every hover path here is gated to `pointerType === "mouse"`.
|
|
72
|
+
**A hover effect that ignores `pointerType` sticks forever on touch.** A tap fires `pointerenter` too, and on touch nothing ever fires the `pointerleave` that undoes it, so a "slow down on hover" leaves the row at 15% speed for good after the first tap. Every hover path here is gated to `pointerType === "mouse"` - and that gate alone is not enough, which cost a CI failure: Chromium follows a touch with *compatibility* pointer events that claim `pointerType: "mouse"`. It emits them on Linux and not on Windows, so the naive gate passes on one machine and fails on the other. A touch therefore also suppresses hover for a second, and `pointerleave` clears the state whatever the pointer type, because a hover that cannot be undone is the bug.
|
|
73
73
|
|
|
74
74
|
The rest, in short: the lap is measured from the step between two copies rather than computed from item widths; the transform is driven by one `requestAnimationFrame` loop rather than CSS keyframes, which restart on every re-author; one integrator carries cruise, hover and momentum so none of them can arrive as a cut; a drag past ~6px cancels the click in the capture phase; a right click never starts a drag; a pointer held still for ~90ms before release throws nothing; and reduced motion drops the autoplay while keeping the drag, because a drag is an answer to the pointer and not motion of the page's own accord.
|
|
75
75
|
|
|
@@ -4,6 +4,7 @@ var DRAG_THRESHOLD = 6;
|
|
|
4
4
|
var STILL_MS = 90;
|
|
5
5
|
var MAX_DT = 0.05;
|
|
6
6
|
var VELOCITY_SMOOTHING = 0.6;
|
|
7
|
+
var TOUCH_HOVER_SUPPRESS_MS = 1e3;
|
|
7
8
|
var IDLE_EPSILON = 0.01;
|
|
8
9
|
var DEFAULTS = {
|
|
9
10
|
speed: 60,
|
|
@@ -33,6 +34,7 @@ function createMarquee(lane, track, options = {}) {
|
|
|
33
34
|
let lastMoveTime = 0;
|
|
34
35
|
let maxTravel = 0;
|
|
35
36
|
let hovering = false;
|
|
37
|
+
let lastTouchAt = 0;
|
|
36
38
|
const motionQuery = typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
|
|
37
39
|
let reducedMotion = motionQuery?.matches ?? false;
|
|
38
40
|
function target() {
|
|
@@ -132,6 +134,7 @@ function createMarquee(lane, track, options = {}) {
|
|
|
132
134
|
return opts.vertical ? event.clientY : event.clientX;
|
|
133
135
|
}
|
|
134
136
|
function onPointerDown(event) {
|
|
137
|
+
if (event.pointerType !== "mouse") noteTouch();
|
|
135
138
|
if (!opts.draggable || event.button > 0 || pointerId !== null) return;
|
|
136
139
|
pointerId = event.pointerId;
|
|
137
140
|
dragging = true;
|
|
@@ -178,13 +181,23 @@ function createMarquee(lane, track, options = {}) {
|
|
|
178
181
|
opts.onItemClick?.(event);
|
|
179
182
|
}
|
|
180
183
|
function onPointerEnter(event) {
|
|
181
|
-
if (event.pointerType !== "mouse")
|
|
184
|
+
if (event.pointerType !== "mouse") {
|
|
185
|
+
noteTouch();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (Date.now() - lastTouchAt < TOUCH_HOVER_SUPPRESS_MS) return;
|
|
182
189
|
hovering = true;
|
|
183
190
|
lane.dataset.hovering = "true";
|
|
184
191
|
start();
|
|
185
192
|
}
|
|
186
|
-
function onPointerLeave(
|
|
187
|
-
|
|
193
|
+
function onPointerLeave() {
|
|
194
|
+
hovering = false;
|
|
195
|
+
delete lane.dataset.hovering;
|
|
196
|
+
start();
|
|
197
|
+
}
|
|
198
|
+
function noteTouch() {
|
|
199
|
+
lastTouchAt = Date.now();
|
|
200
|
+
if (!hovering) return;
|
|
188
201
|
hovering = false;
|
|
189
202
|
delete lane.dataset.hovering;
|
|
190
203
|
start();
|
|
@@ -206,6 +219,7 @@ function createMarquee(lane, track, options = {}) {
|
|
|
206
219
|
lane.addEventListener("pointerdown", onPointerDown);
|
|
207
220
|
lane.addEventListener("pointerenter", onPointerEnter);
|
|
208
221
|
lane.addEventListener("pointerleave", onPointerLeave);
|
|
222
|
+
lane.addEventListener("touchstart", noteTouch, { passive: true });
|
|
209
223
|
lane.addEventListener("click", onClickCapture, true);
|
|
210
224
|
lane.addEventListener("click", onClick);
|
|
211
225
|
motionQuery?.addEventListener("change", onMotionChange);
|
|
@@ -255,6 +269,7 @@ function createMarquee(lane, track, options = {}) {
|
|
|
255
269
|
lane.removeEventListener("pointerdown", onPointerDown);
|
|
256
270
|
lane.removeEventListener("pointerenter", onPointerEnter);
|
|
257
271
|
lane.removeEventListener("pointerleave", onPointerLeave);
|
|
272
|
+
lane.removeEventListener("touchstart", noteTouch);
|
|
258
273
|
lane.removeEventListener("click", onClickCapture, true);
|
|
259
274
|
lane.removeEventListener("click", onClick);
|
|
260
275
|
window.removeEventListener("pointermove", onPointerMove);
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createMarquee } from './chunk-
|
|
1
|
+
export { createMarquee } from './chunk-QDGZANPT.js';
|
package/dist/react/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createMarquee } from '../chunk-
|
|
1
|
+
import { createMarquee } from '../chunk-QDGZANPT.js';
|
|
2
2
|
import { useRef, useState, useEffect, useLayoutEffect } from 'react';
|
|
3
3
|
|
|
4
4
|
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/primitives",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Headless interaction primitives: the behaviour, the timing and the accessibility of components like a draggable infinite marquee, with no styles of their own. Framework-agnostic core plus thin adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/core/marquee.ts
CHANGED
|
@@ -20,6 +20,12 @@ const STILL_MS = 90;
|
|
|
20
20
|
const MAX_DT = 0.05;
|
|
21
21
|
/** One jittery frame must not decide the throw. */
|
|
22
22
|
const VELOCITY_SMOOTHING = 0.6;
|
|
23
|
+
/**
|
|
24
|
+
* How long a touch suppresses hover. Chromium's compatibility pointer events
|
|
25
|
+
* arrive within a frame or two of the touch and claim `pointerType: "mouse"`;
|
|
26
|
+
* a real mouse arriving later than this is a genuine hover.
|
|
27
|
+
*/
|
|
28
|
+
const TOUCH_HOVER_SUPPRESS_MS = 1000;
|
|
23
29
|
/** Below this the row is idle and the frame loop can stop. */
|
|
24
30
|
const IDLE_EPSILON = 0.01;
|
|
25
31
|
|
|
@@ -121,6 +127,7 @@ export function createMarquee(lane: HTMLElement, track: HTMLElement, options: Ma
|
|
|
121
127
|
let lastMoveTime = 0;
|
|
122
128
|
let maxTravel = 0;
|
|
123
129
|
let hovering = false;
|
|
130
|
+
let lastTouchAt = 0;
|
|
124
131
|
|
|
125
132
|
const motionQuery = typeof window.matchMedia === "function"
|
|
126
133
|
? window.matchMedia("(prefers-reduced-motion: reduce)")
|
|
@@ -255,6 +262,7 @@ export function createMarquee(lane: HTMLElement, track: HTMLElement, options: Ma
|
|
|
255
262
|
|
|
256
263
|
function onPointerDown(event: PointerEvent): void {
|
|
257
264
|
// Non-negotiable 9: a right click must leave the context menu alone.
|
|
265
|
+
if (event.pointerType !== "mouse") noteTouch();
|
|
258
266
|
if (!opts.draggable || event.button > 0 || pointerId !== null) return;
|
|
259
267
|
pointerId = event.pointerId;
|
|
260
268
|
dragging = true;
|
|
@@ -316,16 +324,38 @@ export function createMarquee(lane: HTMLElement, track: HTMLElement, options: Ma
|
|
|
316
324
|
opts.onItemClick?.(event);
|
|
317
325
|
}
|
|
318
326
|
|
|
319
|
-
/**
|
|
327
|
+
/**
|
|
328
|
+
* Non-negotiable 8: a tap fires pointerenter and never the pointerleave that
|
|
329
|
+
* undoes it, so the row would keep the hover speed for good after one touch.
|
|
330
|
+
*
|
|
331
|
+
* Checking `pointerType` is NOT enough on its own. After a touch, Chromium
|
|
332
|
+
* also emits the COMPATIBILITY pointer events, and those carry
|
|
333
|
+
* `pointerType: "mouse"` - observed on Linux while the same tap on Windows
|
|
334
|
+
* emitted nothing, which is how this passed locally and failed in CI. So a
|
|
335
|
+
* recent touch suppresses the hover as well.
|
|
336
|
+
*/
|
|
320
337
|
function onPointerEnter(event: PointerEvent): void {
|
|
321
|
-
if (event.pointerType !== "mouse")
|
|
338
|
+
if (event.pointerType !== "mouse") {
|
|
339
|
+
noteTouch();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (Date.now() - lastTouchAt < TOUCH_HOVER_SUPPRESS_MS) return;
|
|
322
343
|
hovering = true;
|
|
323
344
|
lane.dataset.hovering = "true";
|
|
324
345
|
start();
|
|
325
346
|
}
|
|
326
347
|
|
|
327
|
-
function onPointerLeave(
|
|
328
|
-
|
|
348
|
+
function onPointerLeave(): void {
|
|
349
|
+
// Clears on ANY pointer type: a hover that cannot be undone is the bug.
|
|
350
|
+
hovering = false;
|
|
351
|
+
delete lane.dataset.hovering;
|
|
352
|
+
start();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Stamp the last touch and drop any hover it may already have set. */
|
|
356
|
+
function noteTouch(): void {
|
|
357
|
+
lastTouchAt = Date.now();
|
|
358
|
+
if (!hovering) return;
|
|
329
359
|
hovering = false;
|
|
330
360
|
delete lane.dataset.hovering;
|
|
331
361
|
start();
|
|
@@ -351,6 +381,9 @@ export function createMarquee(lane: HTMLElement, track: HTMLElement, options: Ma
|
|
|
351
381
|
lane.addEventListener("pointerdown", onPointerDown);
|
|
352
382
|
lane.addEventListener("pointerenter", onPointerEnter);
|
|
353
383
|
lane.addEventListener("pointerleave", onPointerLeave);
|
|
384
|
+
// Fires BEFORE the compatibility pointer events a touch generates, so the
|
|
385
|
+
// suppression window is already open when the fake "mouse" enter arrives.
|
|
386
|
+
lane.addEventListener("touchstart", noteTouch, { passive: true });
|
|
354
387
|
lane.addEventListener("click", onClickCapture, true);
|
|
355
388
|
lane.addEventListener("click", onClick);
|
|
356
389
|
motionQuery?.addEventListener("change", onMotionChange);
|
|
@@ -390,6 +423,7 @@ export function createMarquee(lane: HTMLElement, track: HTMLElement, options: Ma
|
|
|
390
423
|
lane.removeEventListener("pointerdown", onPointerDown);
|
|
391
424
|
lane.removeEventListener("pointerenter", onPointerEnter);
|
|
392
425
|
lane.removeEventListener("pointerleave", onPointerLeave);
|
|
426
|
+
lane.removeEventListener("touchstart", noteTouch);
|
|
393
427
|
lane.removeEventListener("click", onClickCapture, true);
|
|
394
428
|
lane.removeEventListener("click", onClick);
|
|
395
429
|
window.removeEventListener("pointermove", onPointerMove);
|