Files
TREK/client/src/native/touchInput.ts
T
Maurice d06168d94a feat(native): the app answers a finger like a native app, not like a web page
After a tap, WebKit and Chrome keep :hover on what was tapped and send a
made-up mouseover, so buttons and rows stayed tinted until the next tap.
While a finger drives the app <html> carries data-touch-input: a PostCSS
step guards every :hover rule with it (no specificity change), and the
made-up mouseover and mouseout stop before any handler. A trackpad or mouse
clears the mark and gets hover back; a browser never sets it.

A control without a press of its own dims the moment a finger lands on it,
after a 60 ms wait so a scroll does not flash rows, and only the innermost
pressed control. A focus: ring no longer stays round a tapped button.

Fields get a fitting return key ("done", "go" in a form, "search" on the
search fields), and addresses, codes and numbers are no longer capitalised
or corrected. Ticking off packing items and tasks, a picker chip, a select
option, a picked date and dragging a plan row now give the haptics a native
list gives.

Confirmations ask through the system where the binary has the new
@capacitor/action-sheet and @capacitor/dialog plugins: a sheet with the
action in red when it destroys something, an alert otherwise, also for
"discard changes" in a sheet. An older app, a browser and a question with a
list under it keep the HTML dialog, as does a system that could not ask.

iOS no longer lifts a page preview on a long press on a link.
2026-11-09 08:54:51 +01:00

59 lines
2.7 KiB
TypeScript

/**
* Whether a finger or a pointer drives the app right now, for the styles that
* only make sense with one of them.
*
* After a tap, iOS and Android send the mouse events a mouse would have sent:
* mouseover first, which React reads as onMouseEnter, and WebKit sets :hover on
* the tapped element. Neither is ever taken back by a mouseout, so the tint a
* hover gives stays on the button until the next tap lands elsewhere, and on a
* phone that reads as a web page. While a finger is in use <html> carries
* data-touch-input: the build guards every :hover rule with it
* (postcss/touchHover.js), and the made-up mouseover and mouseout stop here
* before any handler sees them. A real pointer (an iPad trackpad or mouse)
* clears the mark on its first move, and hover works as on a computer.
*
* Only the app calls this. In a browser a tablet keeps tap-to-hover, which is
* how its users reach controls that show on hover.
*/
export const TOUCH_INPUT_ATTR = 'data-touch-input';
export function trackTouchInput(root: HTMLElement = document.documentElement, win: Window = window): () => void {
const setTouch = (on: boolean) => {
if (on === root.hasAttribute(TOUCH_INPUT_ATTR)) return;
if (on) root.setAttribute(TOUCH_INPUT_ATTR, '');
else root.removeAttribute(TOUCH_INPUT_ATTR);
};
// A phone starts with a finger; a trackpad says otherwise as soon as it moves.
setTouch(true);
const onPointerDown = (e: PointerEvent) => setTouch(e.pointerType !== 'mouse');
const onPointerMove = (e: PointerEvent) => {
if (e.pointerType === 'mouse') setTouch(false);
};
// Captured on the window, ahead of React's listener on the root and of every element's.
const swallow = (e: Event) => {
if (root.hasAttribute(TOUCH_INPUT_ATTR)) e.stopImmediatePropagation();
};
// WebKit applies :active on a phone only where a touchstart listener exists.
const noop = () => {};
const opts = { capture: true, passive: true } as const;
win.addEventListener('pointerdown', onPointerDown, opts);
win.addEventListener('pointerover', onPointerMove, opts);
win.addEventListener('pointermove', onPointerMove, opts);
win.addEventListener('mouseover', swallow, true);
win.addEventListener('mouseout', swallow, true);
win.document.addEventListener('touchstart', noop, { passive: true });
return () => {
win.removeEventListener('pointerdown', onPointerDown, opts);
win.removeEventListener('pointerover', onPointerMove, opts);
win.removeEventListener('pointermove', onPointerMove, opts);
win.removeEventListener('mouseover', swallow, true);
win.removeEventListener('mouseout', swallow, true);
win.document.removeEventListener('touchstart', noop);
root.removeAttribute(TOUCH_INPUT_ATTR);
};
}