Hide modal on click outside in react hooks - reactjs

i have a modal component in my react app and i need to close it on click outside
import React from "react";
import ReactDOM from "react-dom";
import style from "./Modal.module.scss";
const Modal = ({ isShowing, hide, childrenContent, childrenHeader }) =>
isShowing
? ReactDOM.createPortal(
<React.Fragment>
<div className={style.modalOverlay} />
<div
className={style.modalWrapper}
aria-modal
aria-hidden
tabIndex={-1}
role="dialog"
>
<div className={style.modal}>
<div className={style.modalHeader}>
{childrenHeader}
<button
type="button"
className={style.modalCloseButton}
data-dismiss="modal"
aria-label="Close"
onClick={hide}
>
<span aria-hidden="true">×</span>
</button>
</div>
{childrenContent}
</div>
</div>
</React.Fragment>,
document.body
)
: null;
export default Modal;
i was try to use this solution but it's not work in my code, how can i fix it?

Just a tip, when looking at the html you can use the native <dialog> tag, this is the semantically correct way to display a dialog type pop-up box, which yours looks to be.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
Dialog has a showModal() method, and a .close() method. This would be a better way of displaying a pop-up type dialog, than using <div> tags. It also allows you to use the native HTML5 methods, rather than trying to provide a work around using React.
I would reccomend this method over trying to look for work arounds

const Modal = ({ children, showModal, toggleModal }) => {
const wrapperRef = React.useRef(null);
const closeModal = React.useCallback(
({ target }) => {
if (
wrapperRef &&
wrapperRef.current &&
!wrapperRef.current.contains(target)
) {
toggleModal();
}
},
[toggleModal]
);
React.useEffect(() => {
document.addEventListener("click", closeModal, { capture: true });
return () => {
document.removeEventListener("click", closeModal, { capture: true });
};
}, [closeModal]);
return showModal
? ReactDOM.createPortal(
<>
<div ref={wrapperRef} className="modal">
{children}
</div>
</>,
document.body
)
: null;
};
Modal.propTypes = {
children: PropTypes.node.isRequired,
showModal: PropTypes.bool.isRequired,
toggleModal: PropTypes.func.isRequired
};
export default Modal;
in your parent component :
const Parent = () => {
const [showModal, setModalState] = React.useState(false);
const toggleModal = React.useCallback(() => {
setModalState((prevState) => !prevState);
}, []);
return (
<div>
<Modal showModal={showModal} toggleModal={toggleModal}>
<h1>Hello!</h1>
... some other childrens
<button
onClick={toggleModal}
>
Close
</button>
</Modal>
</div>
);
};

Related

Unable to pass a function as a prop to child component in React

I'm trying to pass a function (addTask) as a prop from parent component (MainComponent.js) to Child Component (Button.js) but it's not working. I've also passed a variable (btnColor) as prop and it's working properly. What am I missing?
MainComponent.js:
import Button from "./Button";
const MainComponent = () => {
const addTask = () => {
console.log("Task Added...");
};
return (
<div>
<div> Header Component here </div>
<div> Some Data </div>
<Button onClick={addTask} btnColor="red" />
<div> Footer Component here </div>
</div>
);
};
export default MainComponent;
Button.js:
const Button = ({ addTask, btnColor }) => {
return (
<button style={{ backgroundColor: btnColor }} onClick={addTask}>
Add
</button>
);
};
export default Button;
I'm expecting the console to log 'Task Added...' but it isn't logging anything.
You're not passing it through correctly. You're passing it to onClick whereas you want to pass it through as addTask
Incorrect: <Button onClick={addTask} btnColor="red" />
Correct: <Button addTask={addTask} btnColor="red" />
I think it should be like this
const MainComponent = () => {
const addTask = () => {
console.log('Added new task...')
}
return (
<Button addTask={addTask} />
)
}
const Button = ({ addTask }) => {
return (
<button onClick={addTask} ></button>
)
}

Using css property "transition: all", I add a dynamic class: (<div className={`${show ? "show" : "hide"}></div> ), but my animation doesn't work

I am trying to make a Modal component. I would like that when the modal appears, it appears with a transition just like when it disappears. This is currently very jerky, why and how can I fix it?
I would like that when the modal is shown it is shown with an animation and the same behavior when the modal disappears (click on the button).
thank you very much for the help, I know I will learn a lot.
//content of styles.css
.modal {
position: absolute;
width: 100%;
height: 100%;
background: red;
transition: all 300ms ease-out;
transform: translateY(100%);
}
.show {
transform: translateY(0%);
}
.hide {
transform: translateY(100%);
}
app.js
import Modal from "./modal";
/*
const Modal = ({ show, children }) => {
return <div className={`modal ${show ? "show" : "hide"}`}>.
{children} </div>;
};
export default Modal;
*/
import ModalContent from "./modalContent";
/*
const ModalContent = ({ show }) => {
const showModal = () => {
show();
};
return <button onClick={showModal}> close modal</button>;
};
export default ModalContent;
*/
export default function App() {
const [show, setShow] = useState(false);
const closeModal = () => {
setShow(false);
};
useEffect(() => setShow(true), []);
return (
<div className="App">
{show && (
<Modal show={show}>
<ModalContent show={closeModal} />
</Modal>
)}
</div>
);
}
I updated my code:
this is my live code
First of all, in your demo modal disappears immediately, without any transition. It seems, that it's cause by re-rendering of whole App component, on show state change. Extracting Modal component out of App do the trick for me:
const Modal = ({ show, children }) => {
useEffect(() => {}, [show]);
return <div className={`modal ${show ? "show" : "hide"}`}>{children} </div>;
};
export default function App() {
Second point - you can't control initial setup just with with css transition. Transition appears when something (class, attribute, pseudoclass) changes on the given element. To get around this and have smooth modal appearance, you can setup one-time useEffect in the App component, which will change show state from false to true. My overall snippet:
const Modal = ({ show, children }) => {
return <div className={`modal ${show ? "show" : "hide"}`}>{children} </div>;
};
export default function App() {
const ModalContent = ({ show }) => {
const showModal = () => {
show();
};
return <button onClick={showModal}> close modal</button>;
};
const [show, setShow] = useState(false);
const closeModal = () => {
setShow(false);
};
useEffect(() => setShow(true), [])
return (
<div className="App">
<Modal show={show}>
<ModalContent show={closeModal} />
</Modal>
</div>
);
}

Avoid re-creating body component on each render of react-modal

I'm using this lib to create a modal
I have 3 components: Table, Modal and List
Table has Modal (a custom React Modal), and the body of Modal will be List.
Now the problem is, List has some functions which change the states of Table, so when I do something that can make Table's state change, Table and Modal will be re-rendered when Modal is re-rendered, it re-creates a new List which leads to lost all stuffs I'm doing with List.
Here is a simple version of my app. link
Now I don't want List to be re-created each time Modal is re-rendered. Is there any way to archive that? (I don't want to create a modal myself or use global state management in this case)
import { useEffect, useMemo, useState } from "react";
import ReactModal from "react-modal";
ReactModal.setAppElement("#root");
const List = ({ onClick }) => {
useEffect(() => {
console.log("List is mounted");
}, []);
return <button onClick={onClick}>Click me!</button>;
};
const Modal = ({ state, body, isOpen }) => {
useEffect(() => {
console.log("Modal is re-rendered");
});
return (
<div
id="react modal wrapper"
style={{
display: `${isOpen ? "block" : "none"}`
}}
>
<ReactModal isOpen={isOpen}>
<div>
state is {state}
<br />
{body}
</div>
</ReactModal>
</div>
);
};
const Table = ({ state, onClick, isOpen }) => {
useEffect(() => {
console.log("Table is re-rendered");
});
const memorizedList = useMemo(() => <List onClick={onClick} />, []);
return (
<div>
state: {state}
<Modal state={state} body={memorizedList} isOpen={isOpen} />
</div>
);
};
const App = () => {
const [state, setState] = useState(1);
const onClick = () => setState((v) => v + 1);
return (
<div>
<button onClick={onClick}>Change state</button>
<Table state={state} onClick={onClick} isOpen={state % 2 === 0} />
</div>
);
};
export default App;

onClick doesn't update Parent's state from Child component

I'm trying to create a Modal (popup) that may be closed onClick using a button inside the Modal.js itself. To achieve that I create useState inside Parent.js and pass closeModal function (which updates the Parent's state) into Modal.js via. props;
For whatever reason, onClick event doesn't update the Parent's state (even tho. it manages to fire the closeModal function accepted from props). From console.log I can see that the closeModal function is being run but still Parent.js state doesn't change so the Modal doesn't close. Other events like onMouseDown or onChange do work correctly and Modal get's closed as supposed to.
Could you, please, explain why it doesn't work with onClick and what happens here?
Here the code down below and a sandbox to play with: Sandbox
Parent.js
import { useState } from "react";
import Modal from "./Modal";
export default () => {
const [modal, setModal] = useState({
isShown: false,
name: ""
});
const { isShown } = modal;
const openModal = () => {
setModal({ ...modal, isShown: true });
};
const closeModal = () => {
setModal({ ...modal, isShown: false });
console.log("Modal must be close!");
};
return (
<div className="parent" onClick={openModal}>
{isShown ? <Modal closeModal={closeModal} /> : null}
<div className="message">Open Modal</div>
</div>
);
};
Modal.js
export default ({ closeModal }) => {
return (
<div className="modal">
<button className="close" onClick={closeModal}>
onClick
</button>
<button className="close" onMouseDown={closeModal}>
onMouseDown
</button>
<input type="text" placeholder="onChange" onChange={closeModal} />
</div>
);
};
P.S.: I managed to make that work by moving onClick which opens the modal inside Parent.js, but I still don't understand why it didn't work and what really happens. I assume that with onClick the state gets updated so fast that at the moment it gets compared to the old one it appears there is no difference, so it ends up not updating. But this is just my guess...
Could you clarify for me, please?
Parent.js
import { useState } from "react";
import Modal from "./Modal";
export default () => {
const [modal, setModal] = useState({
isShown: false,
name: ""
});
const { isShown } = modal;
const openModal = () => {
setModal({ ...modal, isShown: true });
};
const closeModal = () => {
setModal({ ...modal, isShown: false });
console.log("Modal must be close!");
};
return (
<div className="parent">
{isShown ? <Modal closeModal={closeModal} /> : null}
<div className="message" onClick={openModal}>Open Modal</div>
</div>
);
};
You need to stop the click event propagation. As Child div is overlapping with Parent (its button), both events are taking place in order:
"close the modal (in child/Modal)"
"open the modal (in parent/Button)"
That's the reason it is staying opened. To fix it, use stopPropagation in the Modal (child) component :
<button className="close" onClick={e => {
e.stopPropagation()
closeModal()
}}>
PS: I would suggest going with this modal

Ionic popover with React - How to make it sticky to the button

Following the Ionic documentation, I am trying to get the popover sticky to the button (like on their own example).
Unfortunately I do not know how to achieve this...
Thanks
import React, { useState } from 'react';
import { IonPopover, IonButton } from '#ionic/react';
export const PopoverExample: React.FC = () => {
const [showPopover, setShowPopover] = useState(false);
return (
<>
<IonPopover
isOpen={showPopover}
onDidDismiss={e => setShowPopover(false)}
>
<p>This is popover content</p>
</IonPopover>
<IonButton onClick={() => setShowPopover(true)}>Show Popover</IonButton>
</>
);
};
You also need to include an event in the showPopover hook -
const [showPopover, setShowPopover] = useState<{open: boolean, event: Event | undefined}>({
open: false,
event: undefined,
});
<IonPopover
isOpen={showPopover.open}
event={showPopover.event}
onDidDismiss={e => setShowPopover({open: false, event: undefined})}
>
<p>This is popover content</p>
</IonPopover>
<IonButton onClick={(e) => setShowPopover({open: true, event: e.nativeEvent})}>Click</IonButton>

Resources