I'm working on a React front-end that uses Reactstrap, in which I'm creating my own reusable modal component. Whenever there is too much content for the modal, it becomes scrollable and to make that clear to the user, I created an indicator at the bottom of the modal. (example screenshot)
The indicator sticks at the bottom of the modal while scrolling and I make it disappear when the user reaches the end (check onscroll event and moreContent state in code below).
So far so good, but my problem is that I can't find a way to check if I initially have to show the indicator when rendering the modal. Right now the moreContent state is initially set to true, but that should depend on whether the modal is scrollable or not.
I tried:
to find an event like onScroll that fires when Modal is rendered so that I can check if event.target.scrollHeight == event.target.clientHeight
useEffect hook with a reference to the modal. This fires too soon because scrollHeight and clientHeight are still 0.
The code for my modal component:
import React, {useEffect, useState} from 'react';
import {Button, Modal, ModalBody, ModalHeader} from "reactstrap";
const MyModal = (props) => {
const [moreContent, setMoreContent] = useState(true);
const ref = React.useRef();
useEffect(() => {
console.log("scrollHeight", ref.current._element.scrollHeight);
console.log("scrollTop", ref.current._element.scrollTop);
console.log("clientHeight", ref.current._element.clientHeight);
});
const onScroll = (event) => {
if (moreContent) {
setMoreContent(event.target.scrollHeight - event.target.scrollTop !== event.target.clientHeight);
}
}
return (
<Modal isOpen={props.isOpen} toggle={props.closeHandler} centered={true} scrollable={true} className="my-modal" onScroll={onScroll} ref={ref}>
<ModalHeader tag="div" toggle={props.closeHandler}>
...
</ModalHeader>
<ModalBody>
{props.children}
{moreContent &&
<div className="modal-scroll-down">
<i className="fa fa-arrow-down mr-4"></i> MEER <i className="fa fa-arrow-down ml-4"></i>
</div>
}
</ModalBody>
</Modal>
)
};
MyModal.defaultProps = {
showCloseButton : true
}
export default MyModal;
Any tip, advice, workaround is welcome.
Thanks in advance!
The issue here is that you are attaching ref to a Modal which is not a DOM element and therefore will not have these properties scrollHeight, scrollTop, and clientHeight. Furthermore even if it was a DOM element, it is not the element with the scrollbar - it is actually ModalBody. But, to make matters worst, it looks like Reactstrap does not really expose a prop for you attach a forwarded ref to the ModalBody.
To solve this you can replace ModalBody with a div - this is where we can attach a ref to.
<Modal
isOpen={props.isOpen}
toggle={props.closeHandler}
centered={true}
scrollable={true}
className="my-modal"
onScroll={onScroll}
onOpened={onOpened}
>
<ModalHeader tag="div" toggle={props.closeHandler}>
modal header
</ModalHeader>
<div ref={ref} style={{ overflowY: "auto", padding: "16px" }}>
{props.children}
{moreContent && (
<div className="modal-scroll-down">
<i className="fa fa-arrow-down mr-4"></i> MEER{" "}
<i className="fa fa-arrow-down ml-4"></i>
</div>
)}
</div>
</Modal>
Pay attention to the onOpened prop I attached to <Modal>, this answers what you sought:
execute javascript after bootstrap modal is completely rendered and
visible
const onOpened = () => {
setMoreContent(
ref.current.scrollHeight - ref.current.scrollTop !==
ref.current.clientHeight
);
};
Related
Greetings and thank you for stopping by to check this. I'm having some issues and I don't reall know how to fix this.
I have a react js app acctually running with nextjs. So the thing is, I have a navbar, as a compoents and a sign up page as a component.
What I want is simple, when I click on the sign up button on the navbar, the signUp modal should pop up, and when I click on the close (x) icon on the modal, the modal should close.
So far, I've made it possible with useState to show the signUp modal when the signUp button is clicked.
Since the signUp modal is a component that I'm importing to the navabr, I'm finding it difficult to useState to close it when opened.
This is my code.
import SignUpModal from '../SignIn/Register';
function Navbar(
// Show Reigster Modal when the button is clicked
const [register, setRegister] = useState(false);
return () {
<>
{register ? <SignUpModal /> : ''}
<nav>
<div>
<Link>Home</Link>
<Link>About</Link>
<Link>Login</Link>
<button
type="button"
onClick={() => setRegister(!register)} > Sign Up
</button>
</div>
</nav>
</>
)
}
Now, when I click on the signUp button, the modal that I imported, is shown. But on the modal components, I can't close the modal because I can't pass the state from the navbar to the SignUp compoents and close it when I click on the close(x) icon.
On the SignUp component, I know I can do <button type="button" onClick={() => setNavbar(!navbar)}>Close</button>
But how do I pass it? Please someone should help me, thanks!
`
For closing the modal from within the modal component, you just need to pass in a function as a prop to the component, which you can call within the onClick() function of the close button.
import SignUpModal from '../SignIn/Register';
function Navbar(
// Show Reigster Modal when the button is clicked
const [register, setRegister] = useState(false);
return () {
<>
{register && <SignUpModal onClose={() => setRegister(false)} />}
<nav>
<div>
<Link>Home</Link>
<Link>About</Link>
<Link>Login</Link>
<button
type="button"
onClick={() => setRegister(true)} > Sign Up
</button>
</div>
</nav>
</>
}
)
}
I have a button in a modal that on click opens a popover component, in the popover I have an input text field.
The problem I have is that the input lose focus instantly, I can't type in it because something else is hijacking the focus out of it and I can't figure out what is, here is a working example of the problem.
And here is the code in question:
import "./styles.css";
import { Fragment, useState } from "react";
import { Button, Modal, OverlayTrigger, Popover, Form } from "react-bootstrap";
export default function App() {
const [showModal, setShowModal] = useState(false);
const [inputValue, setInputValue] = useState("");
const popover = (
<Popover id="popover-basic">
<Popover.Content>
<Form.Control
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Enter value"
/>
</Popover.Content>
</Popover>
);
return (
<div className="App">
<Fragment>
<Button variant="primary" onClick={() => setShowModal(true)}>
Open
</Button>
{showModal && (
<Modal
show={showModal}
onHide={() => setShowModal(false)}
centered
backdrop="static"
animation={false}
>
<Modal.Header closeButton>modal</Modal.Header>
<Modal.Body>
<p>Hello</p>
<OverlayTrigger
trigger="click"
placement="right"
overlay={popover}
>
<Button variant="secondary">Open Popover</Button>
</OverlayTrigger>
</Modal.Body>
</Modal>
)}
</Fragment>
</div>
);
}
Modal has a property "enforceFocus", which keeps focus on the Modal component. The property value is set to true per default. Set it to false and you will be able to use your input.
"https://react-bootstrap-v4.netlify.app/components/modal/#modal-props
Disabling the enforceFocus works, but I'm not sure it is the best alternative. If the user navigates the fields using the tab key, he might focus elements outside of the modal, which could create confusion.
Instead, I suggest you play with the overlay's container prop. You could use a ref to the modal itself, or to a container inside the modal. This way, the overlay will be "inside" the modal and won't lose focus.
I created an example to demonstrate it works.
I have a component which has a button within it, like so -
<Button variant="primary" disabled={checkAccepted} onClick={openModal}>Send</Button>
I would like this button to, when it is active, to open up a modal when clicked. I am unsure how to do this and have been messing around with props but can't seem to figure it out. I also want the modal to be reusable so that any content can be passed in the modal body.I am thinking how do I open up the modal from within my openModal function?
I tried returning it like so -
const openModal = () => {
return (
<Modal>
<ModalBody>*Pass in swappable content here*</ModalBody>
</Modal>
)
}
But that doesn't seem to work. I am sure I am missing something.
You can't return components from an event handler. The way to handle events in react is almost always to alter the state of your application which triggers a re-render. In your case you need to keep track of the open state of your modal.
This can be done either in a controlled way (you keep track of the open state yourself and pass it to your <Modal> component as a prop) or in an uncontrolled way (the <Modal> component manages the open state itself). The second approach requires that you provide e.g. an element to render to your Modal component that acts as a trigger:
const MyModal = ({ children, trigger }) => {
const [modal, setModal] = useState(false);
const toggle = () => setModal(!modal);
return (
<div>
{React.cloneElement(trigger, { onClick: toggle })}
<Modal isOpen={modal} toggle={toggle}>
<ModalBody>{children}</ModalBody>
</Modal>
</div>
);
};
Then you can use it like that:
<MyModal trigger={<Button variant="primary">Send</Button>}>
<p>This is the content.</p>
</MyModal>
Or you can implement it in a controlled way. This is more flexible as it allows you to render the triggering element anywhere:
const MyModal = ({ children, isOpen, toggle }) => (
<div>
<Modal isOpen={isOpen} toggle={toggle}>
<ModalBody>{children}</ModalBody>
</Modal>
</div>
);
Usage Example:
function App() {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
return (
<div className="App">
<Button variant="primary" onClick={toggle}>
Send
</Button>
<MyModal isOpen={isOpen} toggle={toggle}>
<p>This is the content.</p>
</MyModal>
</div>
);
}
You should pass the function which triggers the modal to your <Button /> component as prop. Then, in your component, you want to add the onClick event. You can't set an onClick event to the <Button />. It will think of onClick as a prop being passed to <Button />. Within <Button /> you can set the onClick event to an actual <button> element, and use the function which was passed in as a prop on that event.
You can use state to keep track of when the modal button is clicked. Your function can look like: (I am using class based components here, but you can do the same thing with functional components)
buttonClickedHandler = () => {
this.setState({isModalButtonClicked: !this.state.isModalButtonClicked});
}
Then, you can set the Modal component,
<Modal isShow={this.state.isModalButtonClicked} modalButton={this.buttonClickedHandler}>
<div> ...set contents of modal</div>
</Modal>
<button onClick={this.buttonClickedHandler}>Show Modal</button>
So, within the Modal component, you can have something like this:
<React.Fragment>
<Backdrop showModal={this.props.isShow} clicked={this.props.modalButton}/>
{this.props.children}
</React.Fragment>
Backdrop is basically the greyed out background. You can also set an onClick event to listen to when the backdrop is clicked.
Trying to dismiss a Modal dialog from within the Modal. I'm using ReactDOM.createPortal().
index.html
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<div id="modal"></div>
</body>
ResponseModal.js
import React from "react";
import ReactDOM from "react-dom";
// The gray background
const backdropStyle = {
...
};
// The modal "window"
const modalStyle = {
...
};
const MODAL_ROOT = document.querySelector("#modal");
const JSX_MODAL = ({ children, setShowResponses }) => (
<div style={backdropStyle}>
<div style={modalStyle}>
Child: {children}
<br />
<input
type="button"
value="Dismiss"
onClick={() => {
setShowResponses(false);
}}
/>
</div>
</div>
);
function Modal({ showResponses, ...props }) {
console.log("floop", showResponses);
if (showResponses) {
return ReactDOM.createPortal(JSX_MODAL(props), MODAL_ROOT);
}
return null;
}
export default Modal;
And finally, the containing component:
const LargeCell = ({ SCID, extra, fen, color }) => {
const [showResponses, setShowResponses] = useState(false);
return (
<div
style={{
...
}}
onClick={() => setShowResponses(true)}
>
<SmallCell {...{ SCID, color }} />
<DiagramForCell {...{ fen }} padding="3em"></DiagramForCell>
<span className="diff-text opening-text">{extra.opening.desc}</span>
<ResponsesModal {...{ showResponses, setShowResponses }}>
FLUM!
</ResponsesModal>
</div>
);
};
When I click on the LargeCell div, I see:
However, the dismiss button doesn't work. I'm sure that setShowResponses(false) is called, but there is no re-rendering of the Modal, so it is not dismissed. If I look at the Modal component in Chrome devtools, the state of showResponses still shows true.
So the question is: what is the correct way to dismiss this Modal?
So LargeCell was a table cell component, of which there were multiple. What I did was to push the Modal to the table level, and show/hide it from there:
{showResponses ? (
<ResponsesModal {...{ setShowResponses }}>FLUM!</ResponsesModal>
) : null}
setShowResponses is called by the Dismiss button in the Modal as shown previously.
The disadvantage is that to bring up the modal, each LargeCell needs setShowResponses, also. That prop has to be pushed several levels down. That's okay for now, but I'm starting to wonder if I should use a context.
I am using the Material UI library, and cannot figure out if there is a way to have 2 different click events happening on a List Item.
This is basically what I have:
<ListItem
leftAvatar={
<div onClick={() =>
insideAvatarFunction()}
>
{<Avatar />}
</div>
}
primaryText={primaryText}
onTouchTap={() => everywhereClickFunction}
/>);
The onTouchTap triggers, and ignores the onClick inside of the leftAvatar. If the outer onTouchTap is not there, then the onClick triggers fine.
Is there a way to have the onTouchTap to trigger when anywhere BESIDES the avatar is clicked?
Thanks!
Event.stopPropagation()
Prevents further propagation of the current event in the capturing and bubbling phases.
import React from 'react';
import {List, ListItem} from 'material-ui/List';
import Avatar from 'material-ui/Avatar';
export default class App extends React.Component {
insideAvatarFunction(e) {
e.stopPropagation();
console.log('Fired insideAvatarFunction()!');
}
everywhereClickFunction(e) {
console.log('Fired everywhereClickFunction()!');
}
render () {
return (
<List>
<ListItem primaryText="Inbox"
leftAvatar={
<div onClick={this.insideAvatarFunction}>
<Avatar />
</div>
}
onTouchTap={this.everywhereClickFunction}
onClick={this.everywhereClickFunction}
/>
</List>
);
}
}
So when clicked on the Avatar's node, the e.stopPropagation() line in the insideAvatarFunction should prevent the onClick/onTouchTap to further propagate to the parent's node which is the ListItem component.
Here is an interesting read about Javascript Events Order and event bubbling.
In case anyone else comes across this question, the comment by #zv.diego solved the issue.
I put stopPropagation() inside the function of the div's onClick, and switched the parent to onClick instead of onTouchTap and it works!
You can use Avtar wrapped with IconButton.
import { IconButton, Avatar } from '#mui/material'
<IconButton
onClick={() => alert('Hiii')}>
<Avatar
src={userProfilePic(post.user)}
sx={{ bgcolor: red[500] }}
aria-label="recipe">
post.user.firstName[0] + post.user.lastName[0]}
</Avatar>
</IconButton>