I encountered an issue trying to close the headlessui Disclosure modal inside the panel.
My goal is to have a button inside the panel which can close the modal.
The way I tried to solve this problem is doing it manually using useRef, but it works partially.
After opening the panel for the first time, you can close the modal but if you try to open it again, it doesn't work. Can't figure out how to solve this issue.
Any help will be appreciated.
Here is the codesandbox link
And here is the code
import { Disclosure } from "#headlessui/react";
import React, { useState, useRef } from "react";
import CloseIcon from "#material-ui/icons/Close";
import ExpandMoreIcon from "#material-ui/icons/ExpandMore";
const App = () => {
const [isClosed, setIsClosed] = useState(false);
const modalRef = useRef(null);
const hideModalHandler = (e) => {
e.preventDefault();
modalRef.current?.click();
setIsClosed(!isClosed);
};
return (
<Disclosure>
{({ open }) => (
<div ref={modalRef}>
<Disclosure.Button>
<span>modal</span>
<ExpandMoreIcon />
</Disclosure.Button>
{!isClosed && (
<Disclosure.Panel>
<CloseIcon onClick={hideModalHandler} />
<div>name</div>
</Disclosure.Panel>
)}
</div>
)}
</Disclosure>
);
};
export default App;
I haven't used headlessui Disclosure but I see that the function hideModalHandler isn't actually hiding but toggling. Did you mean setIsClosed(true) instead of setIsClosed(!isClosed)?
Also, after a quick look at the documentation, have you tried using the close from the headlessui Disclosure? You don't need useRef
Use the state, and wrap the disclosure button into a DIV
with onClick and some ID string to identify what disclosure must be open. Something like this (works for multiple disclosures):
const [keyOfOpenDisclosure, setKeyOfOpenDisclosure] = useState('')
const toggleDisclosure = (key: string) => {
setKeyOfOpenDisclosure((prev) => (prev !== key ? key : ''))
}
...
<Disclosure>
<div onClick={() => toggleDisclosure(someId)}>
<Disclosure.Button>
Text of disclosure button
</Disclosure.Button>
</div>
<Transition
show={someId === keyOfOpenDisclosure}
...
Related
Watch.js
import React from 'react'
import { useEffect, useState } from 'react'
import axios from 'axios'
const Watch = () => {
const [isPlaying, setIsPlaying] = useState(false)
const toggleVideo = () => {
setIsPlaying(!isPlaying)
}
return (
<button id='play' onClick={() => toggleVideo()} className='fa'>{isPlaying ? '' : ''}</button>
)
}
export default Watch
Here I have a simple function that toggles the isPlaying state and changes the button's icon, but the icon is not being displayed instead the unicode is, I tried setting it's innerHTML but it threw errors and said that it is null, what can I do?
JSX does not allow HTML entities as strings, it will escape those values.
// It displays "First · Second"
<div>{'First · Second'}</div>
From the page I linked above, one option is to use unicode escape values (e.g. '\uXXXX') instead:
<button id="play" onClick={() => toggleVideo()} className="fa">
{isPlaying ? "\uf04b" : "\uf04c"}
</button>;
as you said its not working with innerHTML
i tried like this its working.
<button id='play' onClick={() => toggleVideo()} className="fa">
<div dangerouslySetInnerHTML={{__html: `${isPlaying ? '' : '' }`}} />
</button>
I have an antd modal as shown in the below code, Now when I select Create Manual and click Next, I want to close this modal and open another Modal2 but another modal is not getting opened after clicking next.
Here is my code. ( Codesandbox live demo - link )
Please suggest a workaround to get his second modal generated. Thanks
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Modal, Button, Radio } from "antd";
const App = () => {
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectRadio, setselectRadio] = useState("preselect");
const showModal = () => {
setIsModalVisible(true);
};
const select = (e) => {
// you can save the value in here
setselectRadio(e.target.value);
console.log(e.target.value);
};
function modalclick() {
if (selectRadio === "preselect") {
alert("pre-select");
} else {
//---------------> UNABLE TO OPEN ANOTHER MODAL HERE <-------------------------------------
<Modal title="Create Test Suite" visible={isModalVisible}>
MODAL 2 COMES HERE
</Modal>;
alert("manual");
}
}
return (
<>
<Button type="primary" style={{ float: "right" }} onClick={showModal}>
Create Test Suite
</Button>
<Modal
title="Create Test Suite"
visible={isModalVisible}
footer={[
<Button key="cancel" onClick={() => setIsModalVisible(false)}>
Cancel
</Button>,
<Button type="primary" key="next" onClick={modalclick}>
Next
</Button>
]}
>
<Radio.Group
defaultValue="preselect"
buttonStyle="solid"
onChange={(e) => {
select(e);
}}
>
<Radio value="preselect">Create from Preselect</Radio>
<Radio value="manual">Create Manual</Radio>
</Radio.Group>
</Modal>
</>
);
};
ReactDOM.render(<App />, document.getElementById("container"));
To show the modal 2 you can use a useState hook or a useRef hook. In both methods, you need first to put this modal 2 in the return of your "App".
useState way: Just use a state to control the visibility, like how you do in modal 1, simple.
useRef way: This is a little more complex. You will need to use a useImperativeHandle inside the modal component, and create a function (inside too) to control the visibiliity. So, in your page, you can just call the function that is inside the component, to show the modal. Using this method, the logic about the state control of visibility leaves the page and goes into the component.
Modal2 is not visible because is out of the return of App function. Put Modal 2 inside return and try again.
I am using react-perfect-scrollbar to show images list.
Inside the perfect scroll bar, I am going to lazy load images. But it won't work.
import React, { useState, useCallback, useEffect } from 'react';
import PerfectScrollbar from 'react-perfect-scrollbar';
import useIsMountedRef from 'src/hooks/useIsMountedRef';
import LazyLoad from 'react-lazyload';
import {
Box,
Button,
Link,
List,
ListItem,
ListItemIcon,
ListItemText,
CircularProgress,
Typography,
makeStyles
} from '#material-ui/core';
const isMountedRef = useIsMountedRef();
const [images, setImages] = useState([]);
const getImages = useCallback(async () => {
try {
setLoading(true);
const response = await axios.get(`${backendUrl}/images/get/images`);
if (isMountedRef.current) {
setImages(response.data.projectImages);
}
} catch (err) {
console.error(err);
} finally {
if (isMountedRef.current) {
setLoading(false);
}
}
}, [isMountedRef]);
<PerfectScrollbar options={{ suppressScrollX: true }}>
<List className={classes.list}>
{images.map((image, i) => (
<ListItem
divider={i < images.length - 1}
key={i}
className={classes.listItem}
>
<ListItemIcon>
<LazyLoad height={90} key={i} overflow>
<img
src={`${awsS3Url}/${image.Key}`}
className={classes.listImage}
onClick={() => onSelect(`${awsS3Url}/${image.Key}`)}
/>
</LazyLoad>
</ListItemIcon>
<ListItemText
primary={GetFilename(image.Key)}
primaryTypographyProps={{ variant: 'h5' }}
secondary={bytesToSize(image.Size)}
className={classes.listItemText}
/>
<MoreButton
handleArchive={() => handleRemoveOne(image)}
/>
</ListItem>
))}
</List>
</PerfectScrollbar>
Some of images(the first view without scrolling) are showing.
When I scroll, the images won't load, only the list contents without images are showing.
What did I code wrong?
I had the same problem. The issue is that react-lazyload is trying to find a container that has its overflow property set to scroll or auto, but perfect-scrollbar sets its overflow property to hidden and handles the scrolling manually instead. So we have to tell react-lazyload manually which container it should monitor for scroll events.
This can, by documentation, be done in two ways; by passing an HTMLElement or a query selector string. Alas, there seems to be a bug in the library that causes the property to be ignored if it is not a string (https://github.com/twobin/react-lazyload/blob/055405125d0313014f0951cffc78345297f10a08/lib/index.js#L261) so currently the only way is to pass a query selector string.
But when I tried to pass a query selector string targeting the perfect-scrollbars container, it seems that the container might not always be there yet when react-lazyload attaches its event listeners, so we must check that the container is actually there before we initialize the LazyLoad-container.
So the relevant code is:
import React, { ReactElement, useRef, useState, useEffect } from 'react';
import PerfectScrollbar from 'react-perfect-scrollbar'
import LazyLoad from 'react-lazyload';
export default (): ReactElement => {
// Get a reference to the wrapper element so we know when it is created
const scrollbarWrapperRef = useRef(null);
// Initialize a state setter to notify the view when the scrollParent becomes available
const [scrollParent, setScrollParent] = useState<HTMLElement|null>(null);
// Adjust this selector to your liking
const scrollParentSelector = '#scrollbar-wrapper .scrollbar-container';
// Here we make sure that the PerfectScrollbar container is actually available before we let the content and the LazyLoads be created.
useEffect(() => {
const scrollParentElement = document.querySelector(scrollParentSelector);
if (scrollParentElement) {
setScrollParent(scrollParentElement);
}
}, [scrollbarWrapperRef.current]);
// The relevant DOM
return (
<div ref={scrollbarWrapperRef} id="scrollbar-wrapper">
<PerfectScrollbar>
{ scrollParent &&
<List>
{images.map((image, i) => (
<LazyLoad scrollContainer={scrollParentSelector}>
<img src="..." />
</LazyLoad>
))}
</List>
}
</PerfectScrollbar>
</div>
);
}
Thanks in advance for any help you can provide. I am trying to create a Modal is react and call a get request to load details of a task.
I have most of it working (I think), but essentially what I have done is createa custom Modal Hook that toggles two modals.
The second of the two modals is meant to open a task and render the task details in a form for editing but I am unable to get it working.
Here is the useModal hook:
import { useState } from "react";
const useModal = () => {
const [isShowing, setIsShowing] = useState(false);
const [secondModalIsShowing, secondModalSetIsShowing] = useState(false);
function toggle() {
setIsShowing(!isShowing);
}
function secondToggle() {
secondModalSetIsShowing(!secondModalIsShowing);
console.log("clicked");
}
return {
isShowing,
toggle,
secondModalIsShowing,
secondToggle,
};
};
export default useModal;
I then call the function for the secondToggle which fires the code below to render the modal. Now as you may see I have to comment out the section where it calls getTask() with match.params.id, as well as the component that is then meant to be rendered in the modal.
If I don't do that I get an error message with the following " Line 23:5: Expected an assignment or function call and instead saw an expression no-unused-expressions"
import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import TaskItem from "../tasks/task-item/TaskItem";
import { getTask } from "../../actions/task";
import ReactDOM from "react-dom";
import "./Modal.styles.scss";
import "../dashboard/Dashboard.styles.scss";
import Task from "../task/Task";
import TaskEdit from "../task/TaskEdit";
const TaskModal = ({
getTask,
task: { task, loading },
match,
secondModalIsShowing,
hide,
}) => {
const [displayEdit, toggleDisplayEdit] = useState(false);
useEffect(() => {
getTask();
// match.params.id;
}, [getTask]);
return secondModalIsShowing
? ReactDOM.createPortal(
<React.Fragment>
<button
type="submit"
value="toggle"
onClick={() => toggleDisplayEdit(!displayEdit)}
>
Show/Edit
</button>
{(displayEdit && <TaskItem task={task} />) || (
<div>{/* <TaskEdit /> */}</div>
)}
<div className="modal-overlay" />
<div
className="modal-wrapper"
aria-modal
aria-hidden
tabIndex={-1}
role="dialog"
>
<div className="modal">
<div className="modal-header">
Add New Task
<button
type="button"
className="modal-header__button"
data-dismiss="modal"
aria-label="Close"
onClick={hide}
>
<span aria-hidden="true">×</span>
</button>
</div>
</div>
</div>
</React.Fragment>,
document.body
)
: null;
};
Now if I render this EditTask component outside the modal as a normal component it works correctly. I can also get the modal to render when it's not trying to display the EditTask component.
As a result, I think it's related to the Route path and passing the response to the TaskModal component? When I click the modal to open I cant get it to render the URL with the task ID and therefore I cant render the details of the task in the modal.
<Route path="/taskedit/:id" component={TaskModal} />
For context, I think this guide comes close to solving my issue (https://blog.logrocket.com/building-a-modal-module-for-react-with-react-router/) but I am not familiar with working with class-based components and when I try and convert to functional-based components I'm running into even more issues.
Thanks in advance for any insight you can provide as I keep trying to work through this.
Paul
The first issue I am seeing is you have to pass the task id to TaskModal component
<Route path="/taskedit/:id"
render={(props) => <TaskModal {...props} />}>
</Route>
This will make the task id available as property in TaskModal.
Then in the TaskModal, fetch like below
let taskid = prop.match.params.id;
In our react app (we use reactstrap), we've multiple pages from where a confirmation modal can be shown. We do not want to include the modal code in every page. Is there a way to do this programmatically by invoking a method?
We can use plain bootstrap modals directly in the public index.html and from the util method use dom selector and invoke the modal but want to avoid this. Any pointers on how to go about this?
If what you want is only one modal which can be used across multiple pages(instead of putting one modal in every page), you can put it in the root component usually names as App.
import Modal from "somewhere";
function App() {
const [modal, setModal] = useState(false);
return <>
<Model isOpen={modal} />
{/* If you need to toggle modal when clicking something in PageA, you can pass the prop down like this */}
<PageA onToggleModel={()=>{setModal(!modal)}} />
<PageB />
</>;
}
Just in case, doing import Modal from "somewhere" in every page wouldn't result in duplicate code in your final bundle. It's totally fine to do that.
Here's what we did.
Alert Component:
import React, { useState } from "react";
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
const Alert = props => {
const [modal, setModal] = useState(props.open ? props.open : true);
const toggle = () => {
setModal(!modal);
if (props.cb) {
props.cb();
}
if (props.reloadPage) {
window.location.reload();
}
};
return (
<div>
<Modal isOpen={modal} toggle={toggle}>
<ModalHeader toggle={toggle}>{props.title}</ModalHeader>
<ModalBody>{props.text}</ModalBody>
<ModalFooter>
<Button color="primary" onClick={toggle}>
Ok
</Button>
</ModalFooter>
</Modal>
</div>
);
};
export default Alert;
Util.js:
import React from "react";
import ReactDOM from "react-dom";
import Alert from "./Alert";
const Util = {
alert: (message, okCb, reload) => {
ReactDOM.render(
<Alert
title="Done"
text={message}
cb={() => {
ReactDOM.unmountComponentAtNode(
document.getElementById("modalHolder")
);
if (okCb) {
okCb();
}
}}
reloadPage={reload}
/>,
document.getElementById("modalHolder")
);
}
};
export default Util;
In index.html we created a dom element:
<div id="modalHolder"></div>
So to invoke the modal imperatively, call:
Util.alert("Data has been saved")