@dloizides/game-input 1.3.0 → 1.5.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.
- package/CHANGELOG.md +41 -26
- package/README.md +76 -74
- package/dist/index.d.mts +72 -6
- package/dist/index.d.ts +72 -6
- package/dist/index.js +405 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +392 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -120,11 +120,375 @@ function createGamepadPoller(opts) {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// src/menu/constants.ts
|
|
124
|
+
var MENU_FOCUSABLE_SELECTOR = [
|
|
125
|
+
"button",
|
|
126
|
+
"a[href]",
|
|
127
|
+
"input",
|
|
128
|
+
"select",
|
|
129
|
+
"textarea",
|
|
130
|
+
'[role="button"]',
|
|
131
|
+
'[role="tab"]',
|
|
132
|
+
'[role="option"]',
|
|
133
|
+
'[role="slider"]',
|
|
134
|
+
'[role="switch"]',
|
|
135
|
+
'[role="checkbox"]',
|
|
136
|
+
'[role="radio"]',
|
|
137
|
+
'[tabindex="0"]'
|
|
138
|
+
].join(", ");
|
|
139
|
+
var NATIVE_FOCUSABLE_SELECTOR = "button, input, select, textarea, a[href]";
|
|
140
|
+
var DIALOG_SELECTOR = '[role="dialog"]';
|
|
141
|
+
var HIDDEN_ANCESTOR_SELECTOR = '[hidden], [inert], [aria-hidden="true"]';
|
|
142
|
+
var MENU_DEFAULT_ATTR = "data-menu-default";
|
|
143
|
+
var NAV_INPUT_ATTR = "data-nav-input";
|
|
144
|
+
var NAV_INPUT = { key: "key", pad: "pad" };
|
|
145
|
+
var OBSERVED_ATTRIBUTES = ["hidden", "class", "style", "aria-hidden", "aria-modal", "inert", "open"];
|
|
146
|
+
var ESCAPE_KEY = "Escape";
|
|
147
|
+
var ENTER_KEY = "Enter";
|
|
148
|
+
|
|
149
|
+
// src/menu/controlKind.ts
|
|
150
|
+
var NON_TEXT_INPUTS = ["button", "submit", "reset", "color", "file", "image"];
|
|
151
|
+
var TOGGLE_ROLES = ["checkbox", "switch", "radio"];
|
|
152
|
+
function inputKind(el) {
|
|
153
|
+
const type = el.type;
|
|
154
|
+
if (type === "range") {
|
|
155
|
+
return "slider" /* Slider */;
|
|
156
|
+
}
|
|
157
|
+
if (type === "checkbox" || type === "radio") {
|
|
158
|
+
return "toggle" /* Toggle */;
|
|
159
|
+
}
|
|
160
|
+
return NON_TEXT_INPUTS.includes(type) ? "button" /* Button */ : "textField" /* TextField */;
|
|
161
|
+
}
|
|
162
|
+
function controlKind(el) {
|
|
163
|
+
if (el instanceof HTMLInputElement) {
|
|
164
|
+
return inputKind(el);
|
|
165
|
+
}
|
|
166
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
167
|
+
return "textField" /* TextField */;
|
|
168
|
+
}
|
|
169
|
+
if (el instanceof HTMLSelectElement) {
|
|
170
|
+
return "select" /* Select */;
|
|
171
|
+
}
|
|
172
|
+
const role = el.getAttribute("role") ?? "";
|
|
173
|
+
if (role === "slider") {
|
|
174
|
+
return "slider" /* Slider */;
|
|
175
|
+
}
|
|
176
|
+
return TOGGLE_ROLES.includes(role) ? "toggle" /* Toggle */ : "button" /* Button */;
|
|
177
|
+
}
|
|
178
|
+
var isTextField = (el) => controlKind(el) === "textField" /* TextField */;
|
|
179
|
+
function isStepper(el) {
|
|
180
|
+
const kind = controlKind(el);
|
|
181
|
+
return kind === "slider" /* Slider */ || kind === "select" /* Select */;
|
|
182
|
+
}
|
|
183
|
+
function needsSyntheticClick(el) {
|
|
184
|
+
const kind = controlKind(el);
|
|
185
|
+
const clickable = kind === "button" /* Button */ || kind === "toggle" /* Toggle */;
|
|
186
|
+
return clickable && !el.matches(NATIVE_FOCUSABLE_SELECTOR);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/menu/focusables.ts
|
|
190
|
+
function isShown(el) {
|
|
191
|
+
return el.getClientRects().length > 0 && el.closest(HIDDEN_ANCESTOR_SELECTOR) === null;
|
|
192
|
+
}
|
|
193
|
+
var isDisabled = (el) => el.disabled === true;
|
|
194
|
+
function focusables(root) {
|
|
195
|
+
return Array.from(root.querySelectorAll(MENU_FOCUSABLE_SELECTOR)).filter((el) => isShown(el) && !isDisabled(el));
|
|
196
|
+
}
|
|
197
|
+
function defaultInitialFocus(root) {
|
|
198
|
+
const list = focusables(root);
|
|
199
|
+
return list.find((el) => el.hasAttribute(MENU_DEFAULT_ATTR)) ?? list[0] ?? null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/menu/menuRoot.ts
|
|
203
|
+
function menuRoot(baseRoot) {
|
|
204
|
+
const dialogs = Array.from(document.querySelectorAll(DIALOG_SELECTOR)).filter(isShown);
|
|
205
|
+
const modal = dialogs.filter((d) => d.getAttribute("aria-modal") === "true");
|
|
206
|
+
const top = (modal.length > 0 ? modal : dialogs).pop();
|
|
207
|
+
if (top !== void 0) {
|
|
208
|
+
return top;
|
|
209
|
+
}
|
|
210
|
+
const base = baseRoot();
|
|
211
|
+
return base !== null && isShown(base) ? base : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/menu/pickTarget.ts
|
|
215
|
+
var NAV_DIRS = { up: "arrowup", down: "arrowdown", left: "arrowleft", right: "arrowright" };
|
|
216
|
+
var NAV_KEYS = Object.values(NAV_DIRS);
|
|
217
|
+
var CROSS_WEIGHT = 2;
|
|
218
|
+
var SLACK = 1;
|
|
219
|
+
var navDir = (key) => NAV_KEYS.includes(key.toLowerCase()) ? key.toLowerCase() : null;
|
|
220
|
+
var centre = (b) => ({ cx: b.x + b.w / 2, cy: b.y + b.h / 2 });
|
|
221
|
+
function cost(a, b, dir) {
|
|
222
|
+
const p = centre(a);
|
|
223
|
+
const q = centre(b);
|
|
224
|
+
const dx = q.cx - p.cx;
|
|
225
|
+
const dy = q.cy - p.cy;
|
|
226
|
+
const vertical = dir === NAV_DIRS.up || dir === NAV_DIRS.down;
|
|
227
|
+
const along = vertical ? dir === NAV_DIRS.down ? dy : -dy : dir === NAV_DIRS.right ? dx : -dx;
|
|
228
|
+
if (along <= SLACK) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return along + CROSS_WEIGHT * Math.abs(vertical ? dx : dy);
|
|
232
|
+
}
|
|
233
|
+
function pickTarget(boxes, from, dir) {
|
|
234
|
+
if (boxes.length === 0) {
|
|
235
|
+
return -1;
|
|
236
|
+
}
|
|
237
|
+
if (from < 0 || from >= boxes.length) {
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
const origin = boxes[from];
|
|
241
|
+
let best = -1;
|
|
242
|
+
let bestCost = Infinity;
|
|
243
|
+
boxes.forEach((b, i) => {
|
|
244
|
+
const c = i === from ? null : cost(origin, b, dir);
|
|
245
|
+
if (c !== null && c < bestCost) {
|
|
246
|
+
best = i;
|
|
247
|
+
bestCost = c;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
if (best >= 0) {
|
|
251
|
+
return best;
|
|
252
|
+
}
|
|
253
|
+
const step = dir === NAV_DIRS.down || dir === NAV_DIRS.right ? 1 : -1;
|
|
254
|
+
return (from + step + boxes.length) % boxes.length;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/menu/stepControl.ts
|
|
258
|
+
var DEFAULT_MAX = 100;
|
|
259
|
+
var num = (raw, fallback) => {
|
|
260
|
+
const n = parseFloat(raw ?? "");
|
|
261
|
+
return Number.isFinite(n) ? n : fallback;
|
|
262
|
+
};
|
|
263
|
+
function clampStep(value, delta, min, max, step) {
|
|
264
|
+
const decimals = (String(step).split(".")[1] ?? "").length;
|
|
265
|
+
return Math.min(max, Math.max(min, Number((value + delta * step).toFixed(decimals))));
|
|
266
|
+
}
|
|
267
|
+
function apply(el, current, next, write) {
|
|
268
|
+
if (next === current) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
write(next);
|
|
272
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
273
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
274
|
+
}
|
|
275
|
+
function stepControl(el, delta) {
|
|
276
|
+
if (el instanceof HTMLInputElement) {
|
|
277
|
+
const value = num(el.value, 0);
|
|
278
|
+
const next = clampStep(value, delta, num(el.min, 0), num(el.max, DEFAULT_MAX), num(el.step, 1));
|
|
279
|
+
apply(el, value, next, (v) => {
|
|
280
|
+
el.value = String(v);
|
|
281
|
+
});
|
|
282
|
+
} else if (el instanceof HTMLSelectElement) {
|
|
283
|
+
const next = Math.min(el.options.length - 1, Math.max(0, el.selectedIndex + delta));
|
|
284
|
+
apply(el, el.selectedIndex, next, (v) => {
|
|
285
|
+
el.selectedIndex = v;
|
|
286
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
const value = num(el.getAttribute("aria-valuenow"), 0);
|
|
289
|
+
const next = clampStep(value, delta, num(el.getAttribute("aria-valuemin"), 0), num(el.getAttribute("aria-valuemax"), DEFAULT_MAX), num(el.getAttribute("data-step"), 1));
|
|
290
|
+
apply(el, value, next, (v) => el.setAttribute("aria-valuenow", String(v)));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/menu/navActions.ts
|
|
295
|
+
var boxOf = (el) => {
|
|
296
|
+
const r = el.getBoundingClientRect();
|
|
297
|
+
return { x: r.left, y: r.top, w: r.width, h: r.height };
|
|
298
|
+
};
|
|
299
|
+
function focusControl(el) {
|
|
300
|
+
if (!el.matches(NATIVE_FOCUSABLE_SELECTOR) && !el.hasAttribute("tabindex")) {
|
|
301
|
+
el.setAttribute("tabindex", "-1");
|
|
302
|
+
}
|
|
303
|
+
el.focus({ preventScroll: true });
|
|
304
|
+
if (typeof el.scrollIntoView === "function") {
|
|
305
|
+
el.scrollIntoView({ block: "nearest" });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function activeIn(root) {
|
|
309
|
+
const a = document.activeElement;
|
|
310
|
+
return a instanceof HTMLElement && a !== root && root.contains(a) ? a : null;
|
|
311
|
+
}
|
|
312
|
+
function focusInitial(root, initial) {
|
|
313
|
+
const target = initial(root);
|
|
314
|
+
if (target === null) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
focusControl(target);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
function moveFocus(root, active, dir, initial) {
|
|
321
|
+
const list = focusables(root);
|
|
322
|
+
const from = active === null ? -1 : list.indexOf(active);
|
|
323
|
+
if (from < 0) {
|
|
324
|
+
return focusInitial(root, initial);
|
|
325
|
+
}
|
|
326
|
+
focusControl(list[pickTarget(list.map(boxOf), from, dir)]);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
function direct(root, active, dir, initial) {
|
|
330
|
+
const horizontal = dir === NAV_DIRS.left || dir === NAV_DIRS.right;
|
|
331
|
+
if (active !== null && horizontal && isStepper(active)) {
|
|
332
|
+
stepControl(active, dir === NAV_DIRS.right ? 1 : -1);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
return moveFocus(root, active, dir, initial);
|
|
336
|
+
}
|
|
337
|
+
function leaveField(root, field) {
|
|
338
|
+
const list = focusables(root);
|
|
339
|
+
const next = list[pickTarget(list.map(boxOf), list.indexOf(field), NAV_DIRS.down)];
|
|
340
|
+
if (next === field || next === void 0) {
|
|
341
|
+
field.blur();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
focusControl(next);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/menu/createMenuNav.ts
|
|
348
|
+
var PAD_DIRS = { up: NAV_DIRS.up, down: NAV_DIRS.down, left: NAV_DIRS.left, right: NAV_DIRS.right };
|
|
349
|
+
var isOpen = (el) => el.isConnected && isShown(el);
|
|
350
|
+
var markInput = (kind) => document.documentElement.setAttribute(NAV_INPUT_ATTR, kind);
|
|
351
|
+
var clearInput = () => document.documentElement.removeAttribute(NAV_INPUT_ATTR);
|
|
352
|
+
var sendEscape = () => window.dispatchEvent(new KeyboardEvent("keydown", { key: ESCAPE_KEY, bubbles: true, cancelable: true }));
|
|
353
|
+
function popClosed(stack) {
|
|
354
|
+
let restore = null;
|
|
355
|
+
while (stack.length > 0 && !isOpen(stack[stack.length - 1].dialog)) {
|
|
356
|
+
restore = stack.pop().restore;
|
|
357
|
+
}
|
|
358
|
+
return restore;
|
|
359
|
+
}
|
|
360
|
+
function canRestore(restore, root) {
|
|
361
|
+
return restore !== null && restore.isConnected && root.contains(restore) && isShown(restore);
|
|
362
|
+
}
|
|
363
|
+
function onKey(e, root, initial) {
|
|
364
|
+
const active = activeIn(root);
|
|
365
|
+
const dir = navDir(e.key);
|
|
366
|
+
if (e.key === ESCAPE_KEY) {
|
|
367
|
+
if (active === null || !isTextField(active)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
e.preventDefault();
|
|
371
|
+
e.stopImmediatePropagation();
|
|
372
|
+
leaveField(root, active);
|
|
373
|
+
} else if (e.key === ENTER_KEY) {
|
|
374
|
+
if (active === null || !needsSyntheticClick(active)) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
e.preventDefault();
|
|
378
|
+
active.click();
|
|
379
|
+
} else if (dir !== null) {
|
|
380
|
+
markInput(NAV_INPUT.key);
|
|
381
|
+
if (active !== null && isTextField(active)) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (direct(root, active, dir, initial)) {
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function onPad(action, root, initial) {
|
|
390
|
+
const active = root === null ? null : activeIn(root);
|
|
391
|
+
const dir = PAD_DIRS[action];
|
|
392
|
+
if (action === PAD_ACTIONS.back) {
|
|
393
|
+
if (root !== null && active !== null && isTextField(active)) {
|
|
394
|
+
leaveField(root, active);
|
|
395
|
+
} else {
|
|
396
|
+
sendEscape();
|
|
397
|
+
}
|
|
398
|
+
} else if (root === null) {
|
|
399
|
+
return;
|
|
400
|
+
} else if (dir !== void 0) {
|
|
401
|
+
direct(root, active, dir, initial);
|
|
402
|
+
} else if (action === PAD_ACTIONS.confirm) {
|
|
403
|
+
if (active === null) {
|
|
404
|
+
focusInitial(root, initial);
|
|
405
|
+
} else if (isTextField(active)) {
|
|
406
|
+
focusControl(active);
|
|
407
|
+
} else {
|
|
408
|
+
active.click();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function createMenuNav(options) {
|
|
413
|
+
const suspended = options.suspended ?? (() => false);
|
|
414
|
+
const initial = options.initialFocus ?? defaultInitialFocus;
|
|
415
|
+
const stack = [];
|
|
416
|
+
let root = menuRoot(options.baseRoot);
|
|
417
|
+
let lastInRoot = null;
|
|
418
|
+
const settle = (next) => {
|
|
419
|
+
const restore = popClosed(stack);
|
|
420
|
+
if (next.matches(DIALOG_SELECTOR) && !stack.some((s) => s.dialog === next)) {
|
|
421
|
+
stack.push({ dialog: next, restore: lastInRoot });
|
|
422
|
+
}
|
|
423
|
+
if (activeIn(next) !== null) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (canRestore(restore, next)) {
|
|
427
|
+
focusControl(restore);
|
|
428
|
+
} else {
|
|
429
|
+
focusInitial(next, initial);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
const refresh = () => {
|
|
433
|
+
const next = menuRoot(options.baseRoot);
|
|
434
|
+
if (next === root) {
|
|
435
|
+
return next;
|
|
436
|
+
}
|
|
437
|
+
root = next;
|
|
438
|
+
if (next !== null) {
|
|
439
|
+
settle(next);
|
|
440
|
+
}
|
|
441
|
+
return next;
|
|
442
|
+
};
|
|
443
|
+
const keydown = (e) => {
|
|
444
|
+
if (e.altKey || e.ctrlKey || e.metaKey || suspended()) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const r = refresh();
|
|
448
|
+
if (r !== null) {
|
|
449
|
+
onKey(e, r, initial);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
const focusin = (e) => {
|
|
453
|
+
const t = e.target;
|
|
454
|
+
if (root !== null && root.contains(t) && t.closest(DIALOG_SELECTOR) === root.closest(DIALOG_SELECTOR)) {
|
|
455
|
+
lastInRoot = t;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
const observer = new MutationObserver(() => {
|
|
459
|
+
refresh();
|
|
460
|
+
});
|
|
461
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [...OBSERVED_ATTRIBUTES] });
|
|
462
|
+
window.addEventListener("keydown", keydown, true);
|
|
463
|
+
window.addEventListener("pointerdown", clearInput, true);
|
|
464
|
+
document.addEventListener("focusin", focusin, true);
|
|
465
|
+
return {
|
|
466
|
+
pad: (action) => {
|
|
467
|
+
if (suspended()) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
markInput(NAV_INPUT.pad);
|
|
471
|
+
onPad(action, refresh(), initial);
|
|
472
|
+
},
|
|
473
|
+
refresh: () => {
|
|
474
|
+
refresh();
|
|
475
|
+
},
|
|
476
|
+
dispose: () => {
|
|
477
|
+
observer.disconnect();
|
|
478
|
+
window.removeEventListener("keydown", keydown, true);
|
|
479
|
+
window.removeEventListener("pointerdown", clearInput, true);
|
|
480
|
+
document.removeEventListener("focusin", focusin, true);
|
|
481
|
+
clearInput();
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
123
486
|
// src/constants.ts
|
|
124
487
|
var DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
|
|
125
488
|
var DEFAULT_ENABLE_CONFIRMATION = 15;
|
|
126
489
|
var DISABLED_VALUE = "0";
|
|
127
490
|
var ENABLED_VALUE = "1";
|
|
491
|
+
var USER_ACTIVATION_EVENTS = ["pointerdown", "keydown", "touchstart"];
|
|
128
492
|
var HAPTIC_PATTERNS = {
|
|
129
493
|
/** Light tick for menu navigation / selection / undo / restart. */
|
|
130
494
|
tap: 10,
|
|
@@ -146,6 +510,29 @@ var HAPTIC_PATTERNS = {
|
|
|
146
510
|
reveal: [0, 14, 20, 18, 26, 24]
|
|
147
511
|
};
|
|
148
512
|
|
|
513
|
+
// src/userActivation.ts
|
|
514
|
+
function resolveActivationTarget(explicit) {
|
|
515
|
+
if (explicit) {
|
|
516
|
+
return explicit;
|
|
517
|
+
}
|
|
518
|
+
const candidate = globalThis;
|
|
519
|
+
return typeof candidate.addEventListener === "function" ? candidate : void 0;
|
|
520
|
+
}
|
|
521
|
+
function createActivationProbe(navigator, explicitTarget) {
|
|
522
|
+
let latched = false;
|
|
523
|
+
const target = typeof navigator?.vibrate === "function" ? resolveActivationTarget(explicitTarget) : void 0;
|
|
524
|
+
const onGesture = () => {
|
|
525
|
+
latched = true;
|
|
526
|
+
for (const type of USER_ACTIVATION_EVENTS) {
|
|
527
|
+
target?.removeEventListener(type, onGesture, true);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
for (const type of USER_ACTIVATION_EVENTS) {
|
|
531
|
+
target?.addEventListener(type, onGesture, true);
|
|
532
|
+
}
|
|
533
|
+
return () => latched || navigator?.userActivation?.hasBeenActive === true;
|
|
534
|
+
}
|
|
535
|
+
|
|
149
536
|
// src/haptics.ts
|
|
150
537
|
function resolveNavigator(explicit) {
|
|
151
538
|
if (explicit) {
|
|
@@ -164,6 +551,7 @@ function createHaptics(options = {}) {
|
|
|
164
551
|
const confirmation = options.enableConfirmation === void 0 ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;
|
|
165
552
|
const navigator = resolveNavigator(options.navigator);
|
|
166
553
|
const storage = resolveStorage(options.storage);
|
|
554
|
+
const hasUserActivation = createActivationProbe(navigator, options.activationTarget);
|
|
167
555
|
const readPref = () => {
|
|
168
556
|
try {
|
|
169
557
|
return storage?.getItem(storageKey) !== DISABLED_VALUE;
|
|
@@ -173,18 +561,20 @@ function createHaptics(options = {}) {
|
|
|
173
561
|
};
|
|
174
562
|
let enabled = readPref();
|
|
175
563
|
const vibrate = (pattern) => {
|
|
176
|
-
if (!enabled) {
|
|
564
|
+
if (!enabled || !hasUserActivation()) {
|
|
177
565
|
return;
|
|
178
566
|
}
|
|
179
567
|
try {
|
|
180
568
|
navigator?.vibrate?.(pattern);
|
|
181
569
|
} catch {
|
|
570
|
+
return;
|
|
182
571
|
}
|
|
183
572
|
};
|
|
184
573
|
const persist = () => {
|
|
185
574
|
try {
|
|
186
575
|
storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);
|
|
187
576
|
} catch {
|
|
577
|
+
return;
|
|
188
578
|
}
|
|
189
579
|
};
|
|
190
580
|
const setEnabled = (next) => {
|
|
@@ -203,6 +593,6 @@ function createHaptics(options = {}) {
|
|
|
203
593
|
};
|
|
204
594
|
}
|
|
205
595
|
|
|
206
|
-
export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, HAPTIC_PATTERNS, IDLE_REPEAT, PAD_ACTIONS, STANDARD_BUTTON, createGamepadPoller, createHaptics, stepPad };
|
|
596
|
+
export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, DIALOG_SELECTOR, HAPTIC_PATTERNS, IDLE_REPEAT, MENU_DEFAULT_ATTR, MENU_FOCUSABLE_SELECTOR, NAV_DIRS, NAV_INPUT, NAV_INPUT_ATTR, PAD_ACTIONS, STANDARD_BUTTON, USER_ACTIVATION_EVENTS, createGamepadPoller, createHaptics, createMenuNav, defaultInitialFocus, focusables, isShown, menuRoot, navDir, pickTarget, stepPad };
|
|
207
597
|
//# sourceMappingURL=index.mjs.map
|
|
208
598
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/gamepad/padMap.ts","../src/gamepad/stepPad.ts","../src/gamepad/createGamepadPoller.ts","../src/constants.ts","../src/haptics.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/** The semantic actions a Standard Gamepad produces, independent of the device's labels. */\nexport const PAD_ACTIONS = {\n up: 'up',\n down: 'down',\n left: 'left',\n right: 'right',\n confirm: 'confirm',\n back: 'back',\n mood: 'mood',\n view: 'view',\n pagePrev: 'pagePrev',\n pageNext: 'pageNext',\n} as const;\nexport type PadAction = (typeof PAD_ACTIONS)[keyof typeof PAD_ACTIONS];\n\n/** W3C Standard Gamepad button indices, named by POSITION (Nintendo swaps A/B labels). */\nexport const STANDARD_BUTTON = {\n south: 0,\n east: 1,\n west: 2,\n north: 3,\n leftShoulder: 4,\n rightShoulder: 5,\n dpadUp: 12,\n dpadDown: 13,\n dpadLeft: 14,\n dpadRight: 15,\n} as const;\n\n/** Edge-triggered buttons, in dispatch order. */\nexport const FACE_BUTTON_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.south, PAD_ACTIONS.confirm],\n [STANDARD_BUTTON.east, PAD_ACTIONS.back],\n [STANDARD_BUTTON.west, PAD_ACTIONS.view],\n [STANDARD_BUTTON.north, PAD_ACTIONS.mood],\n [STANDARD_BUTTON.leftShoulder, PAD_ACTIONS.pagePrev],\n [STANDARD_BUTTON.rightShoulder, PAD_ACTIONS.pageNext],\n];\n\n/** Held (auto-repeating) buttons; the first pressed one wins. */\nexport const DPAD_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.dpadUp, PAD_ACTIONS.up],\n [STANDARD_BUTTON.dpadDown, PAD_ACTIONS.down],\n [STANDARD_BUTTON.dpadLeft, PAD_ACTIONS.left],\n [STANDARD_BUTTON.dpadRight, PAD_ACTIONS.right],\n];\n\n/** Left-stick magnitude below which the stick counts as centred. */\nexport const STICK_DEAD_ZONE = 0.5;\n/** Delay before a held direction starts repeating (ms). */\nexport const REPEAT_DELAY_MS = 380;\n/** Interval between repeats once repeating (ms). */\nexport const REPEAT_INTERVAL_MS = 110;\n","import {\n DPAD_ACTIONS,\n FACE_BUTTON_ACTIONS,\n PAD_ACTIONS,\n REPEAT_DELAY_MS,\n REPEAT_INTERVAL_MS,\n STICK_DEAD_ZONE,\n type PadAction,\n} from './padMap';\n\n/** One frame of pad state: pressed flags by Standard button index, and raw axes. */\nexport interface PadSnapshot {\n buttons: readonly boolean[];\n axes: readonly number[];\n}\n\n/** Which direction is held, when it started, and when it last fired. */\nexport interface RepeatState {\n held: PadAction | null;\n since: number;\n lastFire: number;\n}\n\nexport const IDLE_REPEAT: RepeatState = { held: null, since: 0, lastFire: 0 };\n\nconst STICK_X = 0;\nconst STICK_Y = 1;\n\nfunction stickDirection(axes: readonly number[]): PadAction | null {\n const x = axes[STICK_X] ?? 0;\n const y = axes[STICK_Y] ?? 0;\n if (Math.max(Math.abs(x), Math.abs(y)) < STICK_DEAD_ZONE) {\n return null;\n }\n if (Math.abs(x) > Math.abs(y)) {\n return x > 0 ? PAD_ACTIONS.right : PAD_ACTIONS.left;\n }\n return y > 0 ? PAD_ACTIONS.down : PAD_ACTIONS.up;\n}\n\nfunction heldDirection(snapshot: PadSnapshot): PadAction | null {\n const dpad = DPAD_ACTIONS.find(([index]) => snapshot.buttons[index] === true);\n return dpad === undefined ? stickDirection(snapshot.axes) : dpad[1];\n}\n\n/** One poll frame → the actions to dispatch and the next repeat state. Pure. */\nexport function stepPad(\n prev: PadSnapshot | null,\n next: PadSnapshot,\n repeat: RepeatState,\n now: number,\n): { actions: PadAction[]; repeat: RepeatState } {\n const actions: PadAction[] = FACE_BUTTON_ACTIONS\n .filter(([index]) => next.buttons[index] === true && prev?.buttons[index] !== true)\n .map(([, action]) => action);\n const direction = heldDirection(next);\n if (direction === null) {\n return { actions, repeat: IDLE_REPEAT };\n }\n if (direction !== repeat.held) {\n return { actions: [...actions, direction], repeat: { held: direction, since: now, lastFire: now } };\n }\n const firstRepeat = repeat.lastFire === repeat.since;\n const wait = firstRepeat ? REPEAT_DELAY_MS : REPEAT_INTERVAL_MS;\n if (now - repeat.lastFire < wait) {\n return { actions, repeat };\n }\n return { actions: [...actions, direction], repeat: { ...repeat, lastFire: now } };\n}\n","import type { PadAction } from './padMap';\nimport { IDLE_REPEAT, stepPad, type PadSnapshot, type RepeatState } from './stepPad';\n\n/** The structural subset of the DOM `Gamepad` the poller reads. */\nexport interface PadLike {\n connected: boolean;\n buttons: ReadonlyArray<{ pressed: boolean }>;\n axes: readonly number[];\n}\n\nexport interface GamepadPollerOptions {\n /** Usually `() => navigator.getGamepads()`. */\n getPads: () => ReadonlyArray<PadLike | null>;\n /** Usually `requestAnimationFrame`. */\n requestFrame: (cb: (now: number) => void) => number;\n /** Usually `cancelAnimationFrame`. */\n cancelFrame: (id: number) => void;\n onAction: (action: PadAction) => void;\n}\n\n/**\n * rAF-driven poller over the first connected pad. Starts idle; call start().\n * Safe to stop() (or stop() then start()) from inside `onAction`.\n */\nexport function createGamepadPoller(opts: GamepadPollerOptions): { start: () => void; stop: () => void } {\n let prev: PadSnapshot | null = null;\n let repeat: RepeatState = IDLE_REPEAT;\n let running = false;\n let frame: number | null = null;\n\n const tick = (now: number): void => {\n frame = null;\n const pad = opts.getPads().find((p): p is PadLike => p !== null && p.connected);\n if (pad === undefined) {\n prev = null;\n repeat = IDLE_REPEAT;\n } else {\n const snapshot: PadSnapshot = { buttons: pad.buttons.map((b) => b.pressed), axes: pad.axes };\n const step = stepPad(prev, snapshot, repeat, now);\n prev = snapshot;\n repeat = step.repeat;\n step.actions.forEach((action) => opts.onAction(action));\n }\n if (running && frame === null) {\n frame = opts.requestFrame(tick);\n }\n };\n\n return {\n start: (): void => {\n if (running) {\n return;\n }\n running = true;\n if (frame === null) {\n frame = opts.requestFrame(tick);\n }\n },\n stop: (): void => {\n running = false;\n if (frame !== null) {\n opts.cancelFrame(frame);\n frame = null;\n }\n },\n };\n}\n","import type { VibrationPattern } from './types';\n\n/** Default localStorage key for the haptics on/off preference. */\nexport const DEFAULT_HAPTICS_STORAGE_KEY = 'game.haptics';\n\n/** Default confirmation tick fired when haptics are turned ON (ms). */\nexport const DEFAULT_ENABLE_CONFIRMATION = 15;\n\n/** Stored value meaning \"disabled\". Any other value (or absent) means enabled. */\nexport const DISABLED_VALUE = '0';\n\n/** Stored value meaning \"enabled\". */\nexport const ENABLED_VALUE = '1';\n\n/** The semantic intents games map their events onto. */\nexport type HapticIntent =\n | 'tap'\n | 'select'\n | 'lock'\n | 'bump'\n | 'navigate'\n | 'success'\n | 'error'\n | 'loss'\n | 'reveal';\n\n/**\n * A shared vocabulary of vibration patterns keyed by intent, so every game uses\n * the same tuned feel instead of re-picking magic durations. Pass any of these to\n * {@link Haptics.vibrate} (or supply your own pattern).\n */\nexport const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern> = {\n /** Light tick for menu navigation / selection / undo / restart. */\n tap: 10,\n /** Light pulse when a tool/charm/item is picked up or selected. */\n select: 12,\n /** Short pulse when something locks into place. */\n lock: 18,\n /** Dull buzz when an action is blocked. */\n bump: 24,\n /** Quick pulse when stepping through a portal / page turn. */\n navigate: 16,\n /** Celebratory pattern when a level/chamber is cleared. */\n success: [0, 40, 50, 90],\n /** Sharp double crack for a fatal/destructive event. */\n error: [0, 14, 24, 14],\n /** Heavier buzz when the player loses / falls. */\n loss: [0, 40, 50, 40],\n /** Rising triple pulse the instant a puzzle resolves / an exit opens. */\n reveal: [0, 14, 20, 18, 26, 24],\n};\n","import {\n DEFAULT_ENABLE_CONFIRMATION,\n DEFAULT_HAPTICS_STORAGE_KEY,\n DISABLED_VALUE,\n ENABLED_VALUE,\n} from './constants';\nimport type { Haptics, HapticsOptions, HapticsStorage, VibrationCapable, VibrationPattern } from './types';\n\nfunction resolveNavigator(explicit?: VibrationCapable): VibrationCapable | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { navigator?: VibrationCapable }).navigator;\n}\n\nfunction resolveStorage(explicit?: HapticsStorage): HapticsStorage | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { localStorage?: HapticsStorage }).localStorage;\n}\n\n/**\n * Create a player-toggleable, always-safe haptics controller.\n *\n * The on/off preference is read from (and persisted to) the configured storage key\n * so it survives reloads. Every vibration routes through one gated call that no-ops\n * when disabled, when no Vibration API is present, or if the platform blocks it.\n */\nexport function createHaptics(options: HapticsOptions = {}): Haptics {\n const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;\n const confirmation =\n options.enableConfirmation === undefined ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;\n const navigator = resolveNavigator(options.navigator);\n const storage = resolveStorage(options.storage);\n\n const readPref = (): boolean => {\n try {\n return storage?.getItem(storageKey) !== DISABLED_VALUE;\n } catch {\n // Reading storage can throw (blocked cookies / sandboxed iframe) — default ON.\n return true;\n }\n };\n\n let enabled = readPref();\n\n const vibrate = (pattern: VibrationPattern): void => {\n if (!enabled) {\n return;\n }\n try {\n navigator?.vibrate?.(pattern);\n } catch {\n // Vibration can throw if blocked by the platform — never let it break a move.\n }\n };\n\n const persist = (): void => {\n try {\n storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);\n } catch {\n // Writing storage can throw (private mode / quota) — ignore.\n }\n };\n\n const setEnabled = (next: boolean): boolean => {\n enabled = next;\n persist();\n // A confirming tick when turning ON so the player feels it engage.\n if (enabled && confirmation !== null) {\n vibrate(confirmation);\n }\n return enabled;\n };\n\n return {\n isEnabled: (): boolean => enabled,\n setEnabled,\n toggle: (): boolean => setEnabled(!enabled),\n vibrate,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/gamepad/padMap.ts","../src/gamepad/stepPad.ts","../src/gamepad/createGamepadPoller.ts","../src/menu/constants.ts","../src/menu/controlKind.ts","../src/menu/focusables.ts","../src/menu/menuRoot.ts","../src/menu/pickTarget.ts","../src/menu/stepControl.ts","../src/menu/navActions.ts","../src/menu/createMenuNav.ts","../src/constants.ts","../src/userActivation.ts","../src/haptics.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/** The semantic actions a Standard Gamepad produces, independent of the device's labels. */\nexport const PAD_ACTIONS = {\n up: 'up',\n down: 'down',\n left: 'left',\n right: 'right',\n confirm: 'confirm',\n back: 'back',\n mood: 'mood',\n view: 'view',\n pagePrev: 'pagePrev',\n pageNext: 'pageNext',\n} as const;\nexport type PadAction = (typeof PAD_ACTIONS)[keyof typeof PAD_ACTIONS];\n\n/** W3C Standard Gamepad button indices, named by POSITION (Nintendo swaps A/B labels). */\nexport const STANDARD_BUTTON = {\n south: 0,\n east: 1,\n west: 2,\n north: 3,\n leftShoulder: 4,\n rightShoulder: 5,\n dpadUp: 12,\n dpadDown: 13,\n dpadLeft: 14,\n dpadRight: 15,\n} as const;\n\n/** Edge-triggered buttons, in dispatch order. */\nexport const FACE_BUTTON_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.south, PAD_ACTIONS.confirm],\n [STANDARD_BUTTON.east, PAD_ACTIONS.back],\n [STANDARD_BUTTON.west, PAD_ACTIONS.view],\n [STANDARD_BUTTON.north, PAD_ACTIONS.mood],\n [STANDARD_BUTTON.leftShoulder, PAD_ACTIONS.pagePrev],\n [STANDARD_BUTTON.rightShoulder, PAD_ACTIONS.pageNext],\n];\n\n/** Held (auto-repeating) buttons; the first pressed one wins. */\nexport const DPAD_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.dpadUp, PAD_ACTIONS.up],\n [STANDARD_BUTTON.dpadDown, PAD_ACTIONS.down],\n [STANDARD_BUTTON.dpadLeft, PAD_ACTIONS.left],\n [STANDARD_BUTTON.dpadRight, PAD_ACTIONS.right],\n];\n\n/** Left-stick magnitude below which the stick counts as centred. */\nexport const STICK_DEAD_ZONE = 0.5;\n/** Delay before a held direction starts repeating (ms). */\nexport const REPEAT_DELAY_MS = 380;\n/** Interval between repeats once repeating (ms). */\nexport const REPEAT_INTERVAL_MS = 110;\n","import {\n DPAD_ACTIONS,\n FACE_BUTTON_ACTIONS,\n PAD_ACTIONS,\n REPEAT_DELAY_MS,\n REPEAT_INTERVAL_MS,\n STICK_DEAD_ZONE,\n type PadAction,\n} from './padMap';\n\n/** One frame of pad state: pressed flags by Standard button index, and raw axes. */\nexport interface PadSnapshot {\n buttons: readonly boolean[];\n axes: readonly number[];\n}\n\n/** Which direction is held, when it started, and when it last fired. */\nexport interface RepeatState {\n held: PadAction | null;\n since: number;\n lastFire: number;\n}\n\nexport const IDLE_REPEAT: RepeatState = { held: null, since: 0, lastFire: 0 };\n\nconst STICK_X = 0;\nconst STICK_Y = 1;\n\nfunction stickDirection(axes: readonly number[]): PadAction | null {\n const x = axes[STICK_X] ?? 0;\n const y = axes[STICK_Y] ?? 0;\n if (Math.max(Math.abs(x), Math.abs(y)) < STICK_DEAD_ZONE) {\n return null;\n }\n if (Math.abs(x) > Math.abs(y)) {\n return x > 0 ? PAD_ACTIONS.right : PAD_ACTIONS.left;\n }\n return y > 0 ? PAD_ACTIONS.down : PAD_ACTIONS.up;\n}\n\nfunction heldDirection(snapshot: PadSnapshot): PadAction | null {\n const dpad = DPAD_ACTIONS.find(([index]) => snapshot.buttons[index] === true);\n return dpad === undefined ? stickDirection(snapshot.axes) : dpad[1];\n}\n\n/** One poll frame → the actions to dispatch and the next repeat state. Pure. */\nexport function stepPad(\n prev: PadSnapshot | null,\n next: PadSnapshot,\n repeat: RepeatState,\n now: number,\n): { actions: PadAction[]; repeat: RepeatState } {\n const actions: PadAction[] = FACE_BUTTON_ACTIONS\n .filter(([index]) => next.buttons[index] === true && prev?.buttons[index] !== true)\n .map(([, action]) => action);\n const direction = heldDirection(next);\n if (direction === null) {\n return { actions, repeat: IDLE_REPEAT };\n }\n if (direction !== repeat.held) {\n return { actions: [...actions, direction], repeat: { held: direction, since: now, lastFire: now } };\n }\n const firstRepeat = repeat.lastFire === repeat.since;\n const wait = firstRepeat ? REPEAT_DELAY_MS : REPEAT_INTERVAL_MS;\n if (now - repeat.lastFire < wait) {\n return { actions, repeat };\n }\n return { actions: [...actions, direction], repeat: { ...repeat, lastFire: now } };\n}\n","import type { PadAction } from './padMap';\nimport { IDLE_REPEAT, stepPad, type PadSnapshot, type RepeatState } from './stepPad';\n\n/** The structural subset of the DOM `Gamepad` the poller reads. */\nexport interface PadLike {\n connected: boolean;\n buttons: ReadonlyArray<{ pressed: boolean }>;\n axes: readonly number[];\n}\n\nexport interface GamepadPollerOptions {\n /** Usually `() => navigator.getGamepads()`. */\n getPads: () => ReadonlyArray<PadLike | null>;\n /** Usually `requestAnimationFrame`. */\n requestFrame: (cb: (now: number) => void) => number;\n /** Usually `cancelAnimationFrame`. */\n cancelFrame: (id: number) => void;\n onAction: (action: PadAction) => void;\n}\n\n/**\n * rAF-driven poller over the first connected pad. Starts idle; call start().\n * Safe to stop() (or stop() then start()) from inside `onAction`.\n */\nexport function createGamepadPoller(opts: GamepadPollerOptions): { start: () => void; stop: () => void } {\n let prev: PadSnapshot | null = null;\n let repeat: RepeatState = IDLE_REPEAT;\n let running = false;\n let frame: number | null = null;\n\n const tick = (now: number): void => {\n frame = null;\n const pad = opts.getPads().find((p): p is PadLike => p !== null && p.connected);\n if (pad === undefined) {\n prev = null;\n repeat = IDLE_REPEAT;\n } else {\n const snapshot: PadSnapshot = { buttons: pad.buttons.map((b) => b.pressed), axes: pad.axes };\n const step = stepPad(prev, snapshot, repeat, now);\n prev = snapshot;\n repeat = step.repeat;\n step.actions.forEach((action) => opts.onAction(action));\n }\n if (running && frame === null) {\n frame = opts.requestFrame(tick);\n }\n };\n\n return {\n start: (): void => {\n if (running) {\n return;\n }\n running = true;\n if (frame === null) {\n frame = opts.requestFrame(tick);\n }\n },\n stop: (): void => {\n running = false;\n if (frame !== null) {\n opts.cancelFrame(frame);\n frame = null;\n }\n },\n };\n}\n","/** Every control a menu walker must be able to reach (shared with @dloizides/game-e2e). */\nexport const MENU_FOCUSABLE_SELECTOR = [\n 'button',\n 'a[href]',\n 'input',\n 'select',\n 'textarea',\n '[role=\"button\"]',\n '[role=\"tab\"]',\n '[role=\"option\"]',\n '[role=\"slider\"]',\n '[role=\"switch\"]',\n '[role=\"checkbox\"]',\n '[role=\"radio\"]',\n '[tabindex=\"0\"]',\n].join(', ');\n\nexport const NATIVE_FOCUSABLE_SELECTOR = 'button, input, select, textarea, a[href]';\nexport const DIALOG_SELECTOR = '[role=\"dialog\"]';\nexport const HIDDEN_ANCESTOR_SELECTOR = '[hidden], [inert], [aria-hidden=\"true\"]';\nexport const MENU_DEFAULT_ATTR = 'data-menu-default';\nexport const NAV_INPUT_ATTR = 'data-nav-input';\nexport const NAV_INPUT = { key: 'key', pad: 'pad' } as const;\nexport type NavInput = (typeof NAV_INPUT)[keyof typeof NAV_INPUT];\nexport const OBSERVED_ATTRIBUTES: readonly string[] = ['hidden', 'class', 'style', 'aria-hidden', 'aria-modal', 'inert', 'open'];\nexport const ESCAPE_KEY = 'Escape';\nexport const ENTER_KEY = 'Enter';\n","import { NATIVE_FOCUSABLE_SELECTOR } from './constants';\n\nexport const enum ControlKind {\n Button = 'button',\n Slider = 'slider',\n TextField = 'textField',\n Toggle = 'toggle',\n Select = 'select',\n}\n\nconst NON_TEXT_INPUTS: readonly string[] = ['button', 'submit', 'reset', 'color', 'file', 'image'];\nconst TOGGLE_ROLES: readonly string[] = ['checkbox', 'switch', 'radio'];\n\nfunction inputKind(el: HTMLInputElement): ControlKind {\n const type = el.type;\n if (type === 'range') {return ControlKind.Slider;}\n if (type === 'checkbox' || type === 'radio') {return ControlKind.Toggle;}\n return NON_TEXT_INPUTS.includes(type) ? ControlKind.Button : ControlKind.TextField;\n}\n\nexport function controlKind(el: Element): ControlKind {\n if (el instanceof HTMLInputElement) {return inputKind(el);}\n if (el instanceof HTMLTextAreaElement) {return ControlKind.TextField;}\n if (el instanceof HTMLSelectElement) {return ControlKind.Select;}\n const role = el.getAttribute('role') ?? '';\n if (role === 'slider') {return ControlKind.Slider;}\n return TOGGLE_ROLES.includes(role) ? ControlKind.Toggle : ControlKind.Button;\n}\n\nexport const isTextField = (el: Element): boolean => controlKind(el) === ControlKind.TextField;\n\nexport function isStepper(el: Element): boolean {\n const kind = controlKind(el);\n return kind === ControlKind.Slider || kind === ControlKind.Select;\n}\n\nexport function needsSyntheticClick(el: Element): boolean {\n const kind = controlKind(el);\n const clickable = kind === ControlKind.Button || kind === ControlKind.Toggle;\n return clickable && !el.matches(NATIVE_FOCUSABLE_SELECTOR);\n}\n","import { HIDDEN_ANCESTOR_SELECTOR, MENU_DEFAULT_ATTR, MENU_FOCUSABLE_SELECTOR } from './constants';\n\n/** True when the element has a layout box and no hidden, inert or aria-hidden ancestor. */\nexport function isShown(el: Element): boolean {\n return el.getClientRects().length > 0 && el.closest(HIDDEN_ANCESTOR_SELECTOR) === null;\n}\n\nconst isDisabled = (el: Element): boolean => (el as { disabled?: unknown }).disabled === true;\n\n/** The visible, enabled controls inside root, in DOM order. */\nexport function focusables(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)).filter((el) => isShown(el) && !isDisabled(el));\n}\n\n/** The [data-menu-default] control, else the first control, else null. */\nexport function defaultInitialFocus(root: HTMLElement): HTMLElement | null {\n const list = focusables(root);\n return list.find((el) => el.hasAttribute(MENU_DEFAULT_ATTR)) ?? list[0] ?? null;\n}\n","import { DIALOG_SELECTOR } from './constants';\nimport { isShown } from './focusables';\n\n/** Top-most visible dialog (aria-modal preferred, last in DOM wins), else the visible base root, else null. */\nexport function menuRoot(baseRoot: () => HTMLElement | null): HTMLElement | null {\n const dialogs = Array.from(document.querySelectorAll<HTMLElement>(DIALOG_SELECTOR)).filter(isShown);\n const modal = dialogs.filter((d) => d.getAttribute('aria-modal') === 'true');\n const top = (modal.length > 0 ? modal : dialogs).pop();\n if (top !== undefined) {return top;}\n const base = baseRoot();\n return base !== null && isShown(base) ? base : null;\n}\n","export interface Box { readonly x: number; readonly y: number; readonly w: number; readonly h: number }\n\nexport const NAV_DIRS = { up: 'arrowup', down: 'arrowdown', left: 'arrowleft', right: 'arrowright' } as const;\nexport type NavDir = (typeof NAV_DIRS)[keyof typeof NAV_DIRS];\n\nconst NAV_KEYS: readonly string[] = Object.values(NAV_DIRS);\nconst CROSS_WEIGHT = 2;\nconst SLACK = 1;\n\n/** The direction an arrow key names (case-insensitive), or null for any other key. */\nexport const navDir = (key: string): NavDir | null => (NAV_KEYS.includes(key.toLowerCase()) ? (key.toLowerCase() as NavDir) : null);\n\nconst centre = (b: Box): { cx: number; cy: number } => ({ cx: b.x + b.w / 2, cy: b.y + b.h / 2 });\n\nfunction cost(a: Box, b: Box, dir: NavDir): number | null {\n const p = centre(a);\n const q = centre(b);\n const dx = q.cx - p.cx;\n const dy = q.cy - p.cy;\n const vertical = dir === NAV_DIRS.up || dir === NAV_DIRS.down;\n const along = vertical ? (dir === NAV_DIRS.down ? dy : -dy) : dir === NAV_DIRS.right ? dx : -dx;\n if (along <= SLACK) {return null;}\n return along + CROSS_WEIGHT * Math.abs(vertical ? dx : dy);\n}\n\n/** Index of the nearest box from `from` in `dir`; wraps in list order when none lies that way; -1 for no boxes. */\nexport function pickTarget(boxes: readonly Box[], from: number, dir: NavDir): number {\n if (boxes.length === 0) {return -1;}\n if (from < 0 || from >= boxes.length) {return 0;}\n const origin = boxes[from] as Box;\n let best = -1;\n let bestCost = Infinity;\n boxes.forEach((b, i) => {\n const c = i === from ? null : cost(origin, b, dir);\n if (c !== null && c < bestCost) {\n best = i;\n bestCost = c;\n }\n });\n if (best >= 0) {return best;}\n const step = dir === NAV_DIRS.down || dir === NAV_DIRS.right ? 1 : -1;\n return (from + step + boxes.length) % boxes.length;\n}\n","const DEFAULT_MAX = 100;\n\nconst num = (raw: string | null, fallback: number): number => {\n const n = parseFloat(raw ?? '');\n return Number.isFinite(n) ? n : fallback;\n};\n\nfunction clampStep(value: number, delta: number, min: number, max: number, step: number): number {\n const decimals = (String(step).split('.')[1] ?? '').length;\n return Math.min(max, Math.max(min, Number((value + delta * step).toFixed(decimals))));\n}\n\nfunction apply(el: HTMLElement, current: number, next: number, write: (v: number) => void): void {\n if (next === current) {return;}\n write(next);\n el.dispatchEvent(new Event('input', { bubbles: true }));\n el.dispatchEvent(new Event('change', { bubbles: true }));\n}\n\n/** Steps a range, select or role=slider by one step in delta's direction, firing input + change when it moved. */\nexport function stepControl(el: HTMLElement, delta: 1 | -1): void {\n if (el instanceof HTMLInputElement) {\n const value = num(el.value, 0);\n const next = clampStep(value, delta, num(el.min, 0), num(el.max, DEFAULT_MAX), num(el.step, 1));\n apply(el, value, next, (v) => { el.value = String(v); });\n } else if (el instanceof HTMLSelectElement) {\n const next = Math.min(el.options.length - 1, Math.max(0, el.selectedIndex + delta));\n apply(el, el.selectedIndex, next, (v) => { el.selectedIndex = v; });\n } else {\n const value = num(el.getAttribute('aria-valuenow'), 0);\n const next = clampStep(value, delta, num(el.getAttribute('aria-valuemin'), 0), num(el.getAttribute('aria-valuemax'), DEFAULT_MAX), num(el.getAttribute('data-step'), 1));\n apply(el, value, next, (v) => el.setAttribute('aria-valuenow', String(v)));\n }\n}\n","import { NATIVE_FOCUSABLE_SELECTOR } from './constants';\nimport { isStepper } from './controlKind';\nimport { focusables } from './focusables';\nimport { type Box, NAV_DIRS, type NavDir, pickTarget } from './pickTarget';\nimport { stepControl } from './stepControl';\n\nexport type InitialFocus = (root: HTMLElement) => HTMLElement | null;\n\nconst boxOf = (el: Element): Box => {\n const r = el.getBoundingClientRect();\n return { x: r.left, y: r.top, w: r.width, h: r.height };\n};\n\n/** Focuses el (making a non-native control programmatically focusable) and scrolls it into view. */\nexport function focusControl(el: HTMLElement): void {\n if (!el.matches(NATIVE_FOCUSABLE_SELECTOR) && !el.hasAttribute('tabindex')) {el.setAttribute('tabindex', '-1');}\n el.focus({ preventScroll: true });\n if (typeof el.scrollIntoView === 'function') {el.scrollIntoView({ block: 'nearest' });}\n}\n\nexport function activeIn(root: HTMLElement): HTMLElement | null {\n const a = document.activeElement;\n return a instanceof HTMLElement && a !== root && root.contains(a) ? a : null;\n}\n\nexport function focusInitial(root: HTMLElement, initial: InitialFocus): boolean {\n const target = initial(root);\n if (target === null) {return false;}\n focusControl(target);\n return true;\n}\n\nfunction moveFocus(root: HTMLElement, active: HTMLElement | null, dir: NavDir, initial: InitialFocus): boolean {\n const list = focusables(root);\n const from = active === null ? -1 : list.indexOf(active);\n if (from < 0) {return focusInitial(root, initial);}\n focusControl(list[pickTarget(list.map(boxOf), from, dir)] as HTMLElement);\n return true;\n}\n\n/** Left/Right step a focused slider or select; every other case moves focus. True when it acted. */\nexport function direct(root: HTMLElement, active: HTMLElement | null, dir: NavDir, initial: InitialFocus): boolean {\n const horizontal = dir === NAV_DIRS.left || dir === NAV_DIRS.right;\n if (active !== null && horizontal && isStepper(active)) {\n stepControl(active, dir === NAV_DIRS.right ? 1 : -1);\n return true;\n }\n return moveFocus(root, active, dir, initial);\n}\n\n/** Leaves a text field for the control below it; blurs when it is the only control. */\nexport function leaveField(root: HTMLElement, field: HTMLElement): void {\n const list = focusables(root);\n const next = list[pickTarget(list.map(boxOf), list.indexOf(field), NAV_DIRS.down)];\n if (next === field || next === undefined) {\n field.blur();\n return;\n }\n focusControl(next);\n}\n","import { PAD_ACTIONS, type PadAction } from '../gamepad/padMap';\nimport { DIALOG_SELECTOR, ENTER_KEY, ESCAPE_KEY, NAV_INPUT, NAV_INPUT_ATTR, type NavInput, OBSERVED_ATTRIBUTES } from './constants';\nimport { isTextField, needsSyntheticClick } from './controlKind';\nimport { defaultInitialFocus, isShown } from './focusables';\nimport { menuRoot } from './menuRoot';\nimport { activeIn, direct, focusControl, focusInitial, leaveField } from './navActions';\nimport { NAV_DIRS, type NavDir, navDir } from './pickTarget';\n\nexport interface MenuNavOptions {\n /** The game's base menu root when no dialog is open; null = no menu (e.g. during play). */\n baseRoot: () => HTMLElement | null;\n /** True while input must not move focus (e.g. a remap capture). Default: never. */\n suspended?: () => boolean;\n /** Where focus goes when a root opens. Default: [data-menu-default], else the first control. */\n initialFocus?: (root: HTMLElement) => HTMLElement | null;\n}\n\nexport interface MenuNav {\n /** Feed one pad action from stepPad (the game keeps its own poller). */\n pad(action: PadAction): void;\n /** Re-evaluate the root and place focus (call after the game opens a screen). */\n refresh(): void;\n dispose(): void;\n}\n\ninterface OpenDialog { dialog: HTMLElement; restore: HTMLElement | null }\n\nconst PAD_DIRS: Partial<Record<PadAction, NavDir>> = { up: NAV_DIRS.up, down: NAV_DIRS.down, left: NAV_DIRS.left, right: NAV_DIRS.right };\n\nconst isOpen = (el: HTMLElement): boolean => el.isConnected && isShown(el);\nconst markInput = (kind: NavInput): void => document.documentElement.setAttribute(NAV_INPUT_ATTR, kind);\nconst clearInput = (): void => document.documentElement.removeAttribute(NAV_INPUT_ATTR);\nconst sendEscape = (): boolean => window.dispatchEvent(new KeyboardEvent('keydown', { key: ESCAPE_KEY, bubbles: true, cancelable: true }));\n\nfunction popClosed(stack: OpenDialog[]): HTMLElement | null {\n let restore: HTMLElement | null = null;\n while (stack.length > 0 && !isOpen((stack[stack.length - 1] as OpenDialog).dialog)) {restore = (stack.pop() as OpenDialog).restore;}\n return restore;\n}\n\nfunction canRestore(restore: HTMLElement | null, root: HTMLElement): restore is HTMLElement {\n return restore !== null && restore.isConnected && root.contains(restore) && isShown(restore);\n}\n\nfunction onKey(e: KeyboardEvent, root: HTMLElement, initial: (r: HTMLElement) => HTMLElement | null): void {\n const active = activeIn(root);\n const dir = navDir(e.key);\n if (e.key === ESCAPE_KEY) {\n if (active === null || !isTextField(active)) {return;}\n e.preventDefault();\n e.stopImmediatePropagation();\n leaveField(root, active);\n } else if (e.key === ENTER_KEY) {\n if (active === null || !needsSyntheticClick(active)) {return;}\n e.preventDefault();\n active.click();\n } else if (dir !== null) {\n markInput(NAV_INPUT.key);\n if (active !== null && isTextField(active)) {return;}\n if (direct(root, active, dir, initial)) {e.preventDefault();}\n }\n}\n\nfunction onPad(action: PadAction, root: HTMLElement | null, initial: (r: HTMLElement) => HTMLElement | null): void {\n const active = root === null ? null : activeIn(root);\n const dir = PAD_DIRS[action];\n if (action === PAD_ACTIONS.back) {\n if (root !== null && active !== null && isTextField(active)) {leaveField(root, active);}\n else {sendEscape();}\n } else if (root === null) {\n return;\n } else if (dir !== undefined) {\n direct(root, active, dir, initial);\n } else if (action === PAD_ACTIONS.confirm) {\n if (active === null) {focusInitial(root, initial);}\n else if (isTextField(active)) {focusControl(active);}\n else {active.click();}\n }\n}\n\n/** One focus engine for every game menu: arrow keys (capture phase), pad actions, dialogs on top, sliders and text fields. */\nexport function createMenuNav(options: MenuNavOptions): MenuNav {\n const suspended = options.suspended ?? ((): boolean => false);\n const initial = options.initialFocus ?? defaultInitialFocus;\n const stack: OpenDialog[] = [];\n let root = menuRoot(options.baseRoot);\n let lastInRoot: HTMLElement | null = null;\n\n const settle = (next: HTMLElement): void => {\n const restore = popClosed(stack);\n if (next.matches(DIALOG_SELECTOR) && !stack.some((s) => s.dialog === next)) {stack.push({ dialog: next, restore: lastInRoot });}\n if (activeIn(next) !== null) {return;}\n if (canRestore(restore, next)) {focusControl(restore);}\n else {focusInitial(next, initial);}\n };\n const refresh = (): HTMLElement | null => {\n const next = menuRoot(options.baseRoot);\n if (next === root) {return next;}\n root = next;\n if (next !== null) {settle(next);}\n return next;\n };\n const keydown = (e: KeyboardEvent): void => {\n if (e.altKey || e.ctrlKey || e.metaKey || suspended()) {return;}\n const r = refresh();\n if (r !== null) {onKey(e, r, initial);}\n };\n const focusin = (e: FocusEvent): void => {\n const t = e.target as HTMLElement;\n if (root !== null && root.contains(t) && t.closest(DIALOG_SELECTOR) === root.closest(DIALOG_SELECTOR)) {lastInRoot = t;}\n };\n const observer = new MutationObserver(() => { refresh(); });\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [...OBSERVED_ATTRIBUTES] });\n window.addEventListener('keydown', keydown, true);\n window.addEventListener('pointerdown', clearInput, true);\n document.addEventListener('focusin', focusin, true);\n\n return {\n pad: (action: PadAction): void => {\n if (suspended()) {return;}\n markInput(NAV_INPUT.pad);\n onPad(action, refresh(), initial);\n },\n refresh: (): void => { refresh(); },\n dispose: (): void => {\n observer.disconnect();\n window.removeEventListener('keydown', keydown, true);\n window.removeEventListener('pointerdown', clearInput, true);\n document.removeEventListener('focusin', focusin, true);\n clearInput();\n },\n };\n}\n","import type { VibrationPattern } from './types';\r\n\r\n/** Default localStorage key for the haptics on/off preference. */\r\nexport const DEFAULT_HAPTICS_STORAGE_KEY = 'game.haptics';\r\n\r\n/** Default confirmation tick fired when haptics are turned ON (ms). */\r\nexport const DEFAULT_ENABLE_CONFIRMATION = 15;\r\n\r\n/** Stored value meaning \"disabled\". Any other value (or absent) means enabled. */\r\nexport const DISABLED_VALUE = '0';\r\n\r\n/** Stored value meaning \"enabled\". */\r\nexport const ENABLED_VALUE = '1';\r\n\r\n/** DOM events that count as a user activation before `navigator.userActivation` is available. */\r\nexport const USER_ACTIVATION_EVENTS = ['pointerdown', 'keydown', 'touchstart'] as const;\r\n\r\n/** The semantic intents games map their events onto. */\r\nexport type HapticIntent =\r\n | 'tap'\r\n | 'select'\r\n | 'lock'\r\n | 'bump'\r\n | 'navigate'\r\n | 'success'\r\n | 'error'\r\n | 'loss'\r\n | 'reveal';\r\n\r\n/**\r\n * A shared vocabulary of vibration patterns keyed by intent, so every game uses\r\n * the same tuned feel instead of re-picking magic durations. Pass any of these to\r\n * {@link Haptics.vibrate} (or supply your own pattern).\r\n */\r\nexport const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern> = {\r\n /** Light tick for menu navigation / selection / undo / restart. */\r\n tap: 10,\r\n /** Light pulse when a tool/charm/item is picked up or selected. */\r\n select: 12,\r\n /** Short pulse when something locks into place. */\r\n lock: 18,\r\n /** Dull buzz when an action is blocked. */\r\n bump: 24,\r\n /** Quick pulse when stepping through a portal / page turn. */\r\n navigate: 16,\r\n /** Celebratory pattern when a level/chamber is cleared. */\r\n success: [0, 40, 50, 90],\r\n /** Sharp double crack for a fatal/destructive event. */\r\n error: [0, 14, 24, 14],\r\n /** Heavier buzz when the player loses / falls. */\r\n loss: [0, 40, 50, 40],\r\n /** Rising triple pulse the instant a puzzle resolves / an exit opens. */\r\n reveal: [0, 14, 20, 18, 26, 24],\r\n};\r\n","import { USER_ACTIVATION_EVENTS } from './constants';\nimport type { ActivationTarget, VibrationCapable } from './types';\n\nfunction resolveActivationTarget(explicit?: ActivationTarget): ActivationTarget | undefined {\n if (explicit) {return explicit;}\n const candidate = globalThis as Partial<ActivationTarget>;\n return typeof candidate.addEventListener === 'function' ? (candidate as ActivationTarget) : undefined;\n}\n\n/** Returns a probe that is true once the page has received a user gesture. */\nexport function createActivationProbe(\n navigator: VibrationCapable | undefined,\n explicitTarget?: ActivationTarget,\n): () => boolean {\n let latched = false;\n const target = typeof navigator?.vibrate === 'function' ? resolveActivationTarget(explicitTarget) : undefined;\n\n const onGesture = (): void => {\n latched = true;\n for (const type of USER_ACTIVATION_EVENTS) {target?.removeEventListener(type, onGesture, true);}\n };\n\n for (const type of USER_ACTIVATION_EVENTS) {target?.addEventListener(type, onGesture, true);}\n\n return (): boolean => latched || navigator?.userActivation?.hasBeenActive === true;\n}\n","import {\r\n DEFAULT_ENABLE_CONFIRMATION,\r\n DEFAULT_HAPTICS_STORAGE_KEY,\r\n DISABLED_VALUE,\r\n ENABLED_VALUE,\r\n} from './constants';\r\nimport type { Haptics, HapticsOptions, HapticsStorage, VibrationCapable, VibrationPattern } from './types';\r\nimport { createActivationProbe } from './userActivation';\r\n\r\nfunction resolveNavigator(explicit?: VibrationCapable): VibrationCapable | undefined {\r\n if (explicit) {\r\n return explicit;\r\n }\r\n return (globalThis as { navigator?: VibrationCapable }).navigator;\r\n}\r\n\r\nfunction resolveStorage(explicit?: HapticsStorage): HapticsStorage | undefined {\r\n if (explicit) {\r\n return explicit;\r\n }\r\n return (globalThis as { localStorage?: HapticsStorage }).localStorage;\r\n}\r\n\r\n/**\r\n * Create a player-toggleable, always-safe haptics controller: it never vibrates before a\r\n * user gesture, while disabled, or when the Vibration API is missing or blocked.\r\n */\r\nexport function createHaptics(options: HapticsOptions = {}): Haptics {\r\n const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;\r\n const confirmation =\r\n options.enableConfirmation === undefined ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;\r\n const navigator = resolveNavigator(options.navigator);\r\n const storage = resolveStorage(options.storage);\r\n const hasUserActivation = createActivationProbe(navigator, options.activationTarget);\r\n\r\n const readPref = (): boolean => {\r\n try {\r\n return storage?.getItem(storageKey) !== DISABLED_VALUE;\r\n } catch {\r\n return true;\r\n }\r\n };\r\n\r\n let enabled = readPref();\r\n\r\n const vibrate = (pattern: VibrationPattern): void => {\r\n if (!enabled || !hasUserActivation()) {\r\n return;\r\n }\r\n try {\r\n navigator?.vibrate?.(pattern);\r\n } catch {\r\n return;\r\n }\r\n };\r\n\r\n const persist = (): void => {\r\n try {\r\n storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);\r\n } catch {\r\n return;\r\n }\r\n };\r\n\r\n const setEnabled = (next: boolean): boolean => {\r\n enabled = next;\r\n persist();\r\n if (enabled && confirmation !== null) {\r\n vibrate(confirmation);\r\n }\r\n return enabled;\r\n };\r\n\r\n return {\r\n isEnabled: (): boolean => enabled,\r\n setEnabled,\r\n toggle: (): boolean => setEnabled(!enabled),\r\n vibrate,\r\n };\r\n}\r\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dloizides/game-input",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Framework-agnostic game input primitives. v1 ships safe, player-toggleable haptics (Vibration API wrapper, localStorage-gated, zero deps). Extracted from Aurora; consumed across EisaiPollis games.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"haptics",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"@typescript-eslint/parser": "^7.0.0",
|
|
73
73
|
"eslint": "^8.57.0",
|
|
74
74
|
"jest": "^29.7.0",
|
|
75
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
75
76
|
"rimraf": "^5.0.0",
|
|
76
77
|
"ts-jest": "^29.1.0",
|
|
77
78
|
"tsup": "^8.0.0",
|