I've got buttons that represent sports. When a user clicks on a button a Popper is opened with the sport's associated component/content. The same Popper is shared between all the buttons.
This works fine, but once the Popper is open it requires two clicks to open the Popper for another sport. One click to close the Popper and the second click to open it. So if I click the 'Baseball' button, I'll have to click the 'Basketball' button twice before the Popper is reopened with the Basketball content.
Is there any way to accomplish this with just a single click? Link to Sandbox
import React, { useEffect, useState } from 'react';
import { Popper, Button, Paper, Typography } from "#material-ui/core";
function CategoryMenuItemContent(props) {
return (
<>
<p>{props.menu.label}</p>
<p>{props.menu.content}</p>
</>
);
}
export default function App() {
const baseballCategory = {
label: 'Baseball',
content: <p>some content</p>
};
const basketballCategory = {
label: 'Basketball',
content: <p>some content</p>
};
const footballCategory = {
label: 'Football',
content: <p>some content</p>
};
const hockeyCategory = {
label: 'Hockey',
content: <p>some content</p>
};
const [activeCategoryMenuContent, setActiveCategoryMenuContent] = React.useState('baseball');
const categoryMenuItemContentComponents = {
"baseball": <CategoryMenuItemContent menu={baseballCategory} />,
"basketball": <CategoryMenuItemContent menu={basketballCategory} />,
"football": <CategoryMenuItemContent menu={footballCategory} />,
"hockey": <CategoryMenuItemContent menu={hockeyCategory} />,
};
const [anchorEl, setAnchorEl] = useState(null);
const handleClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
setActiveCategoryMenuContent(event.currentTarget.textContent.toLowerCase());
};
const open = Boolean(anchorEl);
const id = open ? 'simple-popper' : undefined;
return (
<>
<Button onClick={(event) => {handleClick(event);}}>Baseball</Button>
<Button onClick={(event) => {handleClick(event);}}>Basketball</Button>
<Button onClick={(event) => {handleClick(event);}}>Football</Button>
<Button onClick={(event) => {handleClick(event);}}>Hockey</Button>
<Popper id={id} open={open} anchorEl={anchorEl}>
{activeCategoryMenuContent === 'baseball' && categoryMenuItemContentComponents['baseball']}
{activeCategoryMenuContent === 'basketball' && categoryMenuItemContentComponents['basketball']}
{activeCategoryMenuContent === 'football' && categoryMenuItemContentComponents['football']}
{activeCategoryMenuContent === 'hockey' && categoryMenuItemContentComponents['hockey']}
</Popper>
</>
);
}
This would make more sense:
const handleClick = (event) => {
if (anchorEl === event.currentTarget) {
setAnchorEl(null)
setActiveCategoryMenuContent('')
} else {
setAnchorEl(event.currentTarget);
setActiveCategoryMenuContent(event.currentTarget.textContent.toLowerCase())
}
};
Related
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>
);
};
I want this popper to show when the "copy link" button is clicked to let the user know that it has been copied, but then disappear on its own after a second or two. Here is the code for the popper
import * as React from 'react';
import Box from '#mui/material/Box';
import Popper from '#mui/material/Popper';
export default function SimplePopper() {
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const open = Boolean(anchorEl);
const id = open ? 'simple-popper' : undefined;
return (
<div>
<button aria-describedby={id} type="button" onClick={handleClick}>
Copy Link
</button>
<Popper id={id} open={open} anchorEl={anchorEl}>
<Box sx={{ border: 1, p: 1, bgcolor: 'background.paper' }}>
Link Copied
</Box>
</Popper>
</div>
);
}
You might be able to do something with setTimeout in handleClick.
Try modifying handleClick like so:
const handleClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
setTimeout(() => setAnchorEl(null), 3000);
};
I am quite new to React and I need to make a dashboard, where I will have a table with some data.
One of the column in the table is clickable and when I click on a particular cell, it will take that cell value and display more details about that item "in a new tab".
Now this is easy with a browser, where you open a new tab on link click. But this is an app i am making on chromium. more of a desktop app.
But I do not want a new window whole together. I need a new tab panel to open with the previous table still there in one tab, and the details in the new tab.
so when I go back to the previous tab and click on another item, it opens a third tab with the details of this item.
Example below (Sorry I am not allowed to insert pictures yet. Please click the link to see them.)
1st picture with initial table.
First Picture With the initial table
Now, if I click on Accountant, a new tab should appear as in second image:
Second image with a new tab opened
I think I found a solution to this. I am pasting it here for someone looking for a solution.
import React, { useState, useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import { withStyles } from "#material-ui/core/styles";
import AppBar from "#material-ui/core/AppBar";
import { Tabs, Tab, IconButton } from "#material-ui/core";
import CloseIcon from "#material-ui/icons/Close";
import TabContainer from "../TabContainer/index.jsx";
const styles = theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
},
colorPrimary: {
color: "red"
}
});
export const TabsDemo = ({
tabs,
selectedTab = 1,
onClose,
tabsProps = { indicatorColor: "primary" },
...rest
}) => {
const [activeTab, setActiveTab] = useState(selectedTab);
const [activetabs, setActiveTabs] = useState([]);
// useEffect(() => {
// if (activeTab !== selectedTab) setActiveTab(selectedTab);
// }, [setActiveTab, selectedTab, activeTab]);
// const handleChange = useCallback(event => setActiveTab(event.target.value), [
// setActiveTab,
// ]);
useEffect(() => {
setActiveTabs(tabs);
}, [tabs]);
const handleChange = useCallback((event, activeTab) => {
setActiveTab(activeTab);
}, []);
const handleClose = useCallback(
(event, tabToDelete) => {
event.stopPropagation();
const tabToDeleteIndex = activetabs.findIndex(
tab => tab.id === tabToDelete.id
);
const updatedTabs = activetabs.filter((tab, index) => {
return index !== tabToDeleteIndex;
});
const previousTab =
activetabs[tabToDeleteIndex - 1] ||
activetabs[tabToDeleteIndex + 1] ||
{};
setActiveTabs(updatedTabs);
setActiveTab(previousTab.id);
},
[activetabs]
);
return (
<>
<div>
<Tabs value={activeTab} onChange={handleChange}>
{activetabs.map(tab => (
<Tab
key={tab.id}
value={tab.id}
label={
typeof tab.label === "string" ? (
<span>
{" "}
tab.label
{tab.closeable && (
<IconButton
component="div"
onClick={event => handleClose(event, tab)}
>
<CloseIcon />
</IconButton>
)}
</span>
) : (
tab.label
)
}
/>
))}
</Tabs>
{activetabs.map(tab =>
activeTab === tab.id ? (
<TabContainer key={tab.id}>{tab.component}</TabContainer>
) : null
)}
</div>
</>
);
};
TabsDemo.propTypes = {
classes: PropTypes.object.isRequired,
tabs: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
id: PropTypes.number.isRequired,
component: PropTypes.object.isRequired,
closeable: PropTypes.bool.isRequired
}).isRequired
).isRequired
};
export default withStyles(styles)(TabsDemo);
Below is the link of a code sandbox that you can use to understand this fully :
https://codesandbox.io/s/mui-closeable-tab-gw2hw?file=/src/components/tabsdemo/index.jsx:0-3123
I am trying to implement a context menu that should be displayed while the user is typing into a textfield using the MaterialUI context menu. The idea is to display suggestions in the context menu based on the currently typed in word in the textfield. So the context menu is being shown next to the caret input and should not disturb the user from continue typing.
But the problem is when the menu is displayed the focus is taken from the textfield and the user is unable to continue typing until the menu has been closed.
Is there a way to allow the menu to be shown and still keep the focus on the textfield allowing the user to continue typing?
<Menu
keepMounted
open={contextMenuOpen}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
{ top: caretPositionY, left: caretPositionX}
}
>
<MenuItem onClick={handleClose}>Suggestion1</MenuItem>
<MenuItem onClick={handleClose}>Suggestion2</MenuItem>
<MenuItem onClick={handleClose}>Suggestion3</MenuItem>
<MenuItem onClick={handleClose}>Suggestion4</MenuItem>
</Menu>
If you can't make it work with Material UI Menu, the here is the alternative:-
Demo.js (using simple ul element with package classnames):-
import React, { useState, useEffect } from "react";
import classNames from "classnames";
import { makeStyles } from "#material-ui/core/styles";
import { Button, TextField } from "#material-ui/core";
import "./style.css";
const Demo = () => {
const classes = useStyles();
const [search, setSearch] = useState("");
const [openMenu, setOpenMenu] = useState(false);
const [menuItems, setMenuItems] = useState([
{ id: 1, name: "Profile" },
{ id: 2, name: "My Account" },
{ id: 3, name: "Logout" }
]);
const [menuItemsFiltered, setMenuItemsFiltered] = useState(menuItems);
const handleClose = () => {
setOpenMenu(false);
};
const handleSearch = value => {
if (value === "") {
setOpenMenu(false);
setMenuItemsFiltered(menuItems);
} else {
setOpenMenu(true);
setMenuItemsFiltered(() =>
menuItems.filter(
item => item.name.toLowerCase().indexOf(value.toLowerCase()) > -1
)
);
}
};
useEffect(() => {
handleSearch(search);
}, [search]);
return (
<div className={classes.root}>
<TextField
type="search"
value={search}
onChange={e => setSearch(e.target.value)}
/>
<div>
<Button
aria-controls="simple-menu"
aria-haspopup="true"
onClick={() => setOpenMenu(!openMenu)}
>
Open Menu
</Button>
<ul
className={
openMenu ? classes.menu : classNames(classes.menu, classes.menuHide)
}
>
{menuItemsFiltered.map(item => (
<li
key={item.id}
className={classes.menuItem}
onClick={() => handleClose()}
>
{item.name}
</li>
))}
</ul>
</div>
</div>
);
};
export default Demo;
const useStyles = makeStyles(theme => ({
root: {
display: "flex"
},
menu: {
listStyle: "none",
padding: "0.5rem",
backgroundColor: theme.palette.grey[100],
"& li:not(:last-child)": {
marginBottom: "1rem"
}
},
menuHide: {
display: "none"
},
menuItem: {
"&:hover": {
cursor: "pointer",
color: theme.palette.primary.main
}
}
}));
You can see the working demo here in sandbox.
Update 2022
https://codesandbox.io/s/react-insert-context-menu-value-into-textarea-rhq5wq?file=/src/App.tsx
Addtional feature is setting cursor after the pasted value
import { Box, Menu, MenuItem, TextField, Typography } from "#mui/material";
import React, { useLayoutEffect } from "react";
import { useRef, useState } from "react";
export default function App() {
const [value, setValue] = useState(
"Lorem ipsum, dolor sit amet consectetur adipisicing elit. Accusamus, nemo. Doloremque eligendi aliquam repellendus reiciendis doloribus excepturi asperiores quaerat corporis."
);
const [selectionEnd, setSelectionEnd] = useState(0);
const textareaRef = useRef();
const [contextMenu, setContextMenu] = React.useState<{
mouseX: number;
mouseY: number;
} | null>(null);
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
setContextMenu(
contextMenu === null
? {
mouseX: event.clientX + 2,
mouseY: event.clientY - 6
}
: // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
// Other native context menus might behave different.
// With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
null
);
};
const handleClose = () => {
setContextMenu(null);
};
const insertText = (text) => () => {
const selectionStart = textareaRef.current.selectionStart;
const selectionEnd = textareaRef.current.selectionEnd;
const insertText = ` {{${text}}} `;
setSelectionEnd(selectionEnd + insertText.length);
const newValue = `${value.substring(
0,
selectionStart
)}${insertText}${value.substring(selectionEnd, value.length)}`;
setValue(newValue);
handleClose();
};
useLayoutEffect(() => {
//Sets the cursor at the end of inserted text
textareaRef.current.selectionEnd = selectionEnd;
}, [selectionEnd]);
return (
<Box onContextMenu={handleContextMenu}>
<Typography mb={2}>Right click on textarea to add some text</Typography>
<TextField
label="Textarea"
multiline
minRows={10}
inputRef={textareaRef}
value={value}
onChange={({ target }) => setValue(target.value)}
fullWidth
/>
<Menu
open={contextMenu !== null}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
contextMenu !== null
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
: undefined
}
>
<MenuItem onClick={insertText("VarA")}>VarA</MenuItem>
<MenuItem onClick={insertText("VarB")}>VarB</MenuItem>
</Menu>
</Box>
);
}
Here's what I've tried doing
import React, { Fragment, useState } from "react";
const Accordion = ({ items }) => {
const [activeIndex, setActiveIndex] = useState(null);
const [display, setDisplay] = useState("");
const handleChange = (index) => {
setActiveIndex(index);
setDisplay(display === "active" ? "" : "active");
};
const renderedItems = items.map((item, index) => {
return (
<Fragment key={item.title}>
<div className={`${display} title`} onClick={() => handleChange(index)}>
<i className="dropdown icon"></i>
{item.title}
</div>
<div className={`${display} content`}>
<p>{item.content}</p>
</div>
</Fragment>
);
});
return (
<div className="ui styled accordion">
{renderedItems}
{activeIndex}
</div>
);
};
export default Accordion;
But on clicking a single item, it expands or collapses all others. Also, I want only one expanded item at any given time, so if a user clicks another item the previous one should close automatically.
Here is my App.js
import React from "react";
import Accordion from "./components/Accordion";
const items = [
{
title: "What is React?",
content: "React is a front end javascript library",
},
{
title: "What is Angular?",
content: "Angular is a front end javascript framework",
},
{
title: "What is Vue?",
content: "Vue is a front end javascript library",
},
];
const App = () => {
return (
<div>
<Accordion items={items} />
</div>
);
};
export default App;
For your issue :
// Means if display is active so nothing else it's active, so everything is active
setDisplay(display === "active" ? "" : "active");
The display variable is shared between each accordion bodies... Always the same
Do you really need an display var ?
import React, { Fragment, useState } from "react";
const Accordion = ({ items }) => {
const [activeIndex, setActiveIndex] = useState(null);
const handleChange = index => {
setActiveIndex(activeIndex === index ? null : index);
};
const renderedItems = items.map((item, index) => {
return (
<Fragment key={item.title}>
<div className={`${display} title`} onClick={() => handleChange(index)}>
<i className="dropdown icon"></i>
{item.title}
</div>
<div className={`${activeIndex === index ? 'active' : ''} content`}>
<p>{item.content}</p>
</div>
</Fragment>
);
});
return (
<div className="ui styled accordion">
{renderedItems}
{activeIndex}
</div>
);
};
export default Accordion;