I am implementing two text fields next to each other and they have end adornments as a buttons. Those buttons toggle popper visibility. Also each popper has clickawaylistener so the popper is closed when mouse clicks outside popper. If first popper is opened it should be closed when I click button of second text field adornment. Issue is that end adornments have event propagation stopped. I do that to prevent clickaway event when clicking adornment, so to prevent instant closing of popper when it is opened by toggle handler.
I was thinking about wrapping TextField into ClickAwayListener but it didn't work.
P.S. Both TextField will be rendered from separate components and I don't want to share any props between them as they should be independent.
https://codesandbox.io/s/basictextfields-material-demo-forked-rykrx
const [firstPopperVisible, setFirstPopperVisible] = React.useState(false);
const [secondPopperVisible, setSecondPopperVisible] = React.useState(false);
const firstTextFieldRef = React.useRef();
const secondTextFieldRef = React.useRef();
const toggleFirstPopperVisible = (e) => {
e.stopPropagation();
setFirstPopperVisible((prev) => !prev);
};
const handleFirstPopperClickAway = (e) => {
setFirstPopperVisible(false);
};
const toggleSecondPopperVisible = (e) => {
e.stopPropagation();
setSecondPopperVisible((prev) => !prev);
};
const handleSecondPoppertClickAway = (e) => {
setSecondPopperVisible(false);
};
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<div>
<TextField
label="Outlined"
variant="outlined"
inputRef={firstTextFieldRef}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
edge="end"
onClick={toggleFirstPopperVisible}
>
<Visibility />
</IconButton>
</InputAdornment>
)
}}
/>
<ClickAwayListener onClickAway={handleFirstPopperClickAway}>
<Popper
open={firstPopperVisible}
anchorEl={firstTextFieldRef.current}
placement="bottom-start"
>
Content
</Popper>
</ClickAwayListener>
</div>
<div>
<TextField
label="Outlined"
variant="outlined"
inputRef={secondTextFieldRef}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
edge="end"
onClick={toggleSecondPopperVisible}
>
<Visibility />
</IconButton>
</InputAdornment>
)
}}
/>
<ClickAwayListener onClickAway={handleSecondPoppertClickAway}>
<Popper
open={secondPopperVisible}
anchorEl={secondTextFieldRef.current}
placement="bottom-start"
>
Content
</Popper>
</ClickAwayListener>
</div>
</div>
);
}
EDIT: Found a temporary solution by wrapping TextField into div and then wrapping tat ClickawayListener. Also prevented propagation on popper itself where needed. This is not ideal, but for my case it worked.
<ClickAwayListener onClickAway={handleFirstPopperClickAway}>
<div style={{display: inline-box}>
<TextField>
.....
</TextField>
</div>
</ClickawayListener>
<Popper>
....
</Popper>
UPDATED
Wrap the ClickAwayListener in a conditional statement:
{firstPopperVisible && (
<ClickAwayListener onClickAway={handleFirstPopperClickAway}>
<Popper open={firstPopperVisible} anchorEl={firstTextFieldRef.current} placement="bottom-start">
Content
</Popper>
</ClickAwayListener>
)}
...
{secondPopperVisible && (
<ClickAwayListener onClickAway={handleSecondPoppertClickAway}>
<Popper open={secondPopperVisible} anchorEl={secondTextFieldRef.current} placement="bottom-start">
Content
</Popper>
</ClickAwayListener>
)}
Codesandbox Demo
PREVIOUS
I recommend you look at Portals for this. Instead of having multiple elements in the dom, you have one that gets added where needed, as needed.
Portals provide a first-class way to render children into a DOM node
that exists outside the DOM hierarchy of the parent component.
Your single Portal component:
import ReactDOM from 'react-dom';
import React, { useEffect, useState } from 'react';
const Component = ({ content, handleCloseClick }) => {
return <div onClick={handleCloseClick}>{content}</div>;
};
interface PortalProps {
isShowing: boolean;
content: any;
location: any;
handleCloseClick: () => void;
}
const Portal = ({ isShowing, content, handleCloseClick, location }: PortalProps) => (isShowing ? ReactDOM.createPortal(<Component handleCloseClick={handleCloseClick} content={content} />, location.current) : null);
export default Portal;
Which is then used once in your main component:
import React, { useState, useRef } from 'react';
import Widget from './Widget';
import './styles.css';
export default function App() {
const [isShowing, setIsShowing] = useState<boolean>(false);
const [content, setContent] = useState<string>();
const buttonRef = useRef(null);
const handleClick = (e) => {
const { target } = e;
buttonRef.current = e.target.parentNode;
setContent(target.dataset.content);
setIsShowing(true);
};
const handleCloseClick = () => {
buttonRef.current = null;
setContent('');
setIsShowing(false);
};
return (
<div className="App">
<div>
<button onClick={handleClick} data-content={`content for one`}>
One
</button>
</div>
<div>
<button onClick={handleClick} data-content={`content for two`}>
Two
</button>
</div>
<Widget isShowing={isShowing} location={buttonRef} content={content} handleCloseClick={handleCloseClick} />
</div>
);
}
Codesandbox Demo
Related
I've got a simple setup for some MUI tabs that I'm working to implement some drag and drop functionality to. The issue I'm running into is that when the SortableContext is nested inside of the TabList component, drag and drop works but the values no longer work for the respective tabs. When I move the SortableContext outside of the TabList component the values work again, but the drag and drop doesn't. If anybody has any guidance here that would be greatly appreciated!
Here is a link to a CodeSandbox using the code below: https://codesandbox.io/s/material-ui-tabs-with-drag-n-drop-functionality-05ktf3
Below is my code snippet:
import { Box } from "#mui/material";
import { useState } from "react";
import { DndContext, closestCenter } from "#dnd-kit/core";
import { arrayMove, SortableContext, rectSortingStrategy } from "#dnd-kit/sortable";
import SortableTab from "./SortableTab";
import { TabContext, TabList } from "#mui/lab";
const App = () => {
const [items, setItems] = useState(["Item One", "Item Two", "Item Three", "Item Four", "Item Five"]);
const [activeTab, setActiveTab] = useState("0");
const handleDragEnd = (event) => {
const { active, over } = event;
console.log("Drag End Called");
if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
const handleChange = (event, newValue) => {
setActiveTab(newValue);
};
return (
<div>
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabContext value={activeTab}>
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={items} strategy={rectSortingStrategy}>
<TabList onChange={handleChange} aria-label="basic tabs example">
{items.map((item, index) => (
<SortableTab value={index.toString()} key={item} id={item} index={index} label={item} />
))}
</TabList>
</SortableContext>
</DndContext>
</TabContext>
</Box>
</Box>
</div>
);
};
export default App;
import { useSortable } from "#dnd-kit/sortable";
import { CSS } from "#dnd-kit/utilities";
import { Tab, IconButton } from "#mui/material";
import DragIndicatorIcon from "#mui/icons-material/DragIndicator";
const SortableTab = (props) => {
const { attributes, listeners, setNodeRef, transform, transition, setActivatorNodeRef } = useSortable({
id: props.id,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} {...attributes} style={style}>
<Tab {...props} />
<IconButton ref={setActivatorNodeRef} {...listeners} size="small">
<DragIndicatorIcon fontSize="5px" />
</IconButton>
</div>
);
};
export default SortableTab;
You can make drag and drop tabs work by moving <SortableContext> inside the <TabList> component something like the below. Then change sorting started to horizontalListSortingStrategy.
In this case, your Dnd will work, but you will lose all MUI transitions/animations. Because now DndContext is overriding MuiContext. The best solution to create something like this is to create custom Tabs components where your context does not depend on TabContext.
I hope it helps.
<TabContext value={activeTab}>
<DndContext
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<TabList onChange={handleChange} aria-label="basic tabs example">
<SortableContext
items={items}
strategy={horizontalListSortingStrategy}
>
{items.map((item, index) => (
<SortableTab
value={index.toString()}
key={item}
id={item}
index={index}
label={item}
/>
))}
</SortableContext>
</TabList>
</DndContext>
</TabContext>
So in order for this to work I ended up figuring out that without a draghandle on the Tab the click event would only either fire for the drag event or setting the value depending on where the SortableContext was placed. This was my solution:
SortableTab
import { useSortable } from "#dnd-kit/sortable";
import { CSS } from "#dnd-kit/utilities";
import { Tab, IconButton } from "#mui/material";
import MoreVertRoundedIcon from "#mui/icons-material/MoreVertRounded";
const SortableTab = (props) => {
const { attributes, listeners, setNodeRef, transform, transition, setActivatorNodeRef } = useSortable({
id: props.id,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} {...attributes} style={style}>
<Tab {...props} />
<IconButton ref={setActivatorNodeRef} {...listeners} size="small">
<MoreVertRoundedIcon fontSize="5px" />
</IconButton>
</div>
);
};
export default SortableTab;
DndContext code chunk:
const renderedTab = selectedScenario ? (
scenarioModels.map((item, index) => <SortableTab key={item} label={models.data[item].model} id={item} index={index} value={index + 1} onClick={() => handleModelClick(item)} />)
) : (
<Tab label="Pick a Scenario" value={0} />
);
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={scenarioModels} strategy={horizontalListSortingStrategy}>
<Tabs value={selectedTab} onChange={handleChange} centered>
{selectedScenario ? <Tab label="Source Data" /> : ""}
{renderedTab}
</Tabs>
</SortableContext>
</DndContext>
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 created a sandbox: https://codesandbox.io/s/happy-rgb-vks06?file=/src/App.js
I am trying to pass props to the SlotSettings component, but I get this error:
Warning: Function components cannot be given refs. Attempts to access
this ref will fail. Did you mean to use React.forwardRef()?
I tried to read both Bootstrap docs and React docs but I could not understand how this should work.
This is the code I'm using:
const SlotSettings = props => {
console.log(props.hello); // this works
return <Popover {...props} id="popover-basic">
<Popover.Title as="h3">Popover right</Popover.Title>
<Popover.Content>
And here's some <strong>amazing</strong> content. It's very engaging.
right?
</Popover.Content>
</Popover>
}
const getDaySlots = slots => {
if (slots.length >= 1) {
return slots.map(slot => {
const variant = slot.status === "free" ? "success" : "secondary";
const buttonRef = createRef();
return (
<OverlayTrigger
key={uuid()}
trigger="click"
placement="bottom"
overlay={<SlotSettings hello="hello" />}
rootClose
>
<Button size="sm" ref={buttonRef} variant={variant} style={{ margin: "8px"}}>{slot.start}</Button>
</OverlayTrigger>
)
});
}
return "No lessons available."
}
I accomplished this by utilizing the popperConfig property of OverlayTrigger.
PopperConfig is used to pass and object to the the underlying popper instance.
link to docs
Simple example:
function renderTooltip(props) {
let message = ""
//Sometimes, props.popper.state is undefined.
//It runs this function enough times that state gets a value
if (props.popper.state) {
message = props.popper.state.options.testObj
}
return (
<Tooltip id="button-tooltip" {...props}>
{message}
</Tooltip>
);
}
function getDaySlots(slots) {
//Other stuff
return (
<OverlayTrigger
placement="right"
delay={{ show: 250, hide: 400 }}
overlay={renderTooltip}
popperConfig={{testObj:"hello there"}}
>
<Button variant="success">Click here</Button>
</OverlayTrigger >
);
}
I messed with your codesandbox, but couldn't get popper to get a state value for some reason. What I posted above works for my project, hope this helps you get started.
import React, { useState ,useRef} from 'react';
import { Popover, Overlay } from 'react-bootstrap';
const DemoComponent = () => {
const [show, setShow] = useState(false);
const target = useRef(null);
return (
<div className="">
<span ref={target} onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
open tooltip
</span>
<Overlay target={target.current} show={show} placement="top">
{(props) => (
<Popover id="popover-basic" className="customize-tooltip" {...props}>
<Popover.Body>
put here dyanamic content
</Popover.Body>
</Popover>
)}
</Overlay>
</div>
);}
export default DemoComponent;
There's two components the Drawer and the AppBar. App Bar has a menu button, and it must fire an event that opens the drawer, I'm stuck in how to make this, i tried to add the "onClick={toggleDrawer('left', true)}" line that exists in the button from Drawer to the IconButton in the TopBar, and this went wrong. How to say to the TopBar that "toggleDrawer" is a function from MyDrawer?
MyDrawer.tsx
import React from 'react';
import Drawer from '#material-ui/core/Drawer';
export default function MyDrawer() {
const classes = useStyles();
const [state, setState] = React.useState({
top: false,
left: false,
bottom: false,
right: false,
});
type DrawerSide = 'top' | 'left' | 'bottom' | 'right';
const toggleDrawer = (side: DrawerSide, open: boolean) => (
event: React.KeyboardEvent | React.MouseEvent,
) => {
if (
event.type === 'keydown' &&
((event as React.KeyboardEvent).key === 'Tab' ||
(event as React.KeyboardEvent).key === 'Shift')
) {
return;
}
setState({ ...state, [side]: open });
};
//drawer list of links
return (
<div>
{/* <Button onClick={toggleDrawer('left', true)}>Open Left</Button> */}
<Drawer open={state.left} onClose={toggleDrawer('left', false)}>
{sideList('left')}
</Drawer>
</div>
);
TopBar.tsx
import MyDrawer from '../Drawer/Drawer'
export default function TopBar() {
const renderDrawer = (
<OlimpoDrawer/>
);
return (
<div className={classes.grow}>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="abrir menu"
>
<MenuIcon />
</IconButton>
//Toolbar features
</Toolbar>
</AppBar>
{renderDrawer}
</div>
);
}
Let me understand your question. You want the IconButton to do the same thing like Button, commented, in MyDrawer.
If that's what you want. You should create and pass the function from TopBar into MyDrawer. toggleDrawer should be defined in TopBar and then put it into the onClick of both IconButton and Button which you had commented.
setState(preState => ({ ...preState, [side]: open }));
Note: setState by this way is better.
How to make a react component call a function from another Material UI Typescript
//...
const toggleDrawer = (side: DrawerSide, open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
if (/* ... */)
) {
return;
}
setState({ ...state, [side]: open });
};
Because toggleDrawer depends on setState which is scoped only inside the MyDrawer functional component so it needs to stay inside toggleDrawer, it's not possible to do something like export TopBar the function for TopBar to use directly.
If all you need is for somewhere to store the open states and for other components to share a callback function, I suggest either use a higher component (which they both share) or Redux to store MyDrawer's open state.
In the end, I though more "out of the box" and made the easiest
In MyDrawer.tsx I changed the render creating a menu button just like the drawer one
return (
<div>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="abrir menu"
onClick={toggleDrawer('left', true)}
>
<MenuIcon />
</IconButton>
<Drawer open={state.left} onClose={toggleDrawer('left', false)}>
{sideList('left')}
</Drawer>
</div>
);
}
In TopBar.tsx
<div className={classes.grow}>
<AppBar position="static">
<Toolbar>
<MyDrawer/>
{/* <IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="abrir menu"
>
<MenuIcon />
</IconButton> */}
I am trying to set up a toolbar on slate using React Hooks. Something seems to be going wrong, and I can't figure out what. Here is my code:
const renderMark = (props, editor, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
case "bold":
return <strong {...attributes}>{children}</strong>
case "italic":
return <i {...attributes}>{children}</i>
case "underline":
return <u {...attributes}>{children}</u>
default:
return next()
}
}
const onClickMark = (event, type, editor) => {
event.preventDefault()
editor.toggleMark(type)
}
<>
<Toolbar>
<Button onPointerDown={event => onClickMark(event, "bold")}>
<BoldIcon />
</Button>
<Button onPointerDown={event => onClickMark(event, "italic")}>
<ItalicIcon />
</Button>
<Button onPointerDown={event => onClickMark(event, "underline")}>
<UnderlineIcon />
</Button>
</Toolbar>
<Editor
onChange={handleChange}
onKeyDown={onKeyDown}
renderMark={renderMark}
renderNode={renderNode}
value={value}
/>
</>
Any idea why this doesn't work?
The issue was that I needed to bind the toolbar to the editor. I did that using the useRef hook as follows.
const editorRef = useRef()
const onClickMark = (event, type, editorRef) => {
event.preventDefault()
editorRef.current.toggleMark(type)
}
return (
<>
<Toolbar>
<Button onPointerDown={event => onClickMark(event, "bold", editorRef)}>
<BoldIcon />
</Button>
...
</Toolbar>
<StyledEditor
...
ref={editorRef}
...
/>
</>