See codesandbox here
I am trying to add a modal that shows up with a delay when hovering over a div. However, it's getting a bit tricky because, for example, if the timeout interval is 1000ms, and you hover over said div and then hover away from that div within the 1000ms, the modal will still show up. What I want to happen is for the modal to show up after the delay (e.g. 1000ms) only if you have maintained mouseover over the div for that delay period. How can I create this effect instead of the side effects I'm seeing now? Thanks!
index.tsx:
import * as React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const Modal: React.FC = () => {
const divRef = React.useRef<HTMLDivElement>(null);
const [showModal, setShowModal] = React.useState<boolean>(false);
React.useEffect(() => {
const divNode = divRef.current;
const handleEvent = (event: Event): void => {
if (divNode) {
if (divNode.contains(event.target as Node)) {
setTimeout(() => setShowModal(true), 1000);
} else {
setShowModal(false);
}
}
};
document.addEventListener("mouseover", handleEvent);
return () => {
document.removeEventListener("mouseover", handleEvent);
};
}, [divRef]);
return (
<div className="container">
<div className="div" ref={divRef}>
Hover Me
</div>
{showModal && <div className="modal">modal</div>}
</div>
);
};
const App: React.FC = () => (
<>
<Modal />
<Modal />
<Modal />
<Modal />
</>
);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You should add a mouse out event that will hide the modal.
Call a funtion on 'mouseout' event listener and set the showModal to false. In that way, it will hide the modal if you move your mouse any time.
setShowModal(false)
Updated: Can you also set timeout to a variable and then on mouseout fire clearTimeout(variable_that_set_to_timeout)
React.useEffect(() => {
const divNode = divRef.current;
let timeout = null;
const handleEvent = (event: Event): void => {
if (divNode) {
if (divNode.contains(event.target as Node)) {
timeout = setTimeout(() => setShowModal(true), 1000);
} else {
setShowModal(false);
}
}
};
const hideModal = (event: Event): void => {
clearTimeout(timeout);
setShowModal(false);
};
divNode.addEventListener("mouseover", handleEvent);
divNode.addEventListener("mouseout", hideModal);
return () => {
document.removeEventListener("mouseover", handleEvent);
};
}, [divRef]);
Link of sandbox
You should really avoid changing the DOM when working with react. React isn't jQuery.
you could try making this your modal code:
const Modal: React.FC = () => {
const [timeout, setModalTimeout] = React.useState(null);
const [showModal, setShowModal] = React.useState<boolean>(false);
return (
<div className="container">
<div className="div" onMouseEnter={() => {
timeout && !showModal && clearTimeout(timeout);
setModalTimeout(setTimeout(() => setShowModal(true), 1000))
}} onMouseLeave={() => {
timeout && clearTimeout(timeout)
setShowModal(false);
}}>
Hover Me
</div>
{showModal && <div className="modal">modal</div>}
</div>
);
};
Sources:
https://reactjs.org/docs/refs-and-the-dom.html
https://reactjs.org/docs/react-dom.html
The proper way to do this would be to create a useTimeout hook and manage maintain the state of the hover.
import { useState } from "react";
import useTimeout from "./useTimeout";
export default function App() {
const [visible, setVisible] = useState(false);
const [hovered, setHovered] = useState(false);
//close after 3s
useTimeout(() => setVisible(true), !visible && hovered ? 3000 : null);
return (
<div className="App">
<h1>Hover Timeout Example</h1>
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
Hover me for 3s to show modal
<div>Hover status: {hovered ? "true" : "false"}</div>
</div>
{visible && (
<div>
<h1>Modal</h1>
<div>
<button onClick={() => setVisible(false)}>close</button>
</div>
</div>
)}
</div>
);
}
Code Sandbox
Related
[Solved] My input component is losing focus as soon as I press any key only when its value is controlled from outside the portal
NOTE: I am sorry. While writing this, I found the problem in my code, but I decided to post this anyway
[Reason] I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.
Portal
const root = document.getElementById('root')
const modalRoot = document.getElementById('modal-root')
const Portal = ({ children, className, drawer = false }) => {
const element = React.useMemo(() => document.createElement('div'), [])
React.useEffect(() => {
element.className = clsx('modal', className)
modalRoot.appendChild(element)
return () => {
modalRoot.removeChild(element)
}
}, [element, className])
return ReactDOM.createPortal(children, element)
}
Modal
const Modal = (props) => {
const { children, show = false, close, className } = props
const backdrop = React.useRef(null)
const handleTransitionEnd = React.useCallback(() => setActive(show), [show])
const handleBackdropClick = React.useCallback(
({ target }) => target === backdrop.current && close(),
[]
)
const handleKeyUp = React.useCallback(
({ key }) => ['Escape'].includes(key) && close(),
[]
)
React.useEffect(() => {
if (backdrop.current) {
window.addEventListener('keyup', handleKeyUp)
}
if (show) {
root.setAttribute('inert', 'true')
document.body.style.overflow = 'hidden'
document.activeElement.blur?.() // ! CULPRIT
}
return () => {
root.removeAttribute('inert')
document.body.style.overflow = 'auto'
window.removeEventListener('keyup', handleKeyUp)
}
}, [show, close])
return (
<>
{show && (
<Portal className={className}>
<div
ref={backdrop}
onClick={handleBackdropClick}
onTransitionEnd={handleTransitionEnd}
className={clsx('backdrop', show && 'active')}>
<div className="content">{children}</div>
</div>
</Portal>
)}
</>
)
}
Custom Textfield
const TextField = React.forwardRef(
({ label, className, ...props }, ref) => {
return (
<div className={clsx('textfield', className)}>
{label && <label>{label}</label>}
<input ref={ref} {...props} />
</div>
)
}
)
I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.
In Modal.jsx
...
React.useEffect(() => {
...
if (show) {
root.setAttribute('inert', 'true')
document.body.style.overflow = 'hidden'
document.activeElement.blur?.() // ! CULPRIT
}
...
}, [show, close]) // as dependency
...
<Modal
show={show}
close={() => setShow(false)} // this was inlined
className="some-modal"
>
...
</Modal>
TAKEAWAY
Do not inline functions
Usually there is no reason to pass a function (pointer) as dependency
I'm really new to React and I've been trying for some time now to make a scroll to top function. Using the existing tutorials and everything on google i've made this component :
Import React, { useState, useEffect } from "react";
export default function ScrollToTop() {
const [isVisible, setIsVisible] = useState(false)
const toggleVisibility = () => {
if(window.pageYOffset > 300) {
setIsVisible(true);
} else {
setIsVisible(false);
}
};
const ScrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
useEffect(() => {
window.addEventListener('scroll', toggleVisibility);
return () => {
window.removeEventListener('scroll', toggleVisibility);
};
}, []);
if (!isVisible) {
return false
}
return (
<button className="button" onClick={ScrollToTop}>
<div className="button__arrow button__arrow--up"></div>
</button>
)
};
the problem is that when i import it in the App.js it doesn't work properly, the scrolling part works perfectly, but the button it just stays at bottom of the page instead of appearing after a certain amount of scroll. This is the App.js:
return (
<div>
{loading ? (
<h1>Loading...</h1>
) : (
<>
<Navbar />
<div className="Grid-container">
{pokemonData.map((pokemon, i) => {
return <Card key={i} pokemon={pokemon} />;
})}
</div>
<ScrollToTop/>
</>
)}
</div>
);
}
I don't see any overt issues in your ScrollTop implementation that would cause the scroll-to-top button to remain on the page even after scrolling back to the top, but I do see room for optimization.
For onScroll events it's best to use passive listeners.
Invoke toggleVisibility once immediately in the mounting useEffect to ensure proper initial state.
Apply Boolean Zen to the visibility toggler; Update the isVisible state with the result of the boolean comparison.
Just conditionally render the button in a single return.
Code:
function ScrollToTop() {
const [isVisible, setIsVisible] = useState(false);
const toggleVisibility = () => {
setIsVisible(window.pageYOffset > 300);
};
const ScrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth"
});
};
useEffect(() => {
toggleVisibility();
window.addEventListener("scroll", toggleVisibility, { passive: true });
return () => {
window.removeEventListener("scroll", toggleVisibility, { passive: true });
};
}, []);
return isVisible ? (
<button className="button" onClick={ScrollToTop}>
<div className="button__arrow button__arrow--up">TOP</div>
</button>
) : null;
}
If you're still having issues with your implementation and usage try to create a codesandbox that reproduces the issue that we can inspect and debug live.
I'm using react-colorful to get colors HEX code.
I would want to show this component when the color's box is clicked, and hide it when clicked outside of it, kinda like the color picker Chrome is using (ex <input type="color" /> )
How to do so ?
https://codesandbox.io/s/react-colorful-demo-forked-wwxq2?file=/src/App.js:308-322
import React, { useState } from "react";
import { HexColorPicker } from "react-colorful";
import "./styles.css";
export default function App() {
const [color, setColor] = useState("#b32aa9");
const handleChange = (e) => {
setColor(e.target.value);
};
return (
<div className="App">
<HexColorPicker color={color} onChange={setColor} />
//Click on this box to show the picker <div className="value" style={{ borderLeftColor: color }}>
Current color is {color}
</div>
<input value={color} onChange={handleChange} />
<div className="buttons">
<button onClick={() => setColor("#c6ad23")}>Choose gold</button>
<button onClick={() => setColor("#556b2f")}>Choose green</button>
<button onClick={() => setColor("#207bd7")}>Choose blue</button>
</div>
</div>
);
}
At first, you should have to declare a state which tracks whether the color box is open or not like
const [open, setopen] = useState(false);
Now add an event handler on the div of the box which toggles the state from false to true and true to false.
const openBox = () => {
setopen(!open);
}
Now in your return statement, add a conditional to open or close colorBox. if clicked on that div which holds the onClick method.
<div className="App">
{open &&<HexColorPicker color={color} onChange={setColor} />
}
<div onClick={() => openBox()}className="value" style={{ borderLeftColor: color }}>
Current color is {color}
</div>
Now if you click on the div which contains the onClick method, it will open the colorBox and on pressing again it will close it.
You can use the following example (from https://codesandbox.io/s/opmco found in Code Recipes)
import React, { useCallback, useRef, useState } from "react";
import { HexColorPicker } from "react-colorful";
import useClickOutside from "./useClickOutside";
export const PopoverPicker = ({ color, onChange }) => {
const popover = useRef();
const [isOpen, toggle] = useState(false);
const close = useCallback(() => toggle(false), []);
useClickOutside(popover, close);
return (
<div className="picker">
<div
className="swatch"
style={{ backgroundColor: color }}
onClick={() => toggle(true)}
/>
{isOpen && (
<div className="popover" ref={popover}>
<HexColorPicker color={color} onChange={onChange} />
</div>
)}
</div>
);
};
useClickOutside.js
import { useEffect } from "react";
// Improved version of https://usehooks.com/useOnClickOutside/
const useClickOutside = (ref, handler) => {
useEffect(() => {
let startedInside = false;
let startedWhenMounted = false;
const listener = (event) => {
// Do nothing if `mousedown` or `touchstart` started inside ref element
if (startedInside || !startedWhenMounted) return;
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) return;
handler(event);
};
const validateEventStart = (event) => {
startedWhenMounted = ref.current;
startedInside = ref.current && ref.current.contains(event.target);
};
document.addEventListener("mousedown", validateEventStart);
document.addEventListener("touchstart", validateEventStart);
document.addEventListener("click", listener);
return () => {
document.removeEventListener("mousedown", validateEventStart);
document.removeEventListener("touchstart", validateEventStart);
document.removeEventListener("click", listener);
};
}, [ref, handler]);
};
export default useClickOutside;
I want to render an element in React just by calling a function.
Usually you'd use a component (let's say a Popup) that takes a boolean from state to make it appear or not and change it with some callback handler. Something like this:
import React, { useState } from "react";
import Popup from "somecomponentlibrary";
import { Button } from "pathtoyourcomponents";
export const SomeComponent = () => {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => { setOpen(true); }}>
this opens a modal
</Button>
<Popup type={"info"} open={open} timeout={1000}>
text within modal
<Button onClick={() => { setOpen(false); }}></Button>
</Popup>
</>
);
};
I was wondering if instead of returning it in the component as above I could just call some method to just show it on the screen like that:
import React from "react";
import { Button, popup } from "pathtoyourcomponents";
export const SomeComponent = () => {
return (
<>
<Button onClick={() => { popup.info("text within modal", 1000); }}>
this opens a modal
</Button>
</>
);
};
How do I write the popup function in order to render a Popup component in the DOM in such way?
You can use ReactDOM.render to render the popup when the function is called:
const node = document.createElement("div");
const popup = (message, {type, timeout}) => {
document.body.appendChild(node);
const PopupContent = () => {
return (
<Popup type={type} open={true} timeout={timeout}>
{message}
<button
onClick={clear}
>Close</button>
</Popup >
);
};
const clear = () => {
ReactDOM.unmountComponentAtNode(node);
node.remove();
}
ReactDOM.render(<PopupContent/>, node);
};
Then call the popup function:
<Button onClick={() => { popup("text within modal", {type: "info", timeout: 1000}); }}>
this opens a modal
</Button>
I made an imperative API for a popup a while back, it allows you to await it until it closes, and even receive input to the caller (you might as well send back the button the user pressed):
const PopupContext = createContext()
export const PopupProvider = ({children}) => {
const [open, setOpen] = useState(false)
const [input, setInput] = useState('')
const resolver = useRef()
const handleOpen = useCallback(() => {
const { promise, resolve } = createDeferredPromise()
resolver.current = resolve
setInput('')
setOpen(true)
return promise
}, [])
const handleClose = useCallback(() => {
resolver.current?.(input)
setOpen(false)
}, [])
return <PopupContext.Provider value={handleOpen}>
{children}
<Popup type={"info"} open={open} timeout={1000} onClose={handleClose}>
<input value={input} onChange={e => setValue(e.target.value)}/>
<Button onClick={handleClose}/>
</Popup>
</PopupContext.Provider>
}
export const usePopup = () => {
const context = useContext(PopupContext);
if (!context)
throw new Error('`usePopup()` must be called inside a `PopupProvider` child.')
return context
}
// used to let await until the popup is closed
const createDeferredPromise = func => {
let resolve, reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
func?.(resolve, reject)
})
return { promise, resolve, reject }
}
You can wrap you app with the provider:
return <PopupProvider>
<App/>
</PopupProvider>
And use it inside your functional components:
const MyComponent = props => {
const popup = usePopup()
return <Button onClick={e => {
const input = await popup()
console.log('popup closed with input: ' + input)
}/>
}
You can do much more interesting stuff, as pass prompt text to the popup function to show in the popup etc. I'll leave this up to you.
You might also want to memoize your top level component being wrapped to avoid rerendering the entire application on popup open/close.
The popup can be rendered as a new separate React app but can still be made to share state with the main app like below.
import React, { useEffect } from "react";
import { render, unmountComponentAtNode } from "react-dom";
const overlay = {
top: "0",
height: "100%",
width: "100%",
position: "fixed",
backgroundColor: "rgb(0,0,0)"
};
const overlayContent = {
position: "relative",
top: "25%",
textAlign: "center",
margin: "30px",
padding: "20px",
backgroundColor: "white"
};
let rootNode;
let containerNode;
function Modal({ children }) {
useEffect(() => {
return () => {
if (rootNode) {
rootNode.removeChild(containerNode);
}
containerNode = null;
};
}, []);
function unmountModal() {
if (containerNode) {
unmountComponentAtNode(containerNode);
}
}
return (
<div style={overlay}>
<div style={overlayContent}>
{children}
<button onClick={unmountModal}>Close Modal</button>
</div>
</div>
);
}
/* additional params like props/context can be passed */
function renderModal(Component) {
if (containerNode) {
return;
}
containerNode = document.createElement("div");
rootNode = document.getElementById("root");
containerNode.setAttribute("id", "modal");
rootNode.appendChild(containerNode);
render(<Modal>{Component}</Modal>, containerNode);
}
const App = () => {
const ModalBody = <p>This is a modal</p>;
return (
<div>
<button onClick={() => renderModal(ModalBody)}>Open Modal</button>
</div>
);
};
render(<App />, document.getElementById("root"));
Yes, I think you can make it.
for example:
Use popup component
import React, { useState } from "react";
import Popup from "somecomponentlibrary";
import { Button } from "pathtoyourcomponents";
export const SomeComponent = () => {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => { setOpen(true); }}>
this opens a modal
</Button>
<Popup type={"info"} open={open} timeout={1000}>
text within modal
<Button onClick={() => { setOpen(false); }}></Button>
</Popup>
</>
);
};
Define pop-up component
const Popup = (props) => {
return(
<div style={{zIndex:props.open?"-100":"100", transition: `all ${props.timeout /
1000}s`, opacity: props.open?1:0}}>
{props.children}
</div>
)
}
I think you can customize the animation effect you like.
I am making a slider that has arrows to scroll through the items only if the items are too wide for the div. It works, except when resizing the window to make it large enough, the arrows don't disappear. The arrows should only appear if scroll arrows is true.
{scrollArrows && (
<div className="arrow arrow-left" onClick={goLeft}>
<
</div>
)}
But when I console.log it, even if the arrows are there, scroll arrows ALWAYS is false. Here is the relevant code:
import "./ProjectRow.css";
import Project from "../Project/Project";
import React, { useEffect, useState } from "react";
const ProjectRow = (props) => {
const rowRef = React.useRef();
const [hasLoaded, setHasLoaded] = useState(false);
const [scrollArrows, setScrollArrows] = useState(false);
const [left, setLeft] = useState(0);
const [rowWidth, setRowWidth] = useState(null);
function debounce(fn, ms) {
let timer;
return () => {
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, arguments);
}, ms);
};
}
useEffect(() => {
const setVariables = () => {
console.log(scrollArrows);
if (rowRef.current?.offsetWidth < rowRef.current?.scrollWidth) {
setRowWidth(rowRef.current.offsetWidth);
if (!scrollArrows) {
console.log("scrollArrows true now");
setScrollArrows(true);
}
} else if (scrollArrows) {
setScrollArrows(false);
}
};
if (!hasLoaded) setVariables();
const debouncedHandleResize = debounce(setVariables, 300);
window.addEventListener("resize", debouncedHandleResize);
setHasLoaded(true);
return () => {
window.removeEventListener("resize", debouncedHandleResize);
};
}, []);
const goLeft = () => {
const projectWidth = rowRef.current.childNodes[2].firstChild.offsetWidth;
if (rowRef.current.childNodes[2].firstChild.getBoundingClientRect().x < 0)
setLeft(left + projectWidth + 20);
};
const goRight = () => {
const projectWidth = rowRef.current.childNodes[2].firstChild.offsetWidth;
if (
rowRef.current.childNodes[2].lastChild.getBoundingClientRect().x +
projectWidth >
rowWidth
)
return setLeft(left - (projectWidth + 20));
};
return (
<div className="project-row" ref={rowRef}>
<h3 className="project-row-title light-gray bg-dark-gray">
{props.data.groupName}
</h3>
<hr className="gray-bar" />
<div className="slider" style={{ left: `${left}px` }}>
{props.data.projects.map((project, i) => (
<Project data={project} key={i} />
))}
</div>
{scrollArrows && (
<div className="arrow arrow-left" onClick={goLeft}>
<
</div>
)}
{scrollArrows && (
<div className="arrow arrow-right" onClick={goRight}>
>
</div>
)}
</div>
);
};
export default ProjectRow;
The "scroll arrows true now" gets logged, but scrollArrows variable stays false, even after waiting to resize or just resizing between two widths that need it.
EDIT: Fixed it by removing the if on the else if. I figured that might be the issue, but I don't know why it was preventing it from functioning properly, so it feels bad.
Your 'useEffect' doesn't look well structured. You have to structure it this way:
React.useEffect(()=>{
// Functions
window.addEventListener('resize', ...
return ()=>{
window.removeEventListener('resize', ...
}
},[])
Please follow this answer and create a Hook: Why useEffect doesn't run on window.location.pathname changes?
Try changing that and it might update your value and work.