I'm trying to implement a virtualisation component with reactjs.
In order to fill the empty top and bottom gap I used an empty div with dynamic height which depends on the scroll position.
<div style={{ height: topPlaceholderHeight }} />
{visibleItems.map(renderItem)}
<div style={{ height: bottomPlaceholderHeight }} />
When the user scrolls > a scroll event is triggered > placeholderHeight is updated > triggers an other scroll event => infinite loop that leads to an auto scroll to the bottom
const [start, setStart] = useState(0);
const [end, setEnd] = useState(0);
const [scrollTop, setScrollTop] = useState(0);
const parentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const listElement = parentRef.current;
if (!listElement) return;
const itemsPerPage = Math.ceil(listElement.offsetHeight / itemHeight) + 2;
const newStart = Math.max(0, Math.floor(scrollTop / itemHeight) - 1);
const newEnd = Math.min(items.length, newStart + itemsPerPage);
setStart(newStart);
setEnd(newEnd);
}, [items, itemHeight, scrollTop]);
useEffect(() => {
const listElement = parentRef.current;
if (!listElement) return;
const scrollHandler = () => {
setScrollTop(listElement.scrollTop);
};
listElement.addEventListener("scroll", scrollHandler);
return () => listElement.removeEventListener("scroll", scrollHandler);
}, [parentRef]);
Code sandbox
Any suggestions for solving this problem ?
Related
i got a canvas in react that i want to allow users to draw rectangles, ive searched through stackoverflow and youtube videos regarding this but my rectangle cant seem to align with the crusor. everything works fine when the canvas is the only thing on the page, as in that its positioned on the top left. but when i include my other components everything does wrong in the canvas. please help, its my first time using canvas and ive only been following tutorials, trying to understand each functions but i wasnt able to find a solution.
const HomeCanvas = () => {
const canvasRef = useRef(null);
const contextRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
const canvasOffSetX = useRef(null);
const canvasOffSetY = useRef(null);
const startX = useRef(null);
const startY = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
canvas.width = 500;
canvas.height = 500;
const context = canvas.getContext("2d");
context.lineCap = "round";
context.strokeStyle = "black";
context.lineWidth = 2;
contextRef.current = context;
const canvasOffSet = canvas.getBoundingClientRect();
canvasOffSetX.current = canvasOffSet.top;
canvasOffSetY.current = canvasOffSet.left;
console.log(canvasOffSet.left);
console.log(canvasOffSet.top);
}, []);
const startDrawingRectangle = ({ nativeEvent }) => {
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
startX.current = nativeEvent.clientX - canvasOffSetX.current;
startY.current = nativeEvent.clientY - canvasOffSetY.current;
setIsDrawing(true);
};
const drawRectangle = ({ nativeEvent }) => {
if (!isDrawing) {
return;
}
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
const newMouseX = nativeEvent.clientX - canvasOffSetX.current;
const newMouseY = nativeEvent.clientY - canvasOffSetY.current;
const rectWidth = newMouseX - startX.current;
const rectHeight = newMouseY - startY.current;
contextRef.current.clearRect(
0,
0,
canvasRef.current.width,
canvasRef.current.height
);
contextRef.current.strokeRect(
startX.current,
startY.current,
rectWidth,
rectHeight
);
};
const stopDrawingRectangle = () => {
setIsDrawing(false);
};
return (
<div className="home-canvas">
<div className="canvas-sidebar">
<div>Merge Table</div>
</div>
<div style={{ width: "100%", paddingLeft: "5%" }}>
<canvas
className="canvas-area"
ref={canvasRef}
onMouseDown={startDrawingRectangle}
onMouseUp={stopDrawingRectangle}
onMouseMove={drawRectangle}
onMouseLeave={stopDrawingRectangle}
></canvas>
</div>
</div>
);
};
what did i do wrong? here is the video of it.
https://youtu.be/ioGINfUKBgc
I need to set height of 'tbody' dynamically. That is why I use 'useRef' which watch to position of the element 'tfoot', and check 'tfoot.offsetTop' and change 'heightTbody' until the tbody is positioned all the way to the bottom. In 'positionChecker' I compare window.height and 'tfoot.offsetTop'. In the condition i try to change the state but unsuccesseble
What I do wrong?
Thanks
Component.tsx
.....several imports
const Component= () => {
const store = useContext(Context);
const [id, setId] = useState('');
const [heightTbody, setHeightTbody] = useState(650);
const checkVisibilityFoot = useRef<HTMLTableSectionElement>(null);
const [list, setList] = useState<ContentI[]>([]);
const fetchData = async () => {
const temp = await store.getContent();
setList(temp);
};
const resizeHolder = (e: UIEvent) => {
const w = e.target as Window;
if (!isNaN(w.innerHeight) && isFinite(w.innerHeight))
positionChecker(w.innerHeight, checkVisibilityFoot.current?.offsetTop as number);
};
const positionChecker = (windHeight: number, checkVisFoot: number) => {
if (windHeight < checkVisFoot) {
setHeightTbody((prevState) => prevState - 1); // <-- here must be changed
positionChecker(windHeight, checkVisibilityFoot.current?.offsetTop as number);
}
};
useEffect(() => {
fetchData();
}, []);
window.addEventListener('resize', resizeHolder);
return (
<table>
<tbody style={{ height: heightTbody + 'px' }}>
......content
</tbody>
<tfoot ref={checkVisibilityFoot}></tfoot>
</table>
)
If you want to watch for changes, pass params to useEffect array.
I'm using react-testing-library with jest to test storybook stories. I have a component that, based on it's scrollWidth and the clientWidth it conditionally renders a button to page through the component:
function Collection({
children,
scrollOffset = DEFAULT_SCROLL_X_OFFSET,
onScroll,
...rest
}: HorizontalChipCollectionProps) {
const [scrollX, setScrollX] = useState(0); // For detecting start scroll postion
const [showRightPager, setShowRightPager] = useState(false); // For detecting end of scrolling
const scrollableAreaRef = useRef();
const scrollableAreaCallback = React.useCallback((node) => {
scrollableAreaRef.current = node;
/** Check to see if the scrollWidth is less than the full width, if so hide the right arrow */
if (node?.scrollWidth <= node?.clientWidth) {
setShowRightPager(true);
}
}, [setShowRightPager]);
const leftPageOnClick = () => pagerOnClick(PAGER_DIRECTION.LEFT);
const rightPageOnClick = () => pagerOnClick(PAGER_DIRECTION.RIGHT);
return (
<Box>
<ScrollableArea
py={{
base: 'xsmall',
medium: 'small',
}}
display="flex"
ref={scrollableAreaCallback}
onScroll={scrollCurrCheck}
overflowX="auto"
css={css`
> *:not(:last-child) {
margin-right: var(--space-small);
}
`}
{...rest}
>
{children}
</ScrollableArea>
{!showRightPager ? <Pager data-testid="right-pager" variant="right" onClick={rightPageOnClick} /> : null}
</Box>
);
}
I'm trying to test the rightPageOnClick to confirm it works but I'm unable to find the element in my test. Here is my test:
test('should show pager buttons', () => {
window.innerWidth = 1440;
window.innerHeight = 900;
window.dispatchEvent(new Event('resize'));
render(<Basic />);
const rightPager = screen.getByTestId('right-pager');
expect(rightPager).toBeInTheDocument();
});
Let me know if you need any more info from me!
I have an overflow: scroll div, and I'd like to check if it is scrolled to the right. How can I get an event listener for scroll in React and how can I calculate if it's scrolled to the right horisontally?
const elem = () => {
const [isScrolledRight, setIsScrolledRight] = useState(false)
const handleScroll = (elem: any) => {
const scrollLeft = elem.target.scrollLeft
const scrollWidth = elem.target.scrollWidth
const clientWidth = elem.target.clientWidth
setIsScrolledRight(scrollLeft + clientWidth == scrollWidth)
}
// use isScrolledRight anywhere you need
return (
<div onScroll={handleScroll.bind(this)} /> // this should be horisontally scrollable
)
}
I need to make a styled-component div with button, button generates size of the div. Then a component nested inside of the div component displays the size. It has to use forwardRef and both components have to be functional.
Base component code:
const StyledComp = () => {
const [size, setSize] = useState({ width: 100, height: 100 });
const [maxMin, setMaxMin] = useState({ max: 500, min: 100 });
const [count, setCount] = useState(0);
let ref = useRef(<Shape />);
const handleResize = () => {
const max = maxMin.max;
const min = maxMin.min;
let width = Math.floor(Math.random() * (max - min)) + min;
let height = Math.floor(Math.random() * (max - min)) + min;
setSize({ width, height });
setCount(count+1);
};
return (
<div className="styled-component">
<Shape height={size.height} width={size.width} ref={ref}>
<ShapeResult count={count} {...{ ref }} />
</Shape>
<p>Width: {size.width}</p>
<p>Height: {size.height}</p>
<button onClick={handleResize}>Generate</button>
</div>
);
};
The component is a simple styled-components div with width and height from props.
The gets the ref just fine, I can print it in console and it does show the current size of the element. But when I try to get the width/height values via innerRef.current.offsetWidth it gets previous generation results. Here's the code:
const ShapeResult = ({ count }, ref) => {
let innerRef = React.useRef(ref);
const [countState, setCountState] = useState(0);
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
useEffect(() => {
innerRef = ref;
console.log(innerRef.current);
setWidth(innerRef.current.offsetWidth);
setHeight(innerRef.current.offsetHeight);
setCountState(count);
}, [count, ref]);
return (
<div className="shape-result">
<p>Click count: {countState}</p>
<p>Width: {width}px</p>
<p>Height: {height}px</p>
</div>
);
};
const ForwardedResult = React.forwardRef(ShapeResult);
export default ForwardedResult;
I'll be grateful for any help, I've been searching for an answer for way too long and it really shouldn't be that hard task...
Apparently the main problem was in the useEffect. It should be:
setWidth(innerRef.current.getAttribute("width"));
setHeight(innerRef.current.getAttribute("height"));
It finally updates now
Your approach seems incorrect.
As shown below, in the StyledComp component, access DOM through shapeResultForwardRef.current.
const StyledComp = () => {
const [size, setSize] = useState({ width: 100, height: 100 });
const [maxMin, setMaxMin] = useState({ max: 500, min: 100 });
const [count, setCount] = useState(0);
let ref = useRef(<Shape />);
const shapeResultForwardRef = useRef(null); // add
const handleResize = () => {
const max = maxMin.max;
const min = maxMin.min;
let width = Math.floor(Math.random() * (max - min)) + min;
let height = Math.floor(Math.random() * (max - min)) + min;
setSize({ width, height });
setCount(count+1);
};
return (
<div className="styled-component">
<Shape height={size.height} width={size.width} ref={ref}>
<ShapeResult count={count} ref={shapeResultForwardRef} />
</Shape>
<p>Width: {size.width}</p>
<p>Height: {size.height}</p>
<button onClick={handleResize}>Generate</button>
</div>
);
};
another:
const ShapeResult = ({ count }, ref) => {
return (
<div className="shape-result" ref={ref}>
...
</div>
);
};
const ForwardedResult = React.forwardRef(ShapeResult);
export default ForwardedResult;