How do I provide a container for popover component? - reactjs

I decided to use react-simple-popover for my popover needs. Documentation is here: https://github.com/dbtek/react-popover. The problem is that the documentation is for stateful components (class components). The container prop takes in this of the current class component, but I am using stateless component with hooks. What do I pass in place of the "this" keyword in this way?
import React, { useRef, useState } from "react";
import styled from "styled-components";
import Popover from "react-simple-popover";
import { HelpQuestion } from "../UI/Common";
const Wrapper = styled.div`
margin-bottom: 1rem;
`;
export const TextInput = styled.input`
border: 1px solid
${(props) => (props.dark ? "#37394B" : props.theme.lightGray)};
padding: 0.5rem 1rem;
width: 100%;
border-radius: 10px;
color: ${(props) => (props.dark ? "white" : "black")};
background-color: ${(props) => (props.dark ? "#37394B" : "white")};
`;
export default function TextField({
label,
value,
onChange,
onClick,
error,
type,
placeholder,
help,
dark,
disabled,
}) {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const popoverRef = useRef(null);
const componentRef = useRef(null);
return (
<Wrapper ref={componentRef}>
<div style={{ display: "flex", alignItems: "center" }}>
<TextInput
value={value}
onChange={!disabled ? onChange : () => {}}
onClick={onClick}
placeholder={placeholder}
type={type}
dark={dark}
disabled={disabled}
/>
{help && (
<div
style={{ marginLeft: "0.5rem" }}
onClick={() => setPopoverOpen(!isPopoverOpen)}
ref={popoverRef}
>
<HelpQuestion className="fas fa-question" />
</div>
)}
</div>
{error && <Error>{error}</Error>}
<Popover
placement="left"
container={componentRef.current} //doesnt work
target={popoverRef}
show={isPopoverOpen}
onHide={() => setPopoverOpen(false)}
>
<p>{help}</p>
</Popover>
</Wrapper>
);
}

How do I provide a container for popover component?
What do I pass in place of the "this" keyword in this way?
No, I don't think you can. View the source.
const Popover = props => {
if (
ReactDOM.findDOMNode(props.container) &&
ReactDOM.findDOMNode(props.container).parentElement.parentElement !==
document.body
) {
ReactDOM.findDOMNode(props.container).style.position = 'relative';
}
return (
<Overlay
show={props.show}
onHide={props.onHide}
placement={props.placement}
container={props.container}
target={p => ReactDOM.findDOMNode(props.target)}
rootClose={props.hideWithOutsideClick}
>
<PopoverContent
showArrow={props.showArrow}
arrowStyle={props.arrowStyle}
innerStyle={props.style}
style={props.containerStyle}
>
{props.children}
</PopoverContent>
</Overlay>
);
};
It doesn't appear to be well maintained and it's using ReactDOM.findDOMNode which is practically deprecated. I tried this and also tried a small class-based component wrapper. Nothing worked. Each time the reported error referred to the current ref value (componentRef.current) not being a react component.
For what its worth I suggest using a more functional component friendly Popover component. Here's an example using Material-UI's Popover component.
function TextField({
label,
value,
onChange,
onClick,
error,
type,
placeholder,
help,
dark,
disabled
}) {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const clickHandler = (e) => {
setPopoverOpen((open) => !open);
setAnchorEl(e.target);
};
return (
<Wrapper>
<div style={{ display: "flex", alignItems: "center" }}>
<TextInput
value={value}
onChange={!disabled ? onChange : () => {}}
onClick={onClick}
placeholder={placeholder}
type={type}
dark={dark}
disabled={disabled}
/>
{help && (
<div style={{ marginLeft: "0.5rem" }} onClick={clickHandler}>
<HelpOutlineIcon />
</div>
)}
</div>
{error && <Error>{error}</Error>}
<Popover
anchorEl={anchorEl}
open={isPopoverOpen}
onClose={() => setPopoverOpen(false)}
anchorOrigin={{
vertical: "center",
horizontal: "left"
}}
transformOrigin={{
vertical: "center",
horizontal: "right"
}}
>
<p>{help}</p>
</Popover>
</Wrapper>
);
}

Related

React, onMouseOver not working on child component but it does in button

Given the following code: -codesandbox-
import "./styles.css";
import React from "react";
const B = () => {
return (
<div>
<C>C inside B</C>
</div>
);
};
const C = ({ children, estado = "lightgrey" }) => {
const handleBoxToggle = (e) =>
(e.target.style.backgroundColor = "blue !important");
return (
<div
style={{
backgroundColor: estado,
display: "flex",
alignItems: "center",
justifyContent: "center",
minWidth: "1rem",
minHeight: "1rem",
outline: "1px solid black"
}}
onMouseOver={(e) => handleBoxToggle(e)}
>
{children}
</div>
);
};
export default function App() {
return (
<div className="App">
<button onMouseOver={() => alert("hi")}>Alert on hover Button</button>
<B></B>
<C>Alert DIV estado</C>
</div>
);
}
Why does the button display the alert on mouse over but the component 'C' does not? Shouldn't it have the function integrated? How can I make the mouseover on 'C' Component work? (it will be created dinamically many times).
You didn't call your function handleBoxToggle inside onMouseOver event callback
Pass it like this onMouseOver={handleBoxToggle}
You need to call your funtion like
onMouseOver={handleBoxToggle}
OR if you want to pass any arguments to this funtion you can call like
onMouseOver={() => handleBoxToggle()}

How to access values from context in a separate functional component

I'm trying to build a simple light mode/dark mode into my app I saw this example on Material UI for light/dark mode but I'm not sure how I can get access to the value for when the user clicks toggleColorMode in my Header component if it's being set in toggleColorMode function?
I guess my question is how can I get access to the value of light/dark mode of the context in my Header component if it's in a different function?
Here is my code.
import React, { useState, useEffect } from "react";
import MoreVertIcon from "#mui/icons-material/MoreVert";
import DarkModeIcon from "#mui/icons-material/DarkMode";
import LightModeIcon from "#mui/icons-material/LightMode";
import Paper from "#mui/material/Paper";
import { useTheme, ThemeProvider, createTheme } from "#mui/material/styles";
import IconButton from "#mui/material/IconButton";
import Navigation from "../Navigation/Navigation";
const ColorModeContext = React.createContext({ toggleColorMode: () => {} });
export const Header = (props) => {
const { mode } = props;
const theme = useTheme();
const colorMode = React.useContext(ColorModeContext);
console.log("mode is...", mode);
return (
<div className="header-container">
<Paper
elevation={3}
style={{ backgroundColor: "#1F1F1F", padding: "15px" }}
>
<div
className="header-contents"
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div
className="logo"
style={{ display: "flex", alignItems: "center" }}
>
<img
src="/images/header-logo.png"
alt="URL Logo Shortener"
width={"50px"}
/>
<h1 style={{ color: "#ea80fc", paddingLeft: "20px" }}>
URL Shortener
</h1>
</div>
<div className="settings">
<IconButton
sx={{ ml: 1 }}
onClick={colorMode.toggleColorMode}
color="inherit"
aria-label="dark/light mode"
>
{theme.palette.mode === "dark" ? (
<DarkModeIcon
style={{
cursor: "pointer",
marginRight: "10px",
}}
/>
) : (
<LightModeIcon
style={{
cursor: "pointer",
marginRight: "10px",
}}
/>
)}
</IconButton>
<IconButton aria-label="settings">
<MoreVertIcon style={{ color: "#fff", cursor: "pointer" }} />
</IconButton>
</div>
</div>
</Paper>
{/* Navigation */}
<Navigation />
</div>
);
};
export default function ToggleColorMode() {
const [mode, setMode] = React.useState("light");
const colorMode = React.useMemo(
() => ({
toggleColorMode: () => {
setMode((prevMode) => (prevMode === "light" ? "dark" : "light"));
},
}),
[]
);
const theme = React.useMemo(
() =>
createTheme({
palette: {
mode,
},
}),
[mode]
);
return (
<ColorModeContext.Provider value={colorMode}>
<ThemeProvider theme={theme}>
<Header mode={mode} />
</ThemeProvider>
</ColorModeContext.Provider>
);
}
Read the documentation: createContext, useContext. You need to render a ContextProvider in your parent (or top-level) component, then you can get the data in any component in the tree like const { theme } = useContext(ColorModeContext);.
You don't need to pass the mode as props, put it as one of the values in the context and access it.
Here's how you would render it in your example:
<ColorModeContext.Provider value={{colorMode, theme}}>
<Header />
</ColorModeContext.Provider>
You can pass an object inside the value in the context provider, in other word you can pass the toggle function inside your value to be consumed in the childern. thus you gain an access to change your mode state.
Note that the way changes are determined can cause some issues when passing objects as value, this might trigger unnecessary rerendering see Caveats for more info. or refer to the useContext docs
<ColorModeContext.Provider
value={{ colorMode: colorMode, toggleColorMode: toggleColorMode }}
>
<ThemeProvider theme={theme}>
<Header />
</ThemeProvider>
</ColorModeContext.Provider>

React JS Material UI Select IconComponent (Dropdown Icon) avoid rotating

By default, in React JS Material UI's Select component, when we provide a custom IconComponent, it gets turned upside down when user has selected the dropdown / Select component.
Sample code:
<Select
multiple
variant="outlined"
MenuProps={CustomMenuProps}
IconComponent={Search}
renderValue={(selected) => (selected as string[]).join(', ')}
{...props}
>
...
I did a sneaky thing to remove "MuiSelect-iconOpen" from the className when calling IconComponent.
Sample Code after my fix:
<Select
multiple
variant="outlined"
MenuProps={CustomMenuProps}
IconComponent={({ className }) => {
className = className.replace("MuiSelect-iconOpen", "")
return <Search className={className} />
}}
renderValue={(selected) => (selected as string[]).join(', ')}
{...props}
>
....
Now is there a better way to do this without replacing the className?
My current solution is to overwrite the original iconOpen class provided by the Material-UI Select.
....
import { makeStyles } from "#material-ui/core";
const useStyles = makeStyles((theme) => ({
iconOpen: {
transform: 'rotate(0deg)',
},
}));
....
export const MyCompo: FC<> = () => {
const classes = useStyles();
return (
<Select
multiple
variant="outlined"
MenuProps={CustomMenuProps}
IconComponent={Search}
classes={{
iconOpen: classes.iconOpen,
}}
renderValue={(selected) => (selected as string[]).join(', ')}
{...props}
>
....
<Select
value={values.phoneCode}
onChange={handleChange("phoneCode")}
inputProps={{ "aria-label": "Without label" }}
IconComponent={(_props) => {
const rotate = _props.className.toString().includes("iconOpen");
return (
<div
style={{
position: "absolute",
cursor: "pointer",
pointerEvents: "none",
right: 10,
height: "15px",
width: "15px",
transform: rotate ? "rotate(180deg)" : "none",
}}
>
<ArrowDown />
</div>
);
}}
>
....
It does not rotate if you use arrow function: IconComponent={()=> <YourIcon/>}

Button components inside ButtonGroup

I'm using React 16.9.0 and Material-UI 4.4.2 and I'm having the following issue.
I want to render a ButtonGroup with Button elements inside it but these buttons come from other custom components which return a Button render with a Modal view linked to the button. Thing is, I can't make them look like a ButtonGroup with the same style since it seems like the Button elements only take the "grouping" styling but not the "visual" styling.
Example code to reproduce behaviour:
<ButtonGroup variant="outlined">
<AModal/>
<BModal/>
<CModal/>
</ButtonGroup>
As you can see, the render output does not look as expected. Bare in mind that I'm defining the buttons with the outlined variant since if not they just render as Text Buttons.
Any help is much appreciated
Adding AModal as requested:
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import { Button } from '#material-ui/core';
import Modal from '#material-ui/core/Modal';
import Backdrop from '#material-ui/core/Backdrop';
import Fade from '#material-ui/core/Fade';
import InnerModalComponent from './InnerModalComponent';
const useStyles = makeStyles((theme) => ({
modal: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
paper: {
backgroundColor: theme.palette.background.paper,
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3),
},
}));
export default function AModal() {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
function handleOpen() {
setOpen(true);
}
function handleClose() {
setOpen(false);
}
return (
<div>
<Button variant="contained" onClick={handleOpen}> A </Button>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
className={classes.modal}
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{ timeout: 500 }}
>
<Fade in={open}>
<div className={classes.paper}>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'stretch',
justifyContent: 'center',
}}
>
<InnerModalComponent/>
</div>
<Button variant="contained" color="secondary" style={{ marginTop: '10px' }}> Button inside Modal</Button>
</div>
</Fade>
</Modal>
</div>
);
}
There are two main issues:
You are adding a div around each of your buttons. This will interfere a little with the styling. Change this to a fragment (e.g. <> or <React.Fragment>) instead.
The way that ButtonGroup works is by cloning the child Button elements and adding props to control the styling. When you introduce a custom component in between, you need to pass through to the Button any props not used by your custom component.
Here is a working example:
import React from "react";
import ReactDOM from "react-dom";
import ButtonGroup from "#material-ui/core/ButtonGroup";
import Button from "#material-ui/core/Button";
import Modal from "#material-ui/core/Modal";
const AModal = props => {
return (
<>
<Button {...props}>A</Button>
<Modal open={false}>
<div>Hello Modal</div>
</Modal>
</>
);
};
const OtherModal = ({ buttonText, ...other }) => {
return (
<>
<Button {...other}>{buttonText}</Button>
<Modal open={false}>
<div>Hello Modal</div>
</Modal>
</>
);
};
// I don't recommend this approach due to maintainability issues,
// but if you have a lint rule that disallows prop spreading, this is a workaround.
const AvoidPropSpread = ({
className,
disabled,
color,
disableFocusRipple,
disableRipple,
fullWidth,
size,
variant
}) => {
return (
<>
<Button
className={className}
disabled={disabled}
color={color}
disableFocusRipple={disableFocusRipple}
disableRipple={disableRipple}
fullWidth={fullWidth}
size={size}
variant={variant}
>
C
</Button>
<Modal open={false}>
<div>Hello Modal</div>
</Modal>
</>
);
};
function App() {
return (
<ButtonGroup>
<AModal />
<OtherModal buttonText="B" />
<AvoidPropSpread />
</ButtonGroup>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

React PDF viewer component rerenders constantly

I am using a React PDF viewer in my project. I have a react mui dialog component that I use with react draggable to drag it around.
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import makeStyles from "#material-ui/core/styles/makeStyles";
import DialogContent from "#material-ui/core/DialogContent";
import IconButton from "#material-ui/core/IconButton";
import ClearIcon from "#material-ui/icons/Clear";
import Draggable from "react-draggable";
import Paper from "#material-ui/core/Paper";
import Dialog from "#material-ui/core/Dialog";
import PDFViewer from "./PDFViewer";
function PaperComponent({...props}) {
return (
<Draggable
>
<Paper {...props} />
</Draggable>
);
}
const StyledDialog = withStyles({
root: {
pointerEvents: "none"
},
paper: {
pointerEvents: "auto"
},
scrollPaper: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
marginRight: 20
}
})(props => <Dialog hideBackdrop {...props} />);
const useStyles = makeStyles({
dialog: {
cursor: 'move'
},
dialogContent: {
'&:first-child': {
padding: 10,
background: 'white'
}
},
clearIcon: {
position: 'absolute',
top: -20,
right: -20,
background: 'white',
zIndex: 1,
'&:hover': {
background: 'white'
}
},
paper: {
overflowY: 'visible',
maxWidth: 'none',
maxHeight: 'none',
width: 550,
height: 730
}
});
const PDFModal = (props) => {
const classes = useStyles();
const {open, onClose, pdfURL} = props;
return (
<StyledDialog
open={open}
classes={{root: classes.dialog, paper: classes.paper}}
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog"
>
<DialogContent classes={{root: classes.dialogContent}} id="draggable-dialog">
<IconButton className={classes.clearIcon} aria-label="Clear" onClick={onClose}>
<ClearIcon/>
</IconButton>
<PDFViewer
url={pdfURL}
/>
</DialogContent>
</StyledDialog>
);
};
export default PDFModal;
And this is the PDFViewer component:
import React from 'react';
import { Viewer, SpecialZoomLevel, Worker } from '#react-pdf-viewer/core';
import { defaultLayoutPlugin } from '#react-pdf-viewer/default-layout';
import '#react-pdf-viewer/core/lib/styles/index.css';
import '#react-pdf-viewer/default-layout/lib/styles/index.css';
import ArrowForward from "#material-ui/icons/ArrowForward";
import ArrowBack from "#material-ui/icons/ArrowBack";
import Button from "#material-ui/core/Button";
import IconButton from "#material-ui/core/IconButton";
import RemoveCircleOutlineIcon from '#material-ui/icons/RemoveCircleOutline';
import AddCircleOutlineIcon from '#material-ui/icons/AddCircleOutline';
import './PDFViewer.css';
const PDFViewer = ({url}) => {
const renderToolbar = (Toolbar) => (
<Toolbar>
{
(slots) => {
const {
CurrentPageLabel, CurrentScale, GoToNextPage, GoToPreviousPage, ZoomIn, ZoomOut,
} = slots;
return (
<div
style={{
alignItems: 'center',
display: 'flex',
}}
>
<div style={{ padding: '0px 2px' }}>
<ZoomOut>
{
(props) => (
<IconButton aria-label="delete" onClick={props.onClick}>
<RemoveCircleOutlineIcon />
</IconButton>
)
}
</ZoomOut>
</div>
<div style={{ padding: '0px 2px' }}>
<CurrentScale>
{
(props) => (
<span>{`${Math.round(props.scale * 100)}%`}</span>
)
}
</CurrentScale>
</div>
<div style={{ padding: '0px 2px' }}>
<ZoomIn>
{
(props) => (
<IconButton aria-label="delete" onClick={props.onClick}>
<AddCircleOutlineIcon />
</IconButton>
)
}
</ZoomIn>
</div>
<div style={{ padding: '0px 2px', marginLeft: 'auto' }}>
<GoToPreviousPage>
{
(props) => (
<Button
style={{
cursor: props.isDisabled ? 'not-allowed' : 'pointer',
height: '30px',
width: '30px'
}}
disabled={props.isDisabled}
disableElevation
disableFocusRipple
onClick={props.onClick}
variant="outlined">
<ArrowBack fontSize="small"/>
</Button>
)
}
</GoToPreviousPage>
</div>
<div style={{ padding: '0px 2px' }}>
<CurrentPageLabel>
{
(props) => (
<span>{`${props.currentPage + 1} av ${props.numberOfPages}`}</span>
)
}
</CurrentPageLabel>
</div>
<div style={{ padding: '0px 2px' }}>
<GoToNextPage>
{
(props) => (
<Button
style={{
cursor: props.isDisabled ? 'not-allowed' : 'pointer',
height: '30px',
width: '30px'
}}
disabled={props.isDisabled}
disableElevation
disableFocusRipple
onClick={props.onClick}
variant="outlined">
<ArrowForward fontSize="small"/>
</Button>
)
}
</GoToNextPage>
</div>
</div>
)
}
}
</Toolbar>
);
const defaultLayoutPluginInstance = defaultLayoutPlugin({
renderToolbar,
sidebarTabs: defaultTabs => [defaultTabs[1]]
});
// constantly called
console.log('entered')
return (
<div
style={{
height: '100%',
}}
>
<Worker workerUrl="https://unpkg.com/pdfjs-dist#2.5.207/build/pdf.worker.min.js">
<Viewer
fileUrl={url}
defaultScale={SpecialZoomLevel.PageFit}
plugins={[
defaultLayoutPluginInstance
]}
/>
</Worker>
</div>
);
};
export default PDFViewer;
I can see in the console that PDFViewer is being constantly called. I am not sure what is causing this rerenders the whole time?
Isn't it make sense to re-render when you have a new fileUrl passed to PDFModal? The following sequence should be how the app is executed.
PDFModal, PDFViewer and other related components init
When a file is dragged into the PaperComponent context, the upper level component handles it and passing pdfURL as props
const PDFModal = (props) => {
const { ......., pdfURL } = props;
//...skipped code
return (
<StyledDialog
PaperComponent={PaperComponent}
>
//...skipped code
<PDFViewer
url={pdfURL}
/>
</StyledDialog>
);
};
PDFViewer updated because there is a new prop.
const PDFViewer = ({ url }) => {
//...skipped code
return (
//...skipped code
<Viewer
fileUrl={url}
/>
);
}
I agree what #LindaPaiste said, putting Toolbar maybe an option since it doesn't use the url props passed in. For the re-render problem, I suggest that useCallback can be used to wrap the whole PDFViewer component. Only update the component when the url has changed.
This link provide some insights on when to use useCallback which can be a reference.
const PDFViewer = useCallback(
({ url }) => {
//...skipped code
return (
//...skipped code
<Viewer
fileUrl={url}
/>
)
}, [url])

Resources