Touch support for arrow clicks on React app - javascript

I am working on a React web app where I have a div of a 4x4 grid which has 16 blocks rendered in them. Where I have arrow click events for right, left, up, and down arrow. I need to add the mobile touch support for those arrow clicks when it's accessed only on mobile devices. I have never implemented these touch/swipe mobile features hence it seems very confusing. Any help would be appreciated.
Component:
const Grid = () => {
const [grid, setGrid] = useState(new Grid());
const arrowLeftKey = 37;
const arrowDownKey = 40;
const handleKeyDown = (event) => {
if (grid.hasWon()) {
return;
}
if (event.keyCode >= arrowLeftKey && event.keyCode <= arrowDownKey) {
let direction = event.keyCode - arrowLeftKey;
let gridClone = Object.assign(
Object.create(Object.getPrototypeOf(grid)),
grid
);
let newGrid = gridClone.move(direction);
setGrid(newGrid);
}
};
useArrowKeyEvent('keydown',handleKeyDown); //hook
const displayBlocks = grid.cells.map((row, rowIndex) => {
return (
<div key={rowIndex}>
{row.map((col, colIndex) => {
return <Block key={rowIndex + colIndex} />;
})}
</div>
);
});
return (
<div className="grid" id="gridId">
{displayBlocks}
</div>
);
I came to know from googling that I would need to use Touch Events, such as touchStart, touchMove, touchEnd. Looking at the touchevents documentation I added the following piece of code to my component. I changed the MouseEvents to ´KeyBoardevent´. Since it's a arrow key click/keydown event. But this is not working. Not sure where am I doing wrong.
const onTouch = (evt) => {
evt.preventDefault();
if (evt.touches.length > 1 || (evt.type === "touchend" && evt.touches.length > 0))
return;
var newEvt = document.createEvent("KeyboardEvent");
var type = null;
var touch = null;
// eslint-disable-next-line default-case
switch (evt.type) {
case "touchstart":
type = "keydown";
touch = evt.changedTouches[0];
break;
case "touchmove":
type = "keydown";
touch = evt.changedTouches[0];
break;
case "touchend":
type = "keydown";
touch = evt.changedTouches[0];
break;
}
newEvt.initEvent(type, true, true, evt.originalTarget.ownerDocument.defaultView, 0,
touch.screenX, touch.screenY, touch.clientX, touch.clientY,
evt.keyCode('37'), evt.keyCode('39'), evt.keyCode('38'), evt.keyCode('40'), 0, null);
evt.originalTarget.dispatchEvent(newEvt);
}
document.addEventListener("touchstart", onTouch, true);
document.addEventListener("touchmove", onTouch, true);
document.addEventListener("touchend", onTouch, true);
I get the following error when I swipe right and expect for right arrow click:
TypeError: Cannot read property 'ownerDocument' of undefined
On the following line of code:
newEvt.initEvent(type, true, true, evt.originalTarget.ownerDocument.defaultView, 0,
touch.screenX, touch.screenY, touch.clientX, touch.clientY,
evt.keyCode('37'), evt.keyCode('39'), evt.keyCode('38'), evt.keyCode('40'), 0, null);
Version 2
Edit: : used react-swipeable after #sschwei1 suggested
I have added the following piece in the component :
const swipeHandlers = useSwipeable({
onSwipedLeft: useArrowKeyEvent('keydown',handleKeyDown),<<<<<problem
onSwipedRight: eventData => console.log("swiped right"),
onSwipedUp: eventData => console.log("swiped up"),
onSwipedDown: eventData => console.log("swiped down")
});
and the return statement:
<div className="grid" {...swipeHandlers}>
{displayBlocks}
</div>
Problem: Can't use the hook as callback function.

React-swipeable is a library which handles swipes for you, it enables you to create handlers for different swipe directions, e.g onSwipedLeft or onSwipedUp and pretty much all other cases you can think of like onTap, onSwiping, onSwiped, and many more.
In these handlers you can just re-use the logic of your arrow keys.
The first solution I would think of (not the prettiest solution, but easy to use and understand) to create a wrapper function for swipes and call the according keyHandler function to it
Here is an example of how these functions could look like:
const handleTouch = (key) => {
handleKeyDown({keyCode:key});
}
And in your touch handlers you can call this function with the according key
const swipeHandlers = useSwipeable({
onSwipedLeft: () => handleTouch('37'),
onSwipedUp: () => handleTouch('38'),
onSwipedRight: () => handleTouch('39'),
onSwipedDown: () => handleTouch('40')
});
Since you are only using the keyCode in your handleKeyDown function, you can just pass an object with the keyCode property to the function and 'simulate' the key press

Related

How to make a "mouseleave" analogue for a "touch" event?

How to make the touchMove event be interrupted if the finger goes beyond the bounds of the object to which the event is attached? And when interrupting, call another function.
I assume that I need to somehow determine the location of the object on which the event occurs and when exiting these coordinates somehow interrupt the event. But I can't find how to do this in React using useRef and how to interrupt the event.
const Scrollable = (props) => {
const items = props.items;
let ref = useRef();
const touchStarts = (e) => {...}
const touchEnd = (e) => {...}
const touchMove = (e) => {
if (ref && ref.current && !ref.current.contains(e.target)) {
return;
}
e.preventDefault();
...
}
useEffect(() => {
document.addEventListener("touchmove", touchMove);
...
return () => {
document.removeEventListener("touchmove", touchMove);
...
};
});
return (
<div>
<div
ref={ref}
onTouchStart={touchStarts}
onTouchMove={touchMove}
onTouchEnd={touchEnd}
>
</div>
</div>
);
}
const touchMove =(e) => {
//Inside the touch event, we use the ref hook to take the coordinates of the object.
let posa = ref.current.getBoundingClientRect();
//Next, we check the coordinates of the touch using clientX / Y checking that the touch is inside the object.
if(e.touches[0].clientX > posa.left &&
e.touches[0].clientX < posa.right &&
e.touches[0].clientY > posa.top &&
e.touches[0].clientY < posa.bottom ) {
ourFunc()
}
//If the condition is not met, we call the function that should be triggered when the finger is released.
else {
stopFunc()
}

WKWebView - prevent automatic scrolling triggered by user text selection

When a user performs a tap and hold gesture to select a word and then drags their finger towards either the top or bottom edges of the screen, the page automatically scrolls in order to accommodate the selection.
here is a short clip demonstrating it
I would like to prevent this behavior inside a WKWebView.
Here is what I have tried so far:
in a bridge.js file which is accessible to the webview:
var shouldAllowScrolling = true;
document.addEventListener('selectionchange', e => {
shouldAllowScrolling = getSelectedText().length === 0;
window.webkit.messageHandlers.selectionChangeHandler.postMessage(
{
shouldAllowScrolling: shouldAllowScrolling
});
console.log('allow scrolling = ', shouldAllowScrolling);
});
and then in a WKScriptMessageHandler implementation:
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
{
switch message.name
{
case "selectionChangeHandler":
let params = paramsDictionary(fromMessageBody: message.body)
let shouldEnableScrolling = params["shouldAllowScrolling"] as? Bool ?? true
cell?.webView.scrollView.isScrollEnabled = shouldEnableScrolling
cell?.webView.scrollView.isUserInteractionEnabled = shouldEnableScrolling // not together with the line above
default:
fatalError("\(#function): received undefined message handler name: \(message.name)")
}
}
Similarly, I have tried calling the preventDefault() function directly in the javascript file for a bunch of events, namely scroll and touchmove, like so:
document.addEventListener('touchmove', e => {
if (!shouldAllowScrolling) {
e.preventDefault()
}
}, {passive: false});
both methods successfully prevent scrolling when some text is selected but do not override the behavior described at the very top of my question.
I can accept solutions in either Swift and JavaScript or a mix of both.
I ended up solving this problem by saving the last scroll position and scrolling to it when appropriate, like so:
var shouldAllowScrolling = true;
var lastSavedScrollLeft = 0;
var lastSavedScrollTop = 0;
function saveScrollPosition() {
lastSavedScrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
lastSavedScrollTop = window.pageYOffset || document.documentElement.scrollTop;
}
document.addEventListener('touchstart', e => {
saveScrollPosition();
});
document.addEventListener('touchend', () => {
// enable scrolling when the user lifts their finger, to allow scrolling while text selection is still present
shouldAllowScrolling = true;
});
document.addEventListener('scroll', e => {
if (!shouldAllowScrolling) {
window.scrollTo(lastSavedScrollLeft, lastSavedScrollTop);
}
});
document.addEventListener('selectionchange', e => {
shouldAllowScrolling = getSelectedText().length === 0;
});
If someone can offer a more elegant solution that prevents the scrolling entirely ill be happy to accept it.
EDIT:
this solution may cause light shaking/jittering.
that can be solved by disabling the scroll natively while shouldAllowScrolling is set to false, like so:
webView.scrollView.isScrollEnabled = false

Bluetooth headphones button event detection in javascript

I am building a web app where I detect the headphones button event. I succeeded in capturing headphones button event when they are plugged in. Now I am trying to capture Bluetooth headphones next button event. Any help on this please?
Code for headphone button detection.
document.addEventListener('volumeupbutton', () => {
//Do something here
}, false);
I need something similar to this.
You can use keydown and keyup events for implementing the long press functionality.
// Imprementation of Long Press
const longPressTime = 1500;
let keyDownTimeout;
document.addEventListener('keydown', e => {
if (keyDownTimeout) {
return;
}
keyDownTimeout = setTimeout(() => {
// button was held for 1500ms, consider it a long-press
if (e.code === 'ArrowUp') {
console.log("Action Performed");
// do long-press action
} else {
console.log("Other action performed");
}
}, longPressTime);
});
document.addEventListener('keyup', e => {
clearTimeout(keyDownTimeout);
keyDownTimeout = 0;
});
Press any key
The above methods work for single key long press. Refer to KeyCode for key code.
Demo of above
I don't believe using the built-in volumeupbutton event will allow you to detect how long the click was, to determine if it should be treated as volume-up or skip-track. Instead you should be able to use the keyup/keydown events, combined with the keyCode property to determine if it is the volume button, like this:
const longPressTime = 1500;
let volumeUpButtonTimeout;
const volumeButtonKeyCode = 0; // you'll need to determine the key code
// cross platform way to get the key code
const getKeyCode = e => {
if (e.key !== undefined) {
return e.key;
} else if (e.keyIdentifier !== undefined) {
return e.keyIdentifier;
} else if (e.keyCode !== undefined) {
return e.keyCode;
}
}
document.addEventListener('keydown', e => {
if (getKeyCode(e) == volumeButtonKeyCode) {
volumeUpButtonTimeout = setTimeout(() => {
// button was held for 1500ms, consider it a long-press
// do long-press action
}, longPressTime)
}
});
document.addEventListener('keyup', e => {
if (getKeyCode(e) == volumeButtonKeyCode) {
clearTimeout(volumeUpButtonTimeout);
}
});
You could use this code to determine what keyCode corresponds to the volume up button:
document.addEventListener('keyup', e => {
console.log(e.keyCode);
});

Why does Javascript drop keyUp events when the metaKey is pressed on Mac browsers?

On Mac browsers, javascript does not receive keyup events for most keys (other modifier keys seem to be an exception) when the metakey is down. Use this jsfiddle to demonstrate (focus the result area and try something like cmd + x, the x will not receive a keyup event):
http://jsfiddle.net/mUEaV/
I've reproduced this in stable releases for Chrome, FF, Safari and Opera. The same thing does not seem to happen with the control key in Windows 7.
Is the OS hijacking the keyup event? This seems especially strange since commands that use the metakey such as save, find, cut, copy, etcetera all activate on keydown not on keyup, and can be hijacked by the javascript just fine.
It's simply not possible to get the onKeyUp events when meta is used, I learned today. Very unfortunate and difficult to work around. You'll have to emulate them some other way.
Edit: To clarify, this is only on Mac and occurs due to OS level handling of the event. It cannot be overridden. Sorry to be the bearer of bad news.
Although event.metaKey returns false, event.keyCode and event.key are still populated.
document.addEventListener('keyup', function(e) {
console.log(e.metaKey || e.key);
});
Click here then press the Command, Control, or Option keys.
Is the browser window retaining the focus when you press those keys? In windows you can get similar result when pressing windows+R or CTRL+ESC and similar key combinations that make browser to loose focus and that results in missed events.
While keyup events are indeed not available when the meta key is pressed, you can still get keydown events for all keys, as well as keyup events for the meta key itself.
This allows us to just simply keep track of the state of the meta key ourselves, like so:
let metaKeyDown = false;
window.addEventListener("keydown", event => {
if (event.key == 'Meta') { metaKeyDown = true; }
});
window.addEventListener("keyup", event => {
if (event.key == 'Meta') { metaKeyDown = false; }
});
By now additionally checking for the main key, plus cancelling the default behavior with Event.preventDefault() we can easily listen for key combinations (like here e.g. CMD+K) and prevent the browser from handling them:
let metaKeyDown = false;
window.addEventListener("keydown", event => {
if (event.key == 'Meta') { metaKeyDown = true; }
if (event.key == 'k' && metaKeyDown) {
event.preventDefault();
console.log('CMD+K pressed!');
}
});
window.addEventListener("keyup", event => {
if (event.key == 'Meta') { metaKeyDown = false; }
});
(Note the observation of the k key taking place already on keydown.)
Also, please be aware that when used incorrectly, this can break standard browser functionality (e.g. like CMD+C or CMD+R), and lead to poor user experience.
You can create an artificial keyup event by waiting for a certain period after the last keydown event. The only caveat is people will have different repeat rates on their os.
https://jsfiddle.net/u7t43coz/10/
const metaKeyCodes = ["MetaLeft", "MetaRight"];
const shiftKeyCodes = ["ShiftLeft", "ShiftRight"];
const ctrlKeyCodes = ["ControlLeft", "ControlRight"];
const altKeyCodes = ["AltLeft", "AltRight"];
const modifierKeyCodes = [
...metaKeyCodes,
...shiftKeyCodes,
...ctrlKeyCodes,
...altKeyCodes
];
// record which keys are down
const downKeys = new Set()
const artificialKeyUpTimes = {}
function onKeydown(e) {
downKeys.add(e.code);
// do other keydown stuff here
console.log("meta", e.metaKey, e.code, "down")
// check if metaKey is down
if (metaKeyCodes.some(k => downKeys.has(k))) {
downKeys.forEach(dk => {
// we want to exclude modifier keys has they dont repeat
if (!modifierKeyCodes.includes(dk)) {
// fire artificial keyup on timeout
if (!artificialKeyUpTimes[dk])
setTimeout(
() => fireArtificialKeyUp(dk, e),
500
);
artificialKeyUpTimes[dk] = Date.now();
}
});
}
}
function fireArtificialKeyUp(code, e) {
// if enough time has passed fire keyup
if (Date.now() - artificialKeyUpTimes[code] > 100) {
delete artificialKeyUpTimes[code];
//if key is still down, fire keyup
if (downKeys.has(code)) {
const eCode = isNaN(code) ? { code: code } : { keyCode: code };
document.dispatchEvent(
new KeyboardEvent("keyup", { ...e, ...eCode })
);
}
} else {
setTimeout(() => fireArtificialKeyUp(code, e), 100);
}
}
function onKeyup(e) {
downKeys.delete(e.code);
// do keyup stuff here
console.log("meta", e.metaKey, e.code, "up")
}
document.addEventListener("keydown", onKeydown)
document.addEventListener("keyup", onKeyup)

Disable arrow key scrolling in users browser

I'm making a game using canvas, and javascript.
When the page is longer than the screen (comments, etc.) pressing the down arrow scrolls the page down, and makes the game impossible to play.
What can I do to prevent the window from scrolling when the player just wants to move down?
I guess with Java games, and such, this is not a problem, as long as the user clicks on the game.
I tried the solution from: How to disable page scrolling in FF with arrow keys ,but I couldn't get it to work.
Summary
Simply prevent the default browser action:
window.addEventListener("keydown", function(e) {
if(["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].indexOf(e.code) > -1) {
e.preventDefault();
}
}, false);
If you need to support Internet Explorer or other older browsers, use e.keyCode instead of e.code, but keep in mind that keyCode is deprecated and you need to use actual codes instead of strings:
// Deprecated code!
window.addEventListener("keydown", function(e) {
// space and arrow keys
if([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) {
e.preventDefault();
}
}, false);
Original answer
I used the following function in my own game:
var keys = {};
window.addEventListener("keydown",
function(e){
keys[e.code] = true;
switch(e.code){
case "ArrowUp": case "ArrowDown": case "ArrowLeft": case "ArrowRight":
case "Space": e.preventDefault(); break;
default: break; // do not block other keys
}
},
false);
window.addEventListener('keyup',
function(e){
keys[e.code] = false;
},
false);
The magic happens in e.preventDefault();. This will block the default action of the event, in this case moving the viewpoint of the browser.
If you don't need the current button states you can simply drop keys and just discard the default action on the arrow keys:
var arrow_keys_handler = function(e) {
switch(e.code){
case "ArrowUp": case "ArrowDown": case "ArrowLeft": case "ArrowRight":
case "Space": e.preventDefault(); break;
default: break; // do not block other keys
}
};
window.addEventListener("keydown", arrow_keys_handler, false);
Note that this approach also enables you to remove the event handler later if you need to re-enable arrow key scrolling:
window.removeEventListener("keydown", arrow_keys_handler, false);
References
MDN: window.addEventListener
MDN: window.removeEventListener
MDN: KeyboardEvent.code interface
For maintainability, I would attach the "blocking" handler on the element itself (in your case, the canvas).
theCanvas.onkeydown = function (e) {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.view.event.preventDefault();
}
}
Why not simply do window.event.preventDefault()? MDN states:
window.event is a proprietary Microsoft Internet Explorer property
which is only available while a DOM event handler is being called. Its
value is the Event object currently being handled.
Further readings:
https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/view
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
I've tried different ways of blocking scrolling when the arrow keys are pressed, both jQuery and native Javascript - they all work fine in Firefox, but don't work in recent versions of Chrome.
Even the explicit {passive: false} property for window.addEventListener, which is recommended as the only working solution, for example here.
In the end, after many tries, I found a way that works for me in both Firefox and Chrome:
window.addEventListener('keydown', (e) => {
if (e.target.localName != 'input') { // if you need to filter <input> elements
switch (e.keyCode) {
case 37: // left
case 39: // right
e.preventDefault();
break;
case 38: // up
case 40: // down
e.preventDefault();
break;
default:
break;
}
}
}, {
capture: true, // this disables arrow key scrolling in modern Chrome
passive: false // this is optional, my code works without it
});
Quote for EventTarget.addEventListener() from MDN
options Optional
   An options object specifies characteristics about the event listener. The available options are:
capture
   A Boolean indicating that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree.
once
   ...
passive
   A Boolean that, if true, indicates that the function specified by listener will never call preventDefault(). If a passive listener does call preventDefault(), the user agent will do nothing other than generate a console warning. ...
This is the accepted answer rewritten for React.
import { useEffect } from "react";
const usePreventKeyboardScrolling = () => {
const onKeyDown = (e) => {
if (
["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].indexOf(
e.code
) > -1
) {
e.preventDefault();
}
};
useEffect(() => {
window.addEventListener("keydown", onKeyDown, false);
return () => window.removeEventListener("keydown", onKeyDown);
});
};
export { usePreventKeyboardScrolling };

Categories

Resources