I would like on hover of my ActionOverflow component the a message 'more options' displays. I'm unable to add onMouseEnter and OnMouseLeave to the component. I've tried wrapping it in a parent div and it displays the message but the ActionOverflow which is displaying an Ellipsis icon moves very far left in the div when it's hovered over. How can I achieve this and not have the ellipsis move?
const menuItems: string[] = ['hghhg']
const EllipsisMenu = () => {
const [active, setActive] = useState(false);
const [hover, setHover] = useState(false);
const handleClick = () => {
setActive(!active);
};
const handleClickAway = () => {
setActive(false);
};
const handleMouseEnter = () => {
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
return (
<>
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
{hover ? 'more options' : null}
{menuItems.length > 0 ?
<ActionOverflow active={active} onClick={handleClick} onClickAway={handleClickAway} >
{menuItems.map((item: string, index: number) => (
<div key={index}>{item}</div>
))}
</ActionOverflow>
:
null
}
</div>
</>
);
};
export default EllipsisMenu
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
The Accordions all have an id:
const CategoryDisplay: React.FC<Props> = (props) => {
...
return (
<>
<Accordion
id={`category-${accordion.id}`}/>
</>
);
};
export default CategoryDisplay;
And I've got a function that finds a specific Accordion with an id via a hash value in the link and scrolls to it:
useLayoutEffect(() => {
const anchor = window.location.hash.split("#")[1];
if (anchor) {
const anchorEl = document.getElementById(anchor);
if (anchorEl) {
anchorEl.scrollIntoView();
}
}
}, []);
Now I also want to expand that specific accordion. How would I go about this for Material-UI accordions? I'd have to set expanded on the anchorEl somehow.
Accordion has expanded prop. Set it to true and Accordion will expand:
const CategoryDisplay: React.FC<Props> = (props) => {
const [expand, setExpand] = useState(new Array(numberOfAccordions).fill(false))
...
return (
<>
<Accordion
id={`category-${accordion.id}`} expanded={expand[accordion.id]}/>
</>
);
};
export default CategoryDisplay;
useLayoutEffect((accordionId) => {
const anchor = window.location.hash.split("#")[1];
if (anchor) {
const anchorEl = document.getElementById(anchor);
if (anchorEl) {
let result = [...expand];
result[accordionId] = true;
setExpand(result);
anchorEl.scrollIntoView();
}
}
}, []);
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
tell me how when you click outside the window to close it? put a click on the main div, but it closes when I click even on the form input field. How to implement this idea?
export default () => {
const cn = useClassName('home-page');
const slider = useRef();
const onNext = () => slider.current.next();
const [active, setActive] = useState(false);
const onStart = () => {
setActive(true);
};
const closeModal = () => {
if (active) {
setActive(!active);
}
};
return (
<div className={cn()} onClick={closeModal}>
<Carousel dotsClass="test-className" ref={slider} effect="fade">
{dataSet.map(elem =>
<Slider title={elem.title} img={elem.image} onNext={onNext} onStart={onStart} key={elem.index}/>
)}
</Carousel>
<LoginForm style={active ? {display: 'block'} : {display: 'none'}}/>
</div>
);
};
I have an image list (more like an image gallery), show 4 images by default and a "show more" button if there are more then 4 images on the list.
Everything is working. But by the time I click "show more", the list blinks (I think it re-renders the whole list).
I want to prevent this unnecessary blink.
GIF to demo the problem: https://media.giphy.com/media/RjrNH5MpSi5ebNCF6g/giphy.gif
export const ImageList: React.FC<IImageList> = (props: IImageList) => {
const initLimit = 4;
const [limit, setLimit] = useState(initLimit);
const [showMore, setShowMore] = useState(false);
const [displayImages, setDisplayImages] = useState(props.images.slice(0, limit));
useEffect(() => {
setDisplayImages(displayImages.concat(props.images.slice(initLimit, limit)));
}, [limit]);
return (
<>
<GridList cellHeight={props.cellHeight} cols={props.totalCols}>
{displayImages.map((item, index) => {
let displayItem = item as IImageListItem;
if (!showMore && index === limit - 1 && props.images.length > limit) {
displayItem.showMoreCovered = true;
displayItem.showMoreLabel = 'Show more';
displayItem.showMoreClick = () => setLimit(props.images.length);
} else {
displayItem.showMoreCovered = false;
}
displayItem.index = index;
displayItem.imageClick = () => toggleModal(index);
return (
<GridListTile key={displayItem.id} cols={displayItem.cols} rows={displayItem.rows}>
{displayItem.showMoreCovered ? (
<div onClick={displayItem.showMoreClick}>
<div>{displayItem.showMoreLabel}</div>
</div>
) : null}
<img src={displayItem.src} className={classes.postImageEl} onClick={displayItem.imageClick}/>
</GridListTile>
);
})}
</GridList>
</>
);
};
Try using the functional form of setState:
useEffect(() => {
setDisplayImages(images => images.concat(props.images.slice(initLimit, limit)));
}, [limit]);