mobile controls fixes

This commit is contained in:
legop3
2026-06-12 14:35:45 -04:00
parent 3efb31caae
commit 033bafeead
9 changed files with 192 additions and 138 deletions
+32 -3
View File
@@ -33,6 +33,7 @@ export default function HornControl({
);
const [waveform, setWaveform] = useState(hornSettings?.waveform || HORN_SETTINGS_DEFAULTS.waveform);
const settingsRootRef = useRef(null);
const activePointerIdRef = useRef(null);
const [freqs, setFreqs] = useState(() => {
const base = Array.isArray(hornSettings?.freqs) ? hornSettings.freqs : HORN_SETTINGS_DEFAULTS.freqs;
return [...base, 0, 0, 0, 0].slice(0, 4).map((f) => clampFreq(f));
@@ -129,16 +130,39 @@ export default function HornControl({
const handlePointerDown = (event) => {
if (disabled) return;
if (activePointerIdRef.current !== null) return;
const tag = event.target?.tagName?.toLowerCase();
if (tag === 'input' || tag === 'select' || tag === 'option' || tag === 'label' || tag === 'button') return;
/*
Horn is a hold control, so it should claim exactly the finger that started
it. Capturing that pointer prevents tiny mobile finger drift or nearby
controls from making the horn feel delayed or randomly interrupted.
*/
event.preventDefault();
activePointerIdRef.current = event.pointerId;
event.currentTarget.setPointerCapture?.(event.pointerId);
start();
};
const handlePointerUp = (event) => {
if (activePointerIdRef.current !== event.pointerId) return;
const tag = event.target?.tagName?.toLowerCase();
if (tag === 'input' || tag === 'select' || tag === 'option' || tag === 'label' || tag === 'button') return;
event.preventDefault();
activePointerIdRef.current = null;
event.currentTarget.releasePointerCapture?.(event.pointerId);
stop();
};
const handlePointerCancel = (event) => {
if (activePointerIdRef.current !== event.pointerId) return;
activePointerIdRef.current = null;
stop();
};
const handleBlur = () => {
activePointerIdRef.current = null;
stop();
};
@@ -164,10 +188,15 @@ export default function HornControl({
aria-pressed={pressed}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onPointerLeave={stop}
onPointerCancel={stop}
onPointerCancel={handlePointerCancel}
onLostPointerCapture={(event) => {
if (activePointerIdRef.current === event.pointerId) {
activePointerIdRef.current = null;
stop();
}
}}
onContextMenu={(event) => event.preventDefault()}
onBlur={stop}
onBlur={handleBlur}
className={buttonClasses}
>
<div
@@ -19,6 +19,7 @@ export default function NightVisionControl({
isBoolean(nightVisionOn) ? nightVisionOn : null,
);
const suppressClickRef = useRef(false);
const suppressClickTimerRef = useRef(null);
useEffect(() => {
if (isBoolean(nightVisionOn)) {
@@ -26,6 +27,15 @@ export default function NightVisionControl({
}
}, [nightVisionOn]);
useEffect(
() => () => {
if (suppressClickTimerRef.current) {
clearTimeout(suppressClickTimerRef.current);
}
},
[],
);
const hasState = isBoolean(optimistic);
const displayOn = hasState ? optimistic : false;
const statusLabel = hasState ? (displayOn ? 'On' : 'Off') : '—';
@@ -52,15 +62,29 @@ export default function NightVisionControl({
*/
event.preventDefault();
suppressClickRef.current = true;
handleToggle();
window.setTimeout(() => {
if (suppressClickTimerRef.current) {
clearTimeout(suppressClickTimerRef.current);
}
/*
The next click normally clears this flag, but Safari may suppress the click
completely after preventDefault(). The timer prevents a stale touch flag
from swallowing a later keyboard or desktop click.
*/
suppressClickTimerRef.current = window.setTimeout(() => {
suppressClickRef.current = false;
}, 0);
suppressClickTimerRef.current = null;
}, 800);
handleToggle();
};
const handleClick = (event) => {
if (suppressClickRef.current) {
event.preventDefault();
suppressClickRef.current = false;
if (suppressClickTimerRef.current) {
clearTimeout(suppressClickTimerRef.current);
suppressClickTimerRef.current = null;
}
return;
}
handleToggle();
+1 -1
View File
@@ -473,7 +473,7 @@ export function ControlSystemProvider({ children }) {
(nightVisionOn) => {
if (!pipeline.nightVision) return;
if (typeof nightVisionOn === 'boolean') {
const action = nightVisionOn ? 'off' : 'on';
const action = nightVisionOn ? 'on' : 'off';
pipeline.sendNightVision(action);
} else {
pipeline.sendNightVision('toggle');