Reactjs onPointer event not functioning properly on mobile browsers - reactjs

I am trying to make a finger-painting canvas, which should work on mobile browsers.
My canvas is working properly on pc browsers (by mouse), but not on mobile browsers. The strokes just disappear even if my finger did not leave the screen.
Is it because the swipe gestures in Chrome (which I am using) overwrite the onPointerMove event on some time point?
You can try my canvas here (my server is not always on). The whole project contains too much code unrelated to this issue so I am not posting those code here.
And my code for Canvas.js is like this:
// canvas.js
// modified from https://pusher.com/tutorials/live-paint-react
import React, { Component } from 'react';
class Canvas extends Component {
constructor(props) {
super(props);
this.onPointerDown = this.onPointerDown.bind(this);
this.onPointerMove = this.onPointerMove.bind(this);
this.endPaintEvent = this.endPaintEvent.bind(this);
}
isPainting = false;
userStrokeStyle = '#EE92C2';
line = [];
prevPos = { offsetX: 0, offsetY: 0 };
onPointerDown({ nativeEvent }) {
const { offsetX, offsetY } = nativeEvent;
this.isPainting = true;
this.prevPos = { offsetX, offsetY };
}
onPointerMove({ nativeEvent }) {
if (this.isPainting) {
const { offsetX, offsetY } = nativeEvent;
const offSetData = { offsetX, offsetY };
// Set the start and stop position of the paint event.
const positionData = {
start: { ...this.prevPos },
stop: { ...offSetData },
};
// Add the position to the line array
this.line = this.line.concat(positionData);
this.paint(this.prevPos, offSetData, this.userStrokeStyle);
}
}
endPaintEvent() {
if (this.isPainting) {
this.isPainting = false;
//this.sendPaintData();
}
}
paint(prevPos, currPos, strokeStyle) {
const { offsetX, offsetY } = currPos;
const { offsetX: x, offsetY: y } = prevPos;
this.ctx.beginPath();
this.ctx.strokeStyle = strokeStyle;
// Move the the prevPosition of the mouse
this.ctx.moveTo(x, y);
// Draw a line to the current position of the mouse
this.ctx.lineTo(offsetX, offsetY);
// Visualize the line using the strokeStyle
this.ctx.stroke();
this.prevPos = { offsetX, offsetY };
}
componentDidMount() {
// Here we set up the properties of the canvas element.
this.canvas.width = 1000;
this.canvas.height = 800;
this.ctx = this.canvas.getContext('2d');
this.ctx.lineJoin = 'round';
this.ctx.lineCap = 'round';
this.ctx.lineWidth = 5;
}
render() {
return (
<canvas
// We use the ref attribute to get direct access to the canvas element.
ref={(ref) => (this.canvas = ref)}
style={{ background: 'black' }}
onPointerDown={this.onPointerDown}
onPointerUp={this.endPaintEvent}
onPointerMove={this.onPointerMove}
/>
);
}
}
export default Canvas;
I tried to disable the scrolling and zooming by these code:
index.css
html, body {
overflow-x:hidden;
overflow-y:hidden;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
position: relative;
}
index.html
<script>
document.body.addEventListener("touchstart", function (e) {
if (e.target == canvas) {
e.preventDefault();
}
}, false);
document.body.addEventListener("touchend", function (e) {
if (e.target == canvas) {
e.preventDefault();
}
}, false);
document.body.addEventListener("touchmove", function (e) {
if (e.target == canvas) {
e.preventDefault();
}
}, false);
</script>
And I change onPointer events to onTouch events (simply change onPointer to onTouch
e.g. onPointerDown -> onTouchStart
But nothing happened when I draw on the canvas, I am not sure whether I am implementing onTouch events correctly.

Related

Trouble with drag functionality in an image slider (React, Next)

I'm trying to recreate a JS draggable slider in React/Next from this codepen but I just can't make it slide on mouse drag. Nothing happens on first click, but at the mouseUp event, the slider starts to slide and it won't stop until you reload the page.
Here is my codesandbox so far.
And here is the original from codepen:
const track = document.getElementById("image-track");
const handleOnDown = e => track.dataset.mouseDownAt = e.clientX;
const handleOnUp = () => {
track.dataset.mouseDownAt = "0";
track.dataset.prevPercentage = track.dataset.percentage;
}
const handleOnMove = e => {
if(track.dataset.mouseDownAt === "0") return;
const mouseDelta = parseFloat(track.dataset.mouseDownAt) - e.clientX,
maxDelta = window.innerWidth / 2;
const percentage = (mouseDelta / maxDelta) * -100,
nextPercentageUnconstrained = parseFloat(track.dataset.prevPercentage) + percentage,
nextPercentage = Math.max(Math.min(nextPercentageUnconstrained, 0), -100);
track.dataset.percentage = nextPercentage;
track.animate({
transform: `translate(${nextPercentage}%, -50%)`
}, { duration: 1200, fill: "forwards" });
for(const image of track.getElementsByClassName("image")) {
image.animate({
objectPosition: `${100 + nextPercentage}% center`
}, { duration: 1200, fill: "forwards" });
}
}
I just figured it out. I was using onClick to activate dragging functionality, which was wrong. The correct event is onMouseDown.

Multiple Shape Selection for resize in React-konva

I am working on drawing app project where I am using react.js and react-konva. Now I am trying to add multiple shape selection for resizing feature. I find a codesandbox example but when I am trying to run the code on my browser adding the code in my project, I am not getting expected output that is shown in codesandbox output. Only the last element is resizing, not the previous elements. and also cannot able to add cntrl and shift key button selection feature as well.
The example is - https://codesandbox.io/s/react-konva-multiple-selection-forked-wysowd?file=/src/index.js:6016-6045
I am sharing the code below of App.js:
import "./App.css";
import { Layer, Rect, Stage, Transformer } from "react-konva";
import { useEffect, useRef, useState } from "react";
import Rectangle from "./Rectangle";
import { v4 as uuidv4 } from 'uuid';
const initialRectangle = [
{
x: 10,
y: 10,
width: 100,
height: 100,
fill: "red",
id: uuidv4(),
},
{
x: 150,
y: 150,
width: 100,
height: 100,
fill: "green",
id: uuidv4(),
},
{
x: 300,
y: 300,
width: 100,
height: 100,
fill: "blue",
id: uuidv4(),
}
];
function App() {
const selectionRect = useRef(null);
const [rectangles, setRectangles] = useState(initialRectangle);
const [nodesArray, setNodes] = useState([]);
const trRef = useRef(null);
const [selectedId, selectShape] = useState(null);
const layerRef = useRef(null);
const Konva = window.Konva;
const selection = useRef({
visible: false,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
});
const updateSelection = () => {
const node = selectionRect.current;
node.setAttrs({
visible: selection.current.visible,
x: Math.min(selection.current.x1, selection.current.x2),
y: Math.min(selection.current.y1, selection.current.y2),
width: Math.abs(selection.current.x1 - selection.current.x2),
height: Math.abs(selection.current.y1 - selection.current.y2),
fill: "rgba(0, 161, 255, 0.3)",
});
};
const onMouseDown = (e) => {
const isElement = e.target.findAncestor(".elements-container");
const isTransformer = e.target.findAncestor("Transformer");
if (isElement || isTransformer) {
return;
}
const pos = e.target.getStage().getPointerPosition();
selection.current.x1 = pos.x;
selection.current.y1 = pos.y;
selection.current.x2 = pos.x;
selection.current.y2 = pos.y;
selection.current.visible = true;
updateSelection();
};
const onMouseMove = (e) => {
if (!selection.current.visible) {
return;
}
const pos = e.target.getStage().getPointerPosition();
selection.current.x2 = pos.x;
selection.current.y2 = pos.y;
updateSelection();
};
const onMouseUp = (e) => {
if (!selection.current.visible) {
return;
}
const selBox = selectionRect.current.getClientRect();
const elements = [];
// console.log({layerRef});
layerRef.current.find(".rectangle").forEach((node) => {
const nodeBox = node.getClientRect();
if (Konva.Util.haveIntersection(selBox, nodeBox)) {
elements.push(node);
}
});
trRef.current.nodes(elements);
console.log({trRef})
selection.current.visible = false;
Konva.listenClickTap = false;
updateSelection();
};
const onClickTap = (e) => {
if (selection.current.visible) {
return;
}
let stage = e.target.getStage();
let layer = layerRef.current;
let tr = trRef.current;
if (e.target === stage) {
selectShape(null);
tr.nodes([]);
setNodes([]);
layer.batchDraw();
return;
}
if (!e.target.hasName("rectangle")) {
return;
}
const metaPressed = e.evt.ctrlKey || e.evt.shiftKey;
// console.log({nodesArray});
const isSelected = tr.nodes().indexOf(e.target) >= 0;
// console.log({isSelected})
// console.log(tr.nodes().indexOf(e.target));
// console.log(metaPressed, isSelected);
// if (!metaPressed && !isSelected) {
// selectShape(e.target.id());
// tr.nodes([e.target]);
// setNodes([e.target]);
// } else if (metaPressed && !isSelected) {
// selectShape(e.target.id());
// tr.nodes([...tr.nodes(), e.target]);
// setNodes([...nodesArray, e.target]);
// } else if (metaPressed && isSelected) {
// selectShape(null);
// const nodes = tr.nodes();
// console.log(nodes);
// const index = nodes.indexOf(e.target);
// nodes.splice(index, 1);
// setNodes(nodes);
// tr.nodes(nodes);
// }
if (!metaPressed && !isSelected) {
// if no key pressed and the node is not selected
// select just one
tr.nodes([e.target]);
} else if (metaPressed && isSelected) {
// if we pressed keys and node was selected
// we need to remove it from selection:
const nodes = tr.nodes().slice(); // use slice to have new copy of array
// remove node from array
nodes.splice(nodes.indexOf(e.target), 1);
tr.nodes(nodes);
} else if (metaPressed && !isSelected) {
// add the node into selection
const nodes = tr.nodes().concat([e.target]);
tr.nodes(nodes);
}
trRef.current.getLayer().batchDraw();
};
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
onClick={onClickTap}
>
<Layer ref={layerRef}>
{rectangles.map((rect, i) => (
<Rectangle
key={i}
shapeProps={rect}
isSelected={rect.id === selectedId}
onSelect={(e) => {
// if (e.current !== undefined) {
// let temp = nodesArray;
// if (!nodesArray.includes(e.current)) temp.push(e.current);
// setNodes(temp);
// trRef.current.nodes(nodesArray);
// trRef.current.getLayer().batchDraw();
// }
selectShape(rect.id);
}}
onChange={(newAttrs) => {
const copyOfRectangles = JSON.parse(JSON.stringify(rectangles));
const currentIndex = rectangles.findIndex(item => item.id === newAttrs.id);
copyOfRectangles[currentIndex] = newAttrs;
setRectangles(copyOfRectangles);
console.log(copyOfRectangles);
}}
/>
))}
<Transformer
ref={trRef}
boundBoxFunc={(oldBox, newBox) => {
if (newBox.width < 5 || newBox.height < 5) {
return oldBox;
}
return newBox;
}}
/>
<Rect fill="blue" ref={selectionRect} />
</Layer>
</Stage>
);
}
export default App;
the below code is Rectangle component -
import React from 'react'
import { Rect } from 'react-konva';
const Rectangle = ({shapeProps, onSelect, onChange}) => {
const shapeRef = React.useRef(null);
return (
<Rect
{...shapeProps}
onClick={() => onSelect(shapeRef)}
// onDragStart={() => onChange(shapeRef)}
onDragEnd={(e) => onChange({
...shapeProps,
x: e.target.x(),
y: e.target.y(),
})}
ref={shapeRef}
onTap={() => onSelect(shapeRef)}
name="rectangle"
draggable
onTransformEnd={(e) => {
const node = shapeRef.current;
const scaleX = node.scaleX();
const scaleY = node.scaleY();
// we will reset it back
node.scaleX(1);
node.scaleY(1);
onChange({
...shapeProps,
x: node.x(),
y: node.y(),
// set minimal value
width: Math.max(5, node.width() * scaleX),
height: node.height() * scaleY
});
}
}
/>
)
}
export default Rectangle
What is the problem I don't understand.

Problem with selecting an area using leaflet

so the problem is that when i'm trying to select an area using leaflet and click somewhere outside of selection tab it removes focus from the selection and after trying to select it doesn't work as intended meaning that i can't select properly
how do i return focus back to the tab?
appreciate any suggestions, if need to clarify something - tell me about it
here are the screenshots of how it should be and how it actually is
EDIT: here's code
import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import L, { FeatureGroup, LatLngBounds, LeafletEvent, LeafletEventHandlerFn, LeafletMouseEvent, Point, PointTuple, Map } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet-area-select';
import { Box, IconButton } from '#material-ui/core';
import GpsNotFixedRoundedIcon from '#material-ui/icons/GpsNotFixedRounded';
import { useStyles } from './styles';
import { disableMap, drawGrid } from '../../../utils/MapHelpers';
import { NewObs, Observation, Panorama } from '../../../state/reducers/types';
import { clearObs, saveGridSize, saveObsBounds, selectObsArea } from '../../../state/actions/inspector';
import OrientationMap from '../OrientationMap';
export type Grid = { gridCoordX: number[]; gridCoordY: number[]; gridLayer: FeatureGroup };
export let inspMap: Map;
interface AreaSelectEvent extends LeafletEvent {
bounds: LatLngBounds;
}
interface ILeafletWrapper {
handleObsForm: (showObsForm: boolean) => void;
handleIsSelectingObs: (isSelecting: boolean) => void;
handleOrientationMap: () => void;
handleGridSizeSelecting: () => void;
isSelectingObs: boolean;
newObs: NewObs;
observations: Observation[];
showOrientationMap: boolean;
isSelectingGridSize: boolean;
panorama: Panorama;
}
export const LeafletWrapper = (props: ILeafletWrapper) => {
const {
isSelectingObs,
handleObsForm,
handleIsSelectingObs,
observations,
showOrientationMap,
handleOrientationMap,
isSelectingGridSize,
handleGridSizeSelecting,
panorama,
} = props;
const { urlTemplates, originalResolution, originalZoomLevel, gridInfo } = panorama;
const [{ gridCoordX, gridCoordY, gridLayer }, setGrid] = useState<Grid>({
gridCoordX: [0],
gridCoordY: [0],
gridLayer: {},
} as Grid);
const [gridSizeBounds, setGridSizeBounds] = useState<LatLngBounds | undefined>();
const classes = useStyles();
const dispatch = useDispatch();
// initialization of Leaflet
useEffect(() => {
inspMap = new L.Map('map', { zoomControl: false, maxBoundsViscosity: 0.9 });
inspMap.setView(inspMap.unproject([originalResolution.x / 2, originalResolution.y / 2], originalZoomLevel), originalZoomLevel);
// you just need to pass something as url, further work with url is rewritten
const TileLayer = L.tileLayer('url', {
// lock to only zoom level for first version of app
minZoom: originalZoomLevel,
maxZoom: originalZoomLevel,
noWrap: true,
tileSize: 8192,
});
// custom internal method to get tile's url, have to use this method due to the inability of
// generating a templated URL from a predefined URLs (they are all unique)
TileLayer.getTileUrl = ({ z, x, y }: { z: number; x: number; y: number }) => {
return urlTemplates[y + '-' + x];
};
TileLayer.addTo(inspMap);
// disable the need to use a ctrl btn to select area
inspMap.selectArea.setControlKey(false);
// draw grid if there is cached panoramas info
if (gridInfo.x) {
const bounds = L.latLngBounds(
inspMap.unproject([0, 0], originalZoomLevel),
inspMap.unproject([gridInfo.x, gridInfo.y], originalZoomLevel),
);
const gridObj = drawGrid(bounds, inspMap, originalZoomLevel, originalResolution);
setGrid(gridObj);
setGridSizeBounds(bounds);
}
return () => {
inspMap.remove();
dispatch(clearObs());
};
}, []);
// disable interactions with leaflet while selectArea is enabled
useEffect(() => {
disableMap(isSelectingObs, inspMap);
if ((isSelectingObs || isSelectingGridSize) && !inspMap.selectArea.enabled()) {
inspMap.selectArea.enable();
inspMap.getContainer().style.cursor = 'crosshair';
inspMap.on('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
} else {
inspMap.getContainer().style.cursor = 'grab';
}
}, [isSelectingObs, isSelectingGridSize]);
// reset grid
useEffect(() => {
if (isSelectingGridSize && gridLayer?.remove) {
gridLayer.remove();
setGridSizeBounds(undefined);
}
}, [isSelectingGridSize, gridLayer]);
// draw observations
useEffect(() => {
// eslint-disable-next-line
observations.map((obs: any) => {
const index = obs.id;
if (obs.position.bounds) return;
const bounds = L.latLngBounds(
inspMap.unproject(obs.position.leftTopPoint, originalZoomLevel),
inspMap.unproject(obs.position.rightBottomPoint, originalZoomLevel),
);
const rectangle = L.rectangle(bounds, { color: '#ff9200', weight: 2, fillOpacity: 0 }).addTo(inspMap);
dispatch(saveObsBounds({ bounds: { remove: rectangle.remove.bind(rectangle) }, index }));
});
}, [observations]);
const handleObsAreaSelected = (e: AreaSelectEvent) => {
if (isSelectingObs) {
const leftTopPoint = inspMap.project(e.bounds.getNorthWest(), originalZoomLevel);
leftTopPoint.x = Math.round(leftTopPoint.x);
leftTopPoint.y = Math.round(leftTopPoint.y);
const rightBottomPoint = inspMap.project(e.bounds.getSouthEast(), originalZoomLevel);
rightBottomPoint.x = Math.round(rightBottomPoint.x);
rightBottomPoint.y = Math.round(rightBottomPoint.y);
const bounds = L.rectangle(e.bounds, { color: '#ff9200', weight: 2, fillOpacity: 0 });
bounds.addTo(inspMap);
dispatch(selectObsArea({ leftTopPoint, rightBottomPoint, bounds: { remove: bounds.remove.bind(bounds) } }));
handleObsForm(true);
handleIsSelectingObs(false);
inspMap.selectArea.disable();
inspMap.removeEventListener('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
}
if (isSelectingGridSize) {
handleGridSizeSelecting();
const gridObj = drawGrid(e.bounds, inspMap, originalZoomLevel, originalResolution);
setGrid(gridObj);
setGridSizeBounds(e.bounds);
inspMap.selectArea.disable();
inspMap.removeEventListener('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
dispatch(saveGridSize({ x: gridObj.gridCoordX[0], y: gridObj.gridCoordY[0] }));
}
};
const panToCell = ({ latlng }: LeafletMouseEvent) => {
// user click coordinates converted to pixel relative coordinate system for original zoom level
const point: Point = inspMap.project(latlng, originalZoomLevel);
const cellCenter = [];
// find center of cell
for (let i = 0; i < gridCoordX.length; i++) {
if (point.x < gridCoordX[i]) {
cellCenter.push(gridCoordX[0] / 2);
break;
}
if (point.x > gridCoordX[i] && (point.x < gridCoordX[i + 1] || i === gridCoordX.length - 1)) {
cellCenter.push(gridCoordX[i] + gridCoordX[0] / 2);
break;
}
}
for (let i = 0; i < gridCoordY.length; i++) {
if (point.y < gridCoordY[i]) {
cellCenter.push(gridCoordY[0] / 2);
break;
}
if (point.y > gridCoordY[i] && (point.y < gridCoordY[i + 1] || i === gridCoordY.length - 1)) {
cellCenter.push(gridCoordY[i] + gridCoordY[0] / 2);
break;
}
}
// temporary locked zoom level due only one zoom level available
inspMap.flyTo(inspMap.unproject(cellCenter as PointTuple, originalZoomLevel), originalZoomLevel);
};
const panToCenter = () => inspMap.panTo(inspMap.unproject([originalResolution.x / 2, originalResolution.y / 2], originalZoomLevel));
const zoomIn = () => inspMap.setZoom(inspMap.getZoom() + 1);
const zoomOut = () => inspMap.setZoom(inspMap.getZoom() - 1);
return (
<Box className={classes.root} height="100%">
<div id="map" style={{ height: '100%', width: '100%' }} />
<Box>
<IconButton className={classes.goToCenterBtn} aria-label="delete" onClick={panToCenter}>
<GpsNotFixedRoundedIcon color="inherit" />
</IconButton>
</Box>
<OrientationMap
panToCell={panToCell}
observations={observations}
showOrientationMap={showOrientationMap}
handleOrientationMap={handleOrientationMap}
handlePanToCenter={panToCenter}
zoomIn={zoomIn}
zoomOut={zoomOut}
gridSizeBounds={gridSizeBounds}
panorama={panorama}
/>
</Box>
);
};

Want a button to trigger the drag animation (translateX) for cards example

It is regarding the following example:
https://codesandbox.io/s/cards-forked-4bcix?file=/src/index.js
I want a tinder like functionality where I can trigger the same transition as drag.
I am trying to add like and dislike button functionality like tinder, but since the buttons are not part of the useSprings props loop, it is hard to align which card I should transform. I want the like or dislike button to communicate with useDrag to trigger a drag, I have tried toggling by useState and passing as an argument of useDrag, and onClick handler on button which set(x:1000, y:0) but that removes all of the cards.
Spent a whole day figuring it out, and I need to deliver things very soon, help will be great please!
Below is the code, and I am using Next.js
import React, { useState, useRef } from "react";
import { useSprings, animated } from "react-spring";
import { useDrag } from "react-use-gesture";
const Try: React.SFC = () => {
const cards = [
"https://upload.wikimedia.org/wikipedia/en/5/53/RWS_Tarot_16_Tower.jpg",
"https://upload.wikimedia.org/wikipedia/en/9/9b/RWS_Tarot_07_Chariot.jpg",
"https://upload.wikimedia.org/wikipedia/en/d/db/RWS_Tarot_06_Lovers.jpg",
// "https://upload.wikimedia.org/wikipedia/en/thumb/8/88/RWS_Tarot_02_High_Priestess.jpg/690px-RWS_Tarot_02_High_Priestess.jpg",
"https://upload.wikimedia.org/wikipedia/en/d/de/RWS_Tarot_01_Magician.jpg",
];
const [hasLiked, setHasLiked] = useState(false);
const [props, set] = useSprings(cards.length, (i) => ({
x: 0,
y: 0,
}));
const [gone, setGone] = useState(() => new Set()); // The set flags all the cards that are flicked out
const itemsRef = useRef([]);
const bind = useDrag(
({
args: [index, hasLiked],
down,
movement: [mx],
distance,
direction: [xDir],
velocity,
}) => {
const trigger = velocity > 0.2;
const dir = xDir < 0 ? -1 : 1;
if (!down && trigger) gone.add(index); // If button/finger's up and trigger velocity is reached, we flag the card ready to fly out
set((i) => {
if (index !== i) return; // We're only interested in changing spring-data for the current spring
const isGone = gone.has(index);
const x = isGone ? (200 + window.innerWidth) * dir : down ? mx : 0; // When a card is gone it flys out left or right, otherwise goes back to zero
const rot = mx / 100 + (isGone ? dir * 10 * velocity : 0); // How much the card tilts, flicking it harder makes it rotate faster
const scale = down ? 1.1 : 1; // Active cards lift up a bit
return {
x,
rot,
scale,
delay: undefined,
config: { friction: 50, tension: down ? 800 : isGone ? 200 : 500 },
};
});
if (!down && gone.size === cards.length)
setTimeout(() => gone.clear(), 600);
}
);
console.log(gone);
function handleLikeButtonClick(e) {
gone.add(1);
}
function handleDisLikeButtonClick(e) {
set({ x: -1000, y: 0 });
}
return (
<>
<div id="deckContainer">
{props.map(({ x, y }, i) => (
<animated.div
{...bind(i, hasLiked)}
key={i}
style={{
x,
y,
}}
ref={(el) => (itemsRef.current[i] = el)}
>
<img src={`${cards[i]}`} />
</animated.div>
))}
</div>
<div className="buttonsContainer">
<button onClick={(e) => handleLikeButtonClick(e)}>Like</button>
<button onClick={(e) => handleDisLikeButtonClick(e)}>Dislike</button>
</div>
<style jsx>{`
.buttonsContainer {
background-color: tomato;
}
#deckContainer {
display: flex;
flex-direction: column;
align-items: center;
width: 100vw;
height: 100vh;
position: relative;
}
`}</style>
</>
);
};
export default Try;
done needed to be smart with js:
function handleLikeButtonClick(e) {
let untransformedCards = [];
//loop through all cards and add ones who still have not been transformed
[].forEach.call(itemsRef.current, (p) => {
if (p.style.transform == "none") {
untransformedCards.push(p);
}
});
// add transform property to the latest card
untransformedCards[untransformedCards.length - 1].style.transform =
"translate3d(1000px, 0 ,0)";
}
function handleDisLikeButtonClick(e) {
let untransformedCards = [];
[].forEach.call(itemsRef.current, (p) => {
if (p.style.transform == "none") {
untransformedCards.push(p);
}
});
untransformedCards[untransformedCards.length - 1].style.transform =
"translate3d(-1000px, 0 ,0)";
}

How to refactor React mousemove Class to functional Component?

I have a button which closes a navigation. This button follows the mouse. Everything is working, but I have a depricationwarning, which I wanna get rid of, but don't know exactly how. (I only know that useEffect is playing a certain role:
Here is the class:
import React from "react"
class NavigationCloseMouseButton extends React.Component {
static defaultProps = {
visible: true,
offsetX: 0,
offsetY: 0,
}
state = {
xPosition: 0,
yPosition: 0,
mouseMoved: false,
listenerActive: false,
}
componentDidMount() {
this.addListener()
}
componentDidUpdate() {
this.updateListener()
}
componentWillUnmount() {
this.removeListener()
}
getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
this.setState({
xPosition,
yPosition,
mouseMoved: true,
})
}
addListener = () => {
window.addEventListener("mousemove", this.getTooltipPosition)
this.setState({ listenerActive: true })
}
removeListener = () => {
window.removeEventListener("mousemove", this.getTooltipPosition)
this.setState({ listenerActive: false })
}
updateListener = () => {
if (!this.state.listenerActive && this.props.visible) {
this.addListener()
}
if (this.state.listenerActive && !this.props.visible) {
this.removeListener()
}
}
render() {
return (
<div
onClick={this.props.toggleNavigation}
className="tooltip color-bg"
style={{
display:
this.props.visible && this.state.mouseMoved ? "block" : "none",
opacity: this.props.visible && this.state.mouseMoved ? "1" : "0",
top: this.state.yPosition + this.props.offsetY,
left: this.state.xPosition + this.props.offsetX,
}}
>
Close Menu
</div>
)
}
}
export default NavigationCloseMouseButton
And this is what I've so far, but results with errors:
ReferenceError: getTooltipPosition is not defined
import React, { useState, useEffect } from "react"
const NavigationCloseMouseButton = () => {
const defaults = {
visible: true,
offsetX: 0,
offsetY: 0,
}
const defaultState = {
xPosition: 0,
yPosition: 0,
mouseMoved: false,
listenerActive: false,
}
const [defaultProps, setDefaultProps] = useState(defaults)
const [state, setState] = useState(defaultState)
useEffect(() => {
// Update the document title using the browser API
addListener()
}, [])
getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
setState({
xPosition,
yPosition,
mouseMoved: true,
})
}
addListener = () => {
window.addEventListener("mousemove", getTooltipPosition)
setState({ listenerActive: true })
}
removeListener = () => {
window.removeEventListener("mousemove", getTooltipPosition)
setState({ listenerActive: false })
}
updateListener = () => {
if (!state.listenerActive && props.visible) {
addListener()
}
if (state.listenerActive && !props.visible) {
removeListener()
}
}
return (
<div
onClick={props.toggleNavigation}
className="tooltip color-bg"
style={{
display: props.visible && state.mouseMoved ? "block" : "none",
opacity: props.visible && state.mouseMoved ? "1" : "0",
top: state.yPosition + props.offsetY,
left: state.xPosition + props.offsetX,
}}
>
Close Menu
</div>
)
}
export default NavigationCloseMouseButton
Setting Defaults
You can destructure individual props from the props object (the argument of the function component). While destructuring, you can use the = operator to set a default value for when this prop is not set.
const NavigationCloseMouseButton = ({ visible = true, offsetX = 0, offsetY = 0, toggleNavigation }) => {
Updating a Listener
I'm sure there a lots of great answers about this so I won't go into too much detail.
You want to handle adding and removing the listener from inside your useEffect. You should use a useEffect cleanup function for the final remove. We don't want to be adding and removing the same listener so we can memoize it with useCallback.
I'm not sure what you are trying to do with listenerActive. This could be a prop, but it also seems a bit redundant with visible. I don't know that we need this at all.
Calculating Offset
I also don't know that it makes sense to pass offsetX and offsetY as props. We need the mouse to be on top of the tooltip in order for it to be clickable. We can measure the tooltip div inside this component and deal with it that way.
// ref to DOM node for measuring
const divRef = useRef<HTMLDivElement>(null);
// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);
Animation
Setting the style property display as "block" or "none" makes it hard to do any sort of CSS transition. Instead, I recommend that you handle style switching by changing the className. You could still set display: block and display: none on those classes, but I am choosing to use transform: scale(0); instead.
Code
const NavigationCloseMouseButton = ({
visible = true,
toggleNavigation
}) => {
// state of the movement
const [state, setState] = useState({
xPosition: 0,
yPosition: 0,
mouseMoved: false
});
// memoized event listener
const getTooltipPosition = useCallback(
// plain event, not a React synthetic event
({ clientX: xPosition, clientY: yPosition }) => {
setState({
xPosition,
yPosition,
mouseMoved: true
});
},
[]
); // never re-creates
useEffect(() => {
// don't need to listen when it's not visible
if (visible) {
window.addEventListener("mousemove", getTooltipPosition);
} else {
window.removeEventListener("mousemove", getTooltipPosition);
}
// clean-up function to remove on unmount
return () => {
window.removeEventListener("mousemove", getTooltipPosition);
};
}, [visible, getTooltipPosition]); // re-run the effect if prop `visible` changes
// ref to DOM node for measuring
const divRef = useRef(null);
// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);
// don't show until after mouse is moved
const isVisible = visible && state.mouseMoved;
return (
<div
ref={divRef}
onClick={toggleNavigation}
// control most styling through className
className={`tooltip ${isVisible ? "tooltip-visible" : "tooltip-hidden"}`}
style={{
// need absolute position to use top and left
position: "absolute",
top: state.yPosition + offsetY,
left: state.xPosition + offsetX
}}
>
Close Menu
</div>
);
};
Other Uses
We can easily make this NavigationCloseMouseButton into a more flexible MovingTooltip by removing some of the hard-coded specifics.
Get the contents from props.children instead of always using "Close Menu"
Accept a className as a prop
Change the name of toggleNavigation to onClick
Code Sandbox Demo

Resources