I want to test carousel in React-testing-library.
I can't check whether state is changed in RTL like enzyme does, so...
What I've done is just give carousel a scrollX and check whether it is changed
fireEvent.scroll(element, { target: { scrollX: 100 } });
// check
element.scrollLeft...
Is it collect or any ideas?
import React, { useRef } from "react";
import "./Carousel.css";
export interface CarouselProps {
children: React.ReactNode;
}
const Carousel = ({ children }: CarouselProps) => {
const carouselRef = useRef<HTMLDivElement>(null);
let isDown = false;
let startX = 0;
let scrollLeft = 0;
const onMouseDownCarousel: React.MouseEventHandler<HTMLDivElement> = (e) => {
if (!carouselRef || !carouselRef.current) {
return;
}
isDown = true;
carouselRef.current.classList.add("active");
startX = e.pageX - carouselRef.current.offsetLeft;
scrollLeft = carouselRef.current.scrollLeft;
};
const onMouseLeaveCarousel: React.MouseEventHandler<HTMLDivElement> = (e) => {
if (!carouselRef || !carouselRef.current) {
return;
}
isDown = false;
carouselRef.current.classList.remove("active");
};
const onMouseUpCarousel: React.MouseEventHandler<HTMLDivElement> = (e) => {
if (!carouselRef || !carouselRef.current) {
return;
}
isDown = false;
carouselRef.current.classList.remove("active");
};
const onMouseMoveCarousel: React.MouseEventHandler<HTMLDivElement> = (e) => {
if (!carouselRef || !carouselRef.current) {
return;
}
if (!isDown) return;
e.preventDefault();
const x = e.pageX - carouselRef.current.offsetLeft;
const walk = (x - startX) * 3;
carouselRef.current.scrollLeft = scrollLeft - walk;
};
return (
<div
className="carousel"
onMouseDown={onMouseDownCarousel}
onMouseLeave={onMouseLeaveCarousel}
onMouseUp={onMouseUpCarousel}
onMouseMove={onMouseMoveCarousel}
ref={carouselRef}
>
{children}
</div>
);
};
export default Carousel;
Related
I am making a 3D walkable world. Implemented the keyboard movement using wasd and space and mouse movement. I have no problem with the mouse movement, but when pressing the keys on the keyboard it doesnt work. I have seen the example multiple times and it works, but for me it doesnt seem so.
I have a useKeyboardInput.js where I add the event listeners and it registers when i`m pressing the key.
import { useCallback, useEffect, useState } from "react";
export const useKeyboardInput = (keysToListen = []) => {
const getKeys = useCallback(() => {
const lowerCaseArray = [];
const hookReturn = {};
keysToListen.forEach((key) => {
const lowerCaseKey = key.toLowerCase();
lowerCaseArray.push(lowerCaseKey);
hookReturn[lowerCaseKey] = false;
});
return {
lowerCaseArray,
hookReturn,
};
}, [keysToListen]);
const [keysPressed, setPressedKeys] = useState(getKeys().hookReturn);
useEffect(() => {
const handleKeyDown = (e) => {
const lowerKey = e.key.toLowerCase();
if (getKeys().lowerCaseArray.includes(lowerKey)) {
setPressedKeys((keysPressed) => ({ ...keysPressed, [lowerKey]: true }));
}
console.log("Pressed Key is: " + e.key);
};
const handleKeyUp = (e) => {
const lowerKey = e.key.toLowerCase();
if (getKeys().lowerCaseArray.includes(lowerKey)) {
setPressedKeys((keysPressed) => ({
...keysPressed,
[lowerKey]: false,
}));
}
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("keyup", handleKeyUp);
};
}, [keysToListen, getKeys]);
console.log("KeysPressed is: " + JSON.stringify(keysPressed));
return keysPressed;
};
And then i have the Player.js where i am using the hooks for keyboard and mouse.
import { useSphere } from "#react-three/cannon";
import React, { useEffect, useRef, useState } from "react";
import { useFrame, useThree } from "#react-three/fiber";
import { Vector3 } from "three";
import { useKeyboardInput } from "../Hooks/useKeyboardInput";
import { useMouseInput } from "../Hooks/useMouseInput";
import { useVariable } from "../Hooks/useVariable";
import { Bullet } from "./Bullet";
import { Raycaster } from "three";
/** Player movement constants */
const speed = 300;
const bulletSpeed = 30;
const bulletCoolDown = 300;
const jumpSpeed = 5;
const jumpCoolDown = 400;
export const Player = () => {
/** Player collider */
const [sphereRef, api] = useSphere(() => ({
mass: 100,
fixedRotation: true,
position: [0, 1, 0],
args: [0.2],
material: {
friction: 0,
},
}));
/** Bullets */
const [bullets, setBullets] = useState([]);
/** Input hooks */
const pressed = useKeyboardInput(["w", "a", "s", "d", " "]);
const pressedMouse = useMouseInput();
/** Converts the input state to ref so they can be used inside useFrame */
const input = useVariable(pressed);
const mouseInput = useVariable(pressedMouse);
/** Player movement constants */
const { camera, scene } = useThree();
/** Player state */
const state = useRef({
timeToShoot: 0,
timeTojump: 0,
vel: [0, 0, 0],
jumping: false,
});
useEffect(() => {
api.velocity.subscribe((v) => (state.current.vel = v));
}, [api]);
/** Player loop */
useFrame((_, delta) => {
/** Handles movement */
console.log("Input.current is: " + JSON.stringify(input.current));
const { w, s, a, d } = input.current;
const space = input.current[" "];
let velocity = new Vector3(0, 0, 0);
let cameraDirection = new Vector3();
camera.getWorldDirection(cameraDirection);
let forward = new Vector3();
forward.setFromMatrixColumn(camera.matrix, 0);
forward.crossVectors(camera.up, forward);
let right = new Vector3();
right.setFromMatrixColumn(camera.matrix, 0);
let [horizontal, vertical] = [0, 0];
if (w == true) {
vertical += 1;
console.log("Pressed w");
} else if (s == true) {
vertical -= 1;
console.log("Pressed s");
} else if (d == true) {
horizontal += 1;
console.log("Pressed d");
} else if (a == true) {
horizontal -= 1;
console.log("Pressed a");
}
console.log(
"Horizontal is: " + horizontal + " and vertical is: " + vertical
);
if (horizontal !== 0 && vertical !== 0) {
velocity
.add(forward.clone().multiplyScalar(speed * vertical))
.add(right.clone().multiplyScalar(speed * horizontal));
velocity.clampLength(-speed, speed);
} else if (horizontal !== 0) {
velocity.add(right.clone().multiplyScalar(speed * horizontal));
} else if (vertical !== 0) {
velocity.add(forward.clone().multiplyScalar(speed * vertical));
}
console.log("velocity is: " + JSON.stringify(velocity));
/** Updates player velocity */
api.velocity.set(
velocity.x * delta,
state.current.vel[1],
velocity.z * delta
);
/** Updates camera position */
camera.position.set(
sphereRef.current.position.x,
sphereRef.current.position.y + 1,
sphereRef.current.position.z
);
/** Handles jumping */
if (state.current.jumping && state.current.vel[1] < 0) {
/** Ground check */
const raycaster = new Raycaster(
sphereRef.current.position,
new Vector3(0, -1, 0),
0,
0.2
);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length !== 0) {
state.current.jumping = false;
}
}
if (space && !state.current.jumping) {
const now = Date.now();
if (now > state.current.timeTojump) {
state.current.timeTojump = now + jumpCoolDown;
state.current.jumping = true;
api.velocity.set(state.current.vel[0], jumpSpeed, state.current.vel[2]);
}
}
/** Handles shooting */
const bulletDirection = cameraDirection.clone().multiplyScalar(bulletSpeed);
const bulletPosition = camera.position
.clone()
.add(cameraDirection.clone().multiplyScalar(2));
if (mouseInput.current.left) {
const now = Date.now();
if (now >= state.current.timeToShoot) {
state.current.timeToShoot = now + bulletCoolDown;
setBullets((bullets) => [
...bullets,
{
id: now,
position: [bulletPosition.x, bulletPosition.y, bulletPosition.z],
forward: [bulletDirection.x, bulletDirection.y, bulletDirection.z],
},
]);
}
}
});
return (
<>
{/** Renders bullets */}
{bullets.map((bullet) => {
return (
<Bullet
key={bullet.id}
velocity={bullet.forward}
position={bullet.position}
/>
);
})}
</>
);
};
It registers the keys that are being pressed but nothing happens.
At first glance, the following snippet seems to be the source of issue.
const getKeys = useCallback(() => {
const lowerCaseArray = [];
const hookReturn = {};
You may have to move those variables outside of the callback.
both handleKeyDown and handleKeyUp calls getKeys() which creates new empty values
I am trying to merge these two hooks to get the direction and the percentage of the scroll.
useScrollPercentage.js
import { useRef, useState, useEffect } from "react";
export default function useScrollPercentage() {
const scrollRef = useRef(null);
const [scrollPercentage, setScrollPercentage] = useState(NaN);
const reportScroll = e => {
setScrollPercentage(getScrollPercentage(e.target));
};
useEffect(
() => {
const node = scrollRef.current;
if (node !== null) {
node.addEventListener("scroll", reportScroll, { passive: true });
if (Number.isNaN(scrollPercentage)) {
setScrollPercentage(getScrollPercentage(node));
}
}
return () => {
if (node !== null) {
node.removeEventListener("scroll", reportScroll);
}
};
},
[scrollPercentage]
);
return [scrollRef, Number.isNaN(scrollPercentage) ? 0 : scrollPercentage];
}
function getScrollPercentage(element) {
if (element === null) {
return NaN;
}
const height = element.scrollHeight - element.clientHeight;
return Math.round((element.scrollTop / height) * 100);
}
useScrollDirection.js
import {useState, useEffect} from 'react';
import _ from 'lodash';
export default function useScrollDirection({
ref,
threshold,
debounce,
scrollHeightThreshold,
}) {
threshold = threshold || 10;
debounce = debounce || 10;
scrollHeightThreshold = scrollHeightThreshold || 0;
const [scrollDir, setScrollDir] = useState(null);
const debouncedSetScrollDir = _.debounce(setScrollDir, debounce);
useEffect(() => {
let lastScrollY = ref?.current?.scrollTop;
let lastScrollDir;
let ticking = false;
const hasScrollHeightThreshold =
ref?.current?.scrollHeight - ref?.current?.clientHeight >
scrollHeightThreshold;
const updateScrollDir = () => {
const scrollY = ref?.current?.scrollTop;
if (
Math.abs(scrollY - lastScrollY) < threshold ||
!hasScrollHeightThreshold
) {
ticking = false;
return;
}
const newScroll = scrollY > lastScrollY ? 'Down' : 'Up';
if (newScroll !== lastScrollDir) {
debouncedSetScrollDir(newScroll);
}
lastScrollY = scrollY > 0 ? scrollY : 0;
lastScrollDir = newScroll;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
ref?.current?.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return scrollDir;
}
Currently I am using it like this
App.js
const [scrollRef, scrollPercentage] = useScrollPercentage();
const scrollDirection = useScrollDirection({ref: scrollRef});
const handleScroll = () => {
console.log(scrollPercentage, scrollDirection);
}
return (
<Modal ref={scrollRef} onScroll={handleScroll}>
// Scrollable Content
</Modal>
)
The problem is that since I don't want to trigger useScrollDirection for every scroll percentage unless the scroll direction changed only it should trigger. Also is it possible to merge these hooks?
Any help is appreciated
I am currently learning React and specifically hooks. I am trying to create a canvas where I am using the hook 'useRef'. I get this point that whenever I have to create a reference to my Canvas I can use the 'useRef' hook and by can also use it to point to other canvas properties.
My question is in the code below I have created a component named "UseWBoard" and I want to pass my ctxReference.current value to the "Marker" component.
Here is the code:
import React, { useRef, useEffect } from "react";
const UseWBoard = () => {
const canvasReference = useRef(null);
const ctxReference = useRef(null);
let initPaint = false;
useEffect(() => {
const canvas = canvasReference.current;
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.lineCap = "round";
ctx.lineWidth = "25";
ctx.strokeStyle = "black";
ctxReference.current = ctx;
}, []);
function startDraw({ nativeEvent }) {
const { offsetX, offsetY } = nativeEvent;
ctxReference.current.beginPath();
ctxReference.current.moveTo(offsetX, offsetY);
initPaint = true;
}
function stopDraw() {
ctxReference.current.closePath();
initPaint = false;
}
function sketch({ nativeEvent }) {
if (!initPaint) {
return;
}
const { offsetX, offsetY } = nativeEvent;
ctxReference.current.lineTo(offsetX, offsetY);
ctxReference.current.stroke();
}
function Marker() {
ctxReference.current.lineCap = "round";
ctxReference.current.lineWidth = "1";
ctxReference.current.strokeStyle = "black";
}
return (
<div>
<button onClick={Marker}>abc</button>
<canvas
ref={canvasReference}
onMouseDown={startDraw}
onMouseUp={stopDraw}
onMouseMove={sketch}
></canvas>
</div>
);
};
const Mark = (ctxReference) => {
ctxReference.current.lineCap = "round";
ctxReference.current.lineWidth = "1";
ctxReference.current.strokeStyle = "black";
};
export { UseWBoard, Mark };
Thanks in advance.
I am trying to convert my component to react hooks.
class AppMenu extends Component<Props> {
menuRef = null;
initMenu() {
if (this.props.mode === 'horizontal') {
const menuRef = new MetisMenu('#menu-bar').on('shown.metisMenu', event => {
const menuClick = e => {
if (!event.target.contains(e.target)) {
menuRef.hide(event.detail.shownElement);
}
};
window.addEventListener('click', menuClick);
});
this.menuRef = menuRef;
} else {
this.menuRef = new MetisMenu('#menu-bar');
}
}
}
How do I convert this to hooks? Especially the menuRef part.
I did const menuRef = useRef(null);, but how do I convert the this.menuRef = new MetisMenu('#menu-bar'); to useRef? I tried menuRef.current = new MetisMenu('#menu-bar'); but it throws an error.
Am I doing this the incorrect way?
Thanks
Here is what you need to do:
function AppMenu(props: Props) {
const menuRef = useRef(null);
function initMenu() {
if (props.mode === "horizontal") {
menuRef.current = new MetisMenu("#menu-bar").on(
"shown.metisMenu",
(event) => {
const menuClick = (e) => {
if (!event.target.contains(e.target)) {
menuRef.hide(event.detail.shownElement);
}
};
window.addEventListener("click", menuClick);
}
);
} else {
menuRef.current = new MetisMenu("#menu-bar");
}
}
useEffect(() => {
initMenu();
}, []);
}
Recently I picked up a project that has d3-flame-graph on it and the graph is displayed according to the Filters defined on another component.
My issue is that when searching with new parameters I can't seem to clean the previous chart and I was wondering if someone could help me. Basically what I'm having right now is, when I first enter the page, the loading component, then I have my graph and when I search for a new date I have the loading component but on top of that I still have the previous graph
I figured I could use flamegraph().destroy() on const updateGraph but nothing is happening
import React, { FC, useEffect, useRef, useState, useCallback } from 'react'
import { useParams } from 'react-router-dom'
import moment from 'moment'
import * as d3 from 'd3'
import { flamegraph } from 'd3-flame-graph'
import Filters, { Filter } from '../../../../../../components/Filters'
import { getFlamegraph } from '../../../../../../services/flamegraph'
import { useQueryFilter } from '../../../../../../hooks/filters'
import FlamegraphPlaceholder from '../../../../../../components/Placeholders/Flamegraph'
import css from './flamegraph.module.css'
import ToastContainer, {
useToastContainerMessage,
} from '../../../../../../components/ToastContainer'
const defaultFilters = {
startDate: moment().subtract(1, 'month'),
endDate: moment(),
text: '',
limit: 10,
}
const getOffSet = (divElement: HTMLDivElement | null) => {
if (divElement !== null) {
const padding = 100
const minGraphHeight = 450
// ensure that the graph has a min height
return Math.max(
window.innerHeight - divElement.offsetTop - padding,
minGraphHeight
)
} else {
const fallBackNavigationHeight = 300
return window.innerHeight - fallBackNavigationHeight
}
}
const Flamegraph: FC = () => {
const [queryFilters, setQueryFilters] = useQueryFilter(defaultFilters)
const [fetching, setFetching] = useState(false)
const [graphData, setGraphData] = useState()
const {
messages: toastMessages,
addMessage: addMessageToContainer,
removeMessage: removeMessageFromContainer,
} = useToastContainerMessage()
const flameContainerRef = useRef<HTMLDivElement | null>(null)
const flameRef = useRef<HTMLDivElement | null>(null)
const graphRef = useRef<any>()
const graphDataRef = useRef<any>()
const timerRef = useRef<any>()
const { projectId, functionId } = useParams()
let [sourceId, sourceLine] = ['', '']
if (functionId) {
;[sourceId, sourceLine] = functionId.split(':')
}
const createGraph = () => {
if (flameContainerRef.current && flameRef.current) {
graphRef.current = flamegraph()
.width(flameContainerRef.current.offsetWidth)
.height(getOffSet(flameRef.current))
.cellHeight(30)
.tooltip(false)
.setColorMapper(function(d, originalColor) {
// Scale green component proportionally to box width (=> the wider the redder)
let greenHex = (192 - Math.round((d.x1 - d.x0) * 128)).toString(16)
return '#FF' + ('0' + greenHex).slice(-2) + '00'
})
}
}
const updateGraph = (newData: any) => {
setGraphData(newData)
graphDataRef.current = newData
if (graphRef.current) {
if (newData === null) {
graphRef.current.destroy()
graphRef.current = null
} else {
d3.select(flameRef.current)
.datum(newData)
.call(graphRef.current)
}
}
}
const fetchGraph = (filters: Filter) => {
setFetching(true)
getFlamegraph(
Number(projectId),
filters.startDate ? filters.startDate.unix() : 0,
filters.endDate ? filters.endDate.unix() : 0,
sourceId,
sourceLine
)
.then(graphData => {
if (!graphRef.current) {
createGraph()
}
updateGraph(graphData)
})
.catch(({ response }) => {
updateGraph(null)
if (response.data) {
addMessageToContainer(response.data.message, true)
}
})
.finally(() => {
setFetching(false)
})
}
const onResize = useCallback(() => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => {
if (graphRef.current && flameContainerRef.current) {
graphRef.current.width(flameContainerRef.current.offsetWidth)
d3.select(flameRef.current)
.datum(graphDataRef.current)
.call(graphRef.current)
}
}, 500)
}, [])
useEffect(() => {
fetchGraph(queryFilters)
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onChangeFilters = (filters: Filter) => {
setQueryFilters(filters)
fetchGraph(filters)
}
return (
<div className={css.host}>
<Filters
defaultValues={queryFilters}
searching={fetching}
onSearch={onChangeFilters}
/>
<div className={css.flameBox}>
<div className={css.flameContainer} ref={flameContainerRef}>
<div ref={flameRef} />
</div>
{fetching || !graphData ? (
<FlamegraphPlaceholder loading={fetching} />
) : null}
</div>
<ToastContainer
messages={toastMessages}
toastDismissed={removeMessageFromContainer}
/>
</div>
)
}
export default Flamegraph
Firstly, flamegraph() creates a new instance of flamegraph, you'd need to use graphref.current.destroy(). Secondly, you'd want to destroy this not when the data has already been loaded, but just as it starts to load, right? Because that's the operation that takes time.
Consider the following:
const cleanGraph = () => {
if (graphref.current !== undefined) {
graphref.current.destroy()
}
}
const fetchGraph = (filters: Filter) => {
setFetching(true)
cleanGraph()
getFlamegraph(
Number(projectId),
filters.startDate ? filters.startDate.unix() : 0,
filters.endDate ? filters.endDate.unix() : 0,
sourceId,
sourceLine
)
...
}