How to create drag behaviour for overlapping SVG elements in React - reactjs

Problem illustrated in https://codesandbox.io/s/dragging-overlapping-svgs-1jd5m
I would like to do something quite simple: implement drag behaviour in my SVG elements in my React App. So my initial thoughts were to capture start and stop dragging by onMouseDown and onMouseUp events, and to implement the updating in the onMouseMove event. In principle, like so:
<circle
...
onMouseDown={() => startDrag()}
onMouseUp={() => endDrag()}
onMouseMove={event => dragging && drag(event)}
/>
In my React class Circ I implemented the behaviours like so:
const Circ = ({ cx, cy, r, fill, stroke }) => {
const [draggingPosition, setDraggingPosition] = useState({ x: 0, y: 0 });
const startDrag = () => {
setDragging(true);
};
const endDrag = () => {
setDragging(false);
};
const drag = event => {
event.stopPropagation();
setDraggingPosition({
x: draggingPosition.x + event.movementX,
y: draggingPosition.y + event.movementY
});
};
...
}
Well, that basically worked. But now when my SVG contains several overlapping elements, it starts to break down:
const SVG = () => (
<svg width={617} height={316} viewBox="0 0 400 400">
<g>
<Circ cx={108} cy={108} r="100" fill="#0ff" stroke="#000" />
<Circ cx={180} cy={209} r="100" fill="#ff0" stroke="#000" />
<Circ cx={220} cy={109} r="100" fill="#f0f" stroke="#000" />
</g>
</svg>
);
When I drag the blue circle, its behaviour is fine until an overlapping circle captures the mouse event and my dragging stops.
So my question is: how do I make sure that once I start dragging, the mouse event is restricted to only that current element? Ideally, also when there are other elements in the editor (Rect, G, etc...) that my Circ class does not necessarily know about?

I am now solving it with registering dragging at the global level and a
pointerEvents={ globalDragging ? "none" : "auto"}
on all my elements. I'm not really happy with this solution, so I'm still happy to hear alternatives.

Related

React state update during control change breaks OrbitControls from three

I am using react three with drei orbitControls and try to detect wether the camera is above or below zero, while moving the camera around. Currently I'm using onEnd listener to trigger my code in my ParentComponent. If I change a state of my parent component like so everything works as expected. Unfortunately if I start another change of orbit controls before ending another and change my parents state, orbit controls will break. E.g. I rotate the camera and hold down my left mouse button, than get below zero with my camera and the scroll with my mouse wheel at the same time, Orbit controls no longer works. This is especially painful when using touch. As soon as a second finger touches and I cross the ground orbit control breaks.
This is my Controls Component:
const Controls = forwardRef(({ handleCamera }, ref) => {
const { gl, camera } = useThree();
const controls = useRef(null);
useImperativeHandle(ref, () => ({
getCamera() {
return camera
},
}))
const handleChange = (e) => {
handleCamera(camera);
}
return (
<OrbitControls ref={controls}
onEnd={(e) => handleChange(e)}
/>
);
});
export default Controls;
And this is my parent:
function App() {
const handleCamera = (camera) => {
setCameraBelowSurface(camera.position.y <= 0 ? true : false);
};
return
<Canvas >
<Suspense>
<Controls
ref={controlsRef}
handleCamera={handleCamera}
/>
</Suspense>
</Canvas>
}
export default App;

,Chain react-spring animations on hover

I'm trying to do a simple multi-step animation on react-spring: rotating an icon that goes to 10deg, then -10deg, and go back to 0deg. This animation will execute when the user hovers the icon.
The following snippet illustrates an animation with just the 1st step:
const [isBooped, setBooped] = useState(false);
const style = useSpring({
rotate: isBooped ? 10 : 0,
config: config.wobbly,
});
const onMouseEnter = useCallback(() => setBooped(true), []);
useEffect(() => {
if (!isBooped) {
return;
}
const timeoutId = window.setTimeout(() => {
setBooped(false);
}, 250);
return () => {
window.clearTimeout(timeoutId);
};
}, [isBooped]);
return (
<Button onMouseEnter={onMouseEnter}>
<Icon style={style} />
</Button>
);
I understand that to accepts an array to chain multiple animations like so:
to: [{ rotate: 10 }, { rotate: -10 }, { rotate: 0 }],
...but I do not know how to handle this along with the isBooped state.
Here's a sandbox with the code:
https://codesandbox.io/s/react-spring-test-o8fwm?file=/src/Notifications.js
This is a good opportunity to use the imperative API react-spring has to offer.
You can also cancel the animation mid flow if you want it to stop animating when someone is not hovering over the icon, or maybe it should just reset itself back to 0 cutting the animation short (this is commented out in the example provided below).
By passing a function to the useSpring hook you get an array where the first item is your styles and the second is the API. You can then use an effect to trigger the async animation to begin running. Here's your codesandbox modified to demonstrate this: https://codesandbox.io/s/react-spring-test-forked-ildtb?file=/src/Notifications.js

D3 React - Cannot remove map of paths before drawing paths with new locations

I am trying to programatically draw a map of SVG paths using D3 in React. Every time I render a new map of paths, the previously drawn paths are not removed from the DOM.
I have tried all suggestions here, but none are working effectively.
When I click a button, a new node is added to an array and D3 draws a SVG path from its starting point. But at render-time previously drawn lines still appear.
Here is my code:
useEffect(() => {
const svg = d3
.select(svgRef.current)
.attr('viewBox', `0 0 ${avaliableHorizontalSpace} ${avaliableHorizontalSpace}`);
svg.select('path').remove(); // this doesn't work
svg.selectAll("*").remove(); // only one stays drawn using this
if (rects.length > 0) {
rects.forEach((_el, idx) => {
const line = d3
.line<Number>()
.x((value, index) => 200 * index)
.y((value) => Number(value));
svg
.selectAll(`.node[nodeid='${nodes[idx][0].id}]'`)
.data([rects])
.join('path')
.attr('d', (value: (Number[] | Iterable<Number>)[]) => {
return line(value[idx]);
});
});
}
}, [rects]);
Later in JSX:
const NodePathWithId = (props: NodePathWithId): ReactElement => {
return (<path {...props} />);
};
...
{rects.length > 0 && rects.map((el, idx) => (
<svg
fill="white"
fillOpacity="0"
stroke="#000"
strokeWidth="1.5"
ref={(ref): void => { svgRef.current = ref; }}
style={{ position: 'absolute' }}
key={`y${nodes[idx][0].id}`}
>
<NodePathWithId nodeid={nodes[idx][0].id} />
</svg>
))}
How do I update the SVG to remove old paths so I only have the number of paths in the rects array?
Update:
Some picture below to demonstrate the problem:
First click
Second click:

How do I stop React Native Maps' Draggable Marker from disabling touches outside the MapView on iOS using Google as the provider?

On iOS with Google provider React Native Maps' Draggable Marker disables touches outside the MapView until it registers a touch inside the MapView. On Android everything is fine, but for some reason on iOS when I finish dragging a marker on the map and onDragEnd is called, no touch events are registered unless they are on TextInputs. Occasionally a TouchableOpacity will flash momentarily, but it's onPress function is never called. However if I touch inside the MapView, even nowhere near the marker, everything goes back to the way it's supposed to be. It's like react native maps has some finishing event that doesn't occur that forces the focus to stay on the map.
There are a couple tricky things going on but I don't think they're the culprits:
I use setState with onDragStart and onDragEnd to disable the ScrollView parent component, otherwise the dragging gets interrupted by the scroll on Android.
As part of onDragEnd I make a callout with react-native-geocoding, then update the region state. I have commented all this out and it still doesn't work.
The plan is to animate to the new region after the state is updated, but until this is resolved there's no point. Here's my code, or what's left of it after taking out all the commented stuff:
const MapSection = (props) => {
const {
location, setIsDraggingMarker, onDragMarkerEnd, region,
} = props;
const [isLoading, setIsLoading] = useState(false);
const onDragEnd = (e) => {
onDragMarkerEnd(e.nativeEvent.coordinate);
};
if (!isEmpty(location)) {
return (
<View style={styles.mapContainer}>
<MapView
provider={PROVIDER_GOOGLE}
style={styles.mapView}
initialRegion={region}
scrollEnabled={false}
zoomEnabled={false}
rotateEnabled={false}
>
<Marker
draggable
onDragStart={() => setIsDraggingMarker(true)}
onDragEnd={onDragEnd}
coordinate={region}
>
<Image
style={styles.imageStyle}
resizeMode="stretch"
source={require('../../../../assets/images/draggableMarkerPin.png')}
/>
<Callout
style={styles.customCallout}
onPress={() => { }}
>
<View style={styles.callout}>
<Feather
name="move"
size={12}
color="black"
/>
<Text style={{
paddingLeft: 4,
fontSize: 14,
}}
>
Press and drag to fine tune location!
</Text>
</View>
</Callout>
</Marker>
</MapView>
</View>
);
}
return null;
};
export default MapSection;
//FROM THE PARENT COMPONENT, PASSED AS PROPS
const onDragMarkerEnd = (coords) => {
setIsDraggingMarker(false);
setRegionWithLatLng(coords.latitude, coords.longitude);
};
const setRegionWithLatLng = async (latitude, longitude) => {
const fullLocationData = await geocodeLocationByCoords(latitude, longitude);
const currentLocation = { ...fullLocationData };
const newRegion = {
...region,
latitude: fullLocationData.geometry.location.lat,
longitude: fullLocationData.geometry.location.lng,
};
setRegion(newRegion);
dispatchEventDetailsState({
type: FORM_INPUT_UPDATE, value: currentLocation, isValid: true, input: 'location',
});
};
If there's any way to just trick the MapView into thinking it's been pressed (I'ved tried referencing the MapView and calling mapView.current.props.onPress(), no dice), then I'm fine with that. Just any workaround.

Snapping to position onDragEnd with motionValues using Framer Motion and React

I'm using framer motion to create a swipe interaction in my project. I'm trying to make it so that when the user is done dragging the child, it will 'snap' back into a set position.
I've seen from the docs that you can use a spring to animate a motion value: const y = useSpring(x, { damping: 10 }), but I guess I'm not doing it correctly? Heres my code:
export default function SwipeContainer(props) {
const x = useMotionValue(0);
const m = useSpring(x, { damping: 10 });
const handleDragEnd = (evt) => {
console.log(evt);
m.set(200);
}
return (
<div className={styles.swipeContainer}>
<motion.div
style= {{ x, m }}
className={styles.motionDiv}
drag="x"
onDragEnd={handleDragEnd}
>
{props.children}
</motion.div>
</div>
);
}
I'm expecting that when the dragEnd event happens, the child will animate to x:200, but thats not happening. Am I setting the value incorrectly, or perhaps its how I'm applying the motion values to the motion.div?
I didn't experiment with useSpring yet, but you can get it to work with useAnimation.
Here's a CodeSandbox with a similar situation: https://codesandbox.io/s/framer-motion-bottom-sheet-fixed-m2vls.
Hope this helps!

Resources