React : Open/close react-colorful picker - javascript

I'm using react-colorful to get colors HEX code.
I would want to show this component when the color's box is clicked, and hide it when clicked outside of it, kinda like the color picker Chrome is using (ex <input type="color" /> )
How to do so ?
https://codesandbox.io/s/react-colorful-demo-forked-wwxq2?file=/src/App.js:308-322
import React, { useState } from "react";
import { HexColorPicker } from "react-colorful";
import "./styles.css";
export default function App() {
const [color, setColor] = useState("#b32aa9");
const handleChange = (e) => {
setColor(e.target.value);
};
return (
<div className="App">
<HexColorPicker color={color} onChange={setColor} />
//Click on this box to show the picker <div className="value" style={{ borderLeftColor: color }}>
Current color is {color}
</div>
<input value={color} onChange={handleChange} />
<div className="buttons">
<button onClick={() => setColor("#c6ad23")}>Choose gold</button>
<button onClick={() => setColor("#556b2f")}>Choose green</button>
<button onClick={() => setColor("#207bd7")}>Choose blue</button>
</div>
</div>
);
}

At first, you should have to declare a state which tracks whether the color box is open or not like
const [open, setopen] = useState(false);
Now add an event handler on the div of the box which toggles the state from false to true and true to false.
const openBox = () => {
setopen(!open);
}
Now in your return statement, add a conditional to open or close colorBox. if clicked on that div which holds the onClick method.
<div className="App">
{open &&<HexColorPicker color={color} onChange={setColor} />
}
<div onClick={() => openBox()}className="value" style={{ borderLeftColor: color }}>
Current color is {color}
</div>
Now if you click on the div which contains the onClick method, it will open the colorBox and on pressing again it will close it.

You can use the following example (from https://codesandbox.io/s/opmco found in Code Recipes)
import React, { useCallback, useRef, useState } from "react";
import { HexColorPicker } from "react-colorful";
import useClickOutside from "./useClickOutside";
export const PopoverPicker = ({ color, onChange }) => {
const popover = useRef();
const [isOpen, toggle] = useState(false);
const close = useCallback(() => toggle(false), []);
useClickOutside(popover, close);
return (
<div className="picker">
<div
className="swatch"
style={{ backgroundColor: color }}
onClick={() => toggle(true)}
/>
{isOpen && (
<div className="popover" ref={popover}>
<HexColorPicker color={color} onChange={onChange} />
</div>
)}
</div>
);
};
useClickOutside.js
import { useEffect } from "react";
// Improved version of https://usehooks.com/useOnClickOutside/
const useClickOutside = (ref, handler) => {
useEffect(() => {
let startedInside = false;
let startedWhenMounted = false;
const listener = (event) => {
// Do nothing if `mousedown` or `touchstart` started inside ref element
if (startedInside || !startedWhenMounted) return;
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) return;
handler(event);
};
const validateEventStart = (event) => {
startedWhenMounted = ref.current;
startedInside = ref.current && ref.current.contains(event.target);
};
document.addEventListener("mousedown", validateEventStart);
document.addEventListener("touchstart", validateEventStart);
document.addEventListener("click", listener);
return () => {
document.removeEventListener("mousedown", validateEventStart);
document.removeEventListener("touchstart", validateEventStart);
document.removeEventListener("click", listener);
};
}, [ref, handler]);
};
export default useClickOutside;

Related

Closing a list item dropdown on selecting an item or clicking outside it

I have made a dropdown in my react js project with list items in it (Dropdown items are shown by ul and li tags here).
The issue i am facing is that on selecting any item the value in state changes but the dropdown doesn't close not do it closes when i click anywhere outside of it.
Please help me out, here is the working codesandbox url for the same repo
Check here
Also i am sharing the code below .
App.js
import React, { Component, useState } from "react";
import Dropdown from "./dropdown";
import "./dropdown.css";
const App = () => {
const [value, setValue] = useState("");
const [showDropdown, setShowDropdown] = useState(false);
const selectedValue = (value) => {
console.log(value);
setValue(value);
};
const onSelectItem = () => {
console.log("12");
setShowDropdown(false);
};
return (
<section className="shadow-border" id="FlightsForm">
<div className="top-sec d-flex">
<div className="left-side">
<span
className="custm-dropdown"
onClick={() => setShowDropdown(true)}
>
<span style={{ backgroundColor: "grey", color: "white" }}>
{value.tripType}
</span>
<span className="dropdown-icon"> </span>
<Dropdown
selectedValue={selectedValue}
onSelectItem={onSelectItem}
showDropdown={showDropdown}
/>
</span>
</div>
</div>
</section>
);
};
export default App;
Dropdown.js
import React from "react";
import { useState, useEffect } from "react";
const TripTypeDropdown = (props) => {
const [values, setValues] = useState([
{ tripType: "One Way", value: 1 },
{ tripType: "Return", value: 2 },
{ tripType: "Multi- City", value: 3 },
]);
const [selectedItem, setSelectedItem] = useState({
tripType: "One Way",
value: 1,
});
useEffect(() => {
props.selectedValue(selectedItem);
console.log(selectedItem);
}, [selectedItem]);
const selectItemFromList = (index) => {
const itemSelected = values[index];
setSelectedItem(itemSelected);
props.onSelectItem();
};
const getActiveClassName = (item) => {
if (selectedItem) {
if (item.tripType == selectedItem.tripType) return "active";
else return "";
}
} ;
return (
<React.Fragment>
<div
className={`dropdown-modal sm-modal ripple trip-type-dropdown`}
style={{ display: `${props.showDropdown ? "block" : "none"}` }}
>
<ul>
{console.log(values)}
{values.map((item, index) => (
<li
className={getActiveClassName(item)}
onClick={() => selectItemFromList(index)}
key={index}
style={{backgroundColor:'yellow',border:"1px solid black",listStyle:"none"}}
>
{item.tripType}
</li>
))}
</ul>
</div>
</React.Fragment>
);
};
export default TripTypeDropdown;
You need to stop bubbling up of click event from your dropdown component to it's parent span element. On click you need to pass thevent argument and call stopPropagation function of event object
Here is the condesandbox
Dropdown.js
const selectItemFromList = (e,index) => {
e.stopPropagation();
...
<li
className={getActiveClassName(item)}
onClick={(e) => selectItemFromList(e,index)}
key={index}
Also added code for outside click.
const ref = useRef();
...
useEffect(() => {
document.addEventListener("click", handleDropdownClick);
}, [ref]);
const handleDropdownClick = (e) => {
e.stopPropagation();
if (ref.current && ref.current.contains(e.target)) {
setShowDropdown(true);
} else {
setShowDropdown(false);
}
};
...
<span
ref={ref}
className="custm-dropdown"
onClick={handleDropdownClick}
>
You should stop the propagation of the click event from the list item to the outer span, this is defeating any attempts to toggle the dropdown closed again from the parent.
const selectItemFromList = (index) => (e) => {
e.stopPropagation();
const itemSelected = values[index];
setSelectedItem(itemSelected);
props.onSelectItem();
};
...
<li
key={index}
...
onClick={selectItemFromList(index)}
...
>
{item.tripType}
</li>
To handle outside clicks you attach a React ref to the dropdown div and use an useEffect hook to add an onClick event listener to the widow object and check that the onClick's event target is not contained within the dropdown div.
useEffect(() => {
const outsideClickHandler = (e) => {
if (props.showDropdown && !outsideRef.current?.contains(e.target)) {
props.onSelectItem(false);
}
};
window.addEventListener("click", outsideClickHandler);
return () => window.removeEventListener("click", outsideClickHandler);
}, [props]);
...
<div
ref={outsideRef}
...
>
...
</div>
Demo

How to render a popup by calling a function?

I want to render an element in React just by calling a function.
Usually you'd use a component (let's say a Popup) that takes a boolean from state to make it appear or not and change it with some callback handler. Something like this:
import React, { useState } from "react";
import Popup from "somecomponentlibrary";
import { Button } from "pathtoyourcomponents";
export const SomeComponent = () => {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => { setOpen(true); }}>
this opens a modal
</Button>
<Popup type={"info"} open={open} timeout={1000}>
text within modal
<Button onClick={() => { setOpen(false); }}></Button>
</Popup>
</>
);
};
I was wondering if instead of returning it in the component as above I could just call some method to just show it on the screen like that:
import React from "react";
import { Button, popup } from "pathtoyourcomponents";
export const SomeComponent = () => {
return (
<>
<Button onClick={() => { popup.info("text within modal", 1000); }}>
this opens a modal
</Button>
</>
);
};
How do I write the popup function in order to render a Popup component in the DOM in such way?
You can use ReactDOM.render to render the popup when the function is called:
const node = document.createElement("div");
const popup = (message, {type, timeout}) => {
document.body.appendChild(node);
const PopupContent = () => {
return (
<Popup type={type} open={true} timeout={timeout}>
{message}
<button
onClick={clear}
>Close</button>
</Popup >
);
};
const clear = () => {
ReactDOM.unmountComponentAtNode(node);
node.remove();
}
ReactDOM.render(<PopupContent/>, node);
};
Then call the popup function:
<Button onClick={() => { popup("text within modal", {type: "info", timeout: 1000}); }}>
this opens a modal
</Button>
I made an imperative API for a popup a while back, it allows you to await it until it closes, and even receive input to the caller (you might as well send back the button the user pressed):
const PopupContext = createContext()
export const PopupProvider = ({children}) => {
const [open, setOpen] = useState(false)
const [input, setInput] = useState('')
const resolver = useRef()
const handleOpen = useCallback(() => {
const { promise, resolve } = createDeferredPromise()
resolver.current = resolve
setInput('')
setOpen(true)
return promise
}, [])
const handleClose = useCallback(() => {
resolver.current?.(input)
setOpen(false)
}, [])
return <PopupContext.Provider value={handleOpen}>
{children}
<Popup type={"info"} open={open} timeout={1000} onClose={handleClose}>
<input value={input} onChange={e => setValue(e.target.value)}/>
<Button onClick={handleClose}/>
</Popup>
</PopupContext.Provider>
}
export const usePopup = () => {
const context = useContext(PopupContext);
if (!context)
throw new Error('`usePopup()` must be called inside a `PopupProvider` child.')
return context
}
// used to let await until the popup is closed
const createDeferredPromise = func => {
let resolve, reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
func?.(resolve, reject)
})
return { promise, resolve, reject }
}
You can wrap you app with the provider:
return <PopupProvider>
<App/>
</PopupProvider>
And use it inside your functional components:
const MyComponent = props => {
const popup = usePopup()
return <Button onClick={e => {
const input = await popup()
console.log('popup closed with input: ' + input)
}/>
}
You can do much more interesting stuff, as pass prompt text to the popup function to show in the popup etc. I'll leave this up to you.
You might also want to memoize your top level component being wrapped to avoid rerendering the entire application on popup open/close.
The popup can be rendered as a new separate React app but can still be made to share state with the main app like below.
import React, { useEffect } from "react";
import { render, unmountComponentAtNode } from "react-dom";
const overlay = {
top: "0",
height: "100%",
width: "100%",
position: "fixed",
backgroundColor: "rgb(0,0,0)"
};
const overlayContent = {
position: "relative",
top: "25%",
textAlign: "center",
margin: "30px",
padding: "20px",
backgroundColor: "white"
};
let rootNode;
let containerNode;
function Modal({ children }) {
useEffect(() => {
return () => {
if (rootNode) {
rootNode.removeChild(containerNode);
}
containerNode = null;
};
}, []);
function unmountModal() {
if (containerNode) {
unmountComponentAtNode(containerNode);
}
}
return (
<div style={overlay}>
<div style={overlayContent}>
{children}
<button onClick={unmountModal}>Close Modal</button>
</div>
</div>
);
}
/* additional params like props/context can be passed */
function renderModal(Component) {
if (containerNode) {
return;
}
containerNode = document.createElement("div");
rootNode = document.getElementById("root");
containerNode.setAttribute("id", "modal");
rootNode.appendChild(containerNode);
render(<Modal>{Component}</Modal>, containerNode);
}
const App = () => {
const ModalBody = <p>This is a modal</p>;
return (
<div>
<button onClick={() => renderModal(ModalBody)}>Open Modal</button>
</div>
);
};
render(<App />, document.getElementById("root"));
Yes, I think you can make it.
for example:
Use popup component
import React, { useState } from "react";
import Popup from "somecomponentlibrary";
import { Button } from "pathtoyourcomponents";
export const SomeComponent = () => {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => { setOpen(true); }}>
this opens a modal
</Button>
<Popup type={"info"} open={open} timeout={1000}>
text within modal
<Button onClick={() => { setOpen(false); }}></Button>
</Popup>
</>
);
};
Define pop-up component
const Popup = (props) => {
return(
<div style={{zIndex:props.open?"-100":"100", transition: `all ${props.timeout /
1000}s`, opacity: props.open?1:0}}>
{props.children}
</div>
)
}
I think you can customize the animation effect you like.

Can't update parent component

I'm trying to edit an input value in a child component and send to the parent
:
https://codesandbox.io/s/sleepy-rain-skoss?file=/src/Editlabel.js:0-389
Parent:
import "./styles.css";
import EditLabel from "./Editlabel";
import { useEffect, useState } from "react";
export default function App() {
const [newName, setNewName] = useState();
useEffect(() => {
console.log("newName", newName);
}, [newName]);
return (
<div className="App">
<EditLabel
value={"hello"}
click={(changedName) => {
setNewName(changedName);
}}
/>
</div>
);
}
Child:
import React, { useState } from "react";
const EditLabel = ({ value, click }) => {
const [name, setName] = useState(value);
return (
<>
<input type={"text"} placeholder={name}></input>
<button
onClick={(e) => {
setName(e.target.value);
click(name);
}}
>
Edit
</button>
</>
);
};
export default EditLabel;
However, the console logs "hello" and then it just logs empty strings.
How can I make it work?
try this on your child's input box
<input type={"text"} placeholder={name} onChange={(e) => setName(e.target.value)}>
Change EditLabel to use a ref to capture the input value:
const EditLabel = ({ value, click }) => {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} type={"text"} placeholder={value}></input>
<button
onClick={() => {
click(inputRef.current.value);
}}
>
Edit
</button>
</>
);
};
Update App to use the values it gets via the click callback:
export default function App() {
const [newName, setNewName] = useState("hello");
useEffect(() => {
console.log("newName", newName);
}, [newName]);
return (
<div className="App">
<EditLabel
value={newName}
click={(changedName) => {
setNewName(changedName);
}}
/>
</div>
);
}

React Material UI - Snackbar closes when Dialog closes but its not supposed to

Material ui dialog closes the snackbar along with it.
This is a weird problem so I created a demo to demonstrate the issue:
https://codesandbox.io/s/react-hooks-counter-demo-v20w3
I am passing states from parent to child so the parent state can update based on the child
<Child openDialog={openDialog} setOpenDialog={setOpenDialog} />
In the child, I am using these as below
export default function Child({openDialog, setOpenDialog}) {
The button in the child is supposed to close only the dialog but it closes snackbar too.
<button
onClick={() => dialogClick()}
>Click this dialog button
</button>
Line 12 in the child has setOpenSnack("true"); This only works when I comment out line 13 setOpenDialog(false);
const dialogClick = (event) => {
setMsg("This should close dialog only and open the snackbar");
setOpenSnack(true);
setOpenDialog(false);
};
This behavior is only when I split it into child and parent.
In short, why does setOpenSnack(true); in the child not work?
Move your Snackbar to your parent component so that it's not dependent on Dialog's lifecycle.
App.js
import DialogBody from "./child";
function App() {
const [openDialog, setOpenDialog] = useState(false);
const [openSnack, setOpenSnack] = useState(false);
const [msg, setMsg] = useState("nothing");
function doApiCall(formData) {
// do your API calls here
console.log(formData);
// when done hide dialog, show snackbar
setOpenDialog(false);
setOpenSnack(true);
}
const fireOnClick = (event) => {
setMsg("you clicked a button");
setOpenDialog(true);
};
const handleCloseSnack = (event, reason) => {
if (reason === "clickaway") {
return;
}
setOpenSnack(false);
};
function Alert(props) {
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
return (
<div className="App">
<h3>click button below to open dialog</h3>
<button onClick={() => fireOnClick()}>Click me</button>
<Dialog open={openDialog}>
<DialogTitle>This is a dialog</DialogTitle>
<DialogBody doApiCall={doApiCall} />
</Dialog>
<Snackbar
open={openSnack}
autoHideDuration={6000}
onClose={handleCloseSnack}
>
<Alert onClose={handleCloseSnack} severity="error">
{msg}
</Alert>
</Snackbar>
</div>
);
}
Child.js
export default function DialogBody({ doApiCall }) {
const [name, setName] = React.useState("");
function onChange(event) {
setName(event.target.value);
}
return (
<div>
<input type="text" value={name} onChange={onChange} />
<button onClick={() => doApiCall({ name })}>
Click this dialog button
</button>
</div>
);
}
Our DialogBody component only accepts a prop called doApiCall from our parent, which is invoked by clicking the Click this dialog button.
doApiCall might be an asynchronous call to your backends API which when resolves will hide the dialog and sets the snackbar to open.
When the dialog is closed, the Child component is dismantled. This justifies the disappearance of the Snack.
Try this to see:
index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import DialogTitle from "#material-ui/core/DialogTitle";
import Dialog from "#material-ui/core/Dialog";
import Child from "./child";
import Snackbar from "#material-ui/core/Snackbar";
import MuiAlert from "#material-ui/lab/Alert";
import "./styles.css";
function App() {
const [msg, setMsg] = useState("nothing");
const [openSnack, setOpenSnack] = useState(false);
const [openDialog, setOpenDialog] = useState(false);
//Close snackbar except when clickaway
const fireOnClick = (event) => {
setMsg("you clicked a button");
setOpenDialog(true);
//setOpenSnack(true);
};
const snackState = (value) => {
setOpenSnack(value)
}
const snackMessage = (value) => {
setMsg(value)
}
const handleCloseSnack = (event, reason) => {
if (reason === "clickaway") {
return;
}
setOpenSnack(false);
};
function Alert(props) {
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
const closeDialogOnly = (event) => {
setMsg("you closed dialog only");
setOpenDialog(false);
};
return (
<div className="App">
<h3>click button below to open dialog</h3>
<button onClick={() => fireOnClick()}>Click me</button>
<Dialog onClose={closeDialogOnly} open={openDialog}>
<DialogTitle>This is a dialog</DialogTitle>
<Child snackbarStat={snackState} snackMessage={snackMessage} setOpenDialog={setOpenDialog} />
</Dialog>
<Snackbar
open={openSnack}
autoHideDuration={6000}
onClose={handleCloseSnack}
>
<Alert onClose={handleCloseSnack} severity="error">
{msg}
</Alert>
</Snackbar>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
child.js
import React from "react";
export default function Child({ snackbarStat, setOpenDialog, snackMessage }) {
const dialogClick = (event) => {
snackMessage("snackMessage")
snackbarStat(true);
setOpenDialog(false);
};
return (
<div className="child">
<button onClick={() => dialogClick()}>Click this dialog button</button>
<p>
{" "}
Clicking the button should close dialog but why is it closing the
snackbar also? <br />
The snack bar opens for one second and disappears.
<br />
setOpenSnack("true") lines is useless when dialog is false.
</p>
</div>
);
}
You have to place the Snackbar in the parent component because when placing it in child component, it will disappear whenever the child is unmounted
Moreover, if you want to set the message, just pass the setMsg to the child component and do the job
export default function Child({
openDialog,
setOpenDialog,
setMsg,
setOpenSnack
}) {
const dialogClick = (event) => {
setMsg("you closed from CHILD");
setOpenSnack(true);
setOpenDialog(false);
};
return (
<div className="child">
<button onClick={() => dialogClick()}>Click this dialog button</button>
<p>
...
</p>
</div>
);
}

Adding hovering effects with a timeout in React

See codesandbox here
I am trying to add a modal that shows up with a delay when hovering over a div. However, it's getting a bit tricky because, for example, if the timeout interval is 1000ms, and you hover over said div and then hover away from that div within the 1000ms, the modal will still show up. What I want to happen is for the modal to show up after the delay (e.g. 1000ms) only if you have maintained mouseover over the div for that delay period. How can I create this effect instead of the side effects I'm seeing now? Thanks!
index.tsx:
import * as React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const Modal: React.FC = () => {
const divRef = React.useRef<HTMLDivElement>(null);
const [showModal, setShowModal] = React.useState<boolean>(false);
React.useEffect(() => {
const divNode = divRef.current;
const handleEvent = (event: Event): void => {
if (divNode) {
if (divNode.contains(event.target as Node)) {
setTimeout(() => setShowModal(true), 1000);
} else {
setShowModal(false);
}
}
};
document.addEventListener("mouseover", handleEvent);
return () => {
document.removeEventListener("mouseover", handleEvent);
};
}, [divRef]);
return (
<div className="container">
<div className="div" ref={divRef}>
Hover Me
</div>
{showModal && <div className="modal">modal</div>}
</div>
);
};
const App: React.FC = () => (
<>
<Modal />
<Modal />
<Modal />
<Modal />
</>
);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You should add a mouse out event that will hide the modal.
Call a funtion on 'mouseout' event listener and set the showModal to false. In that way, it will hide the modal if you move your mouse any time.
setShowModal(false)
Updated: Can you also set timeout to a variable and then on mouseout fire clearTimeout(variable_that_set_to_timeout)
React.useEffect(() => {
const divNode = divRef.current;
let timeout = null;
const handleEvent = (event: Event): void => {
if (divNode) {
if (divNode.contains(event.target as Node)) {
timeout = setTimeout(() => setShowModal(true), 1000);
} else {
setShowModal(false);
}
}
};
const hideModal = (event: Event): void => {
clearTimeout(timeout);
setShowModal(false);
};
divNode.addEventListener("mouseover", handleEvent);
divNode.addEventListener("mouseout", hideModal);
return () => {
document.removeEventListener("mouseover", handleEvent);
};
}, [divRef]);
Link of sandbox
You should really avoid changing the DOM when working with react. React isn't jQuery.
you could try making this your modal code:
const Modal: React.FC = () => {
const [timeout, setModalTimeout] = React.useState(null);
const [showModal, setShowModal] = React.useState<boolean>(false);
return (
<div className="container">
<div className="div" onMouseEnter={() => {
timeout && !showModal && clearTimeout(timeout);
setModalTimeout(setTimeout(() => setShowModal(true), 1000))
}} onMouseLeave={() => {
timeout && clearTimeout(timeout)
setShowModal(false);
}}>
Hover Me
</div>
{showModal && <div className="modal">modal</div>}
</div>
);
};
Sources:
https://reactjs.org/docs/refs-and-the-dom.html
https://reactjs.org/docs/react-dom.html
The proper way to do this would be to create a useTimeout hook and manage maintain the state of the hover.
import { useState } from "react";
import useTimeout from "./useTimeout";
export default function App() {
const [visible, setVisible] = useState(false);
const [hovered, setHovered] = useState(false);
//close after 3s
useTimeout(() => setVisible(true), !visible && hovered ? 3000 : null);
return (
<div className="App">
<h1>Hover Timeout Example</h1>
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
Hover me for 3s to show modal
<div>Hover status: {hovered ? "true" : "false"}</div>
</div>
{visible && (
<div>
<h1>Modal</h1>
<div>
<button onClick={() => setVisible(false)}>close</button>
</div>
</div>
)}
</div>
);
}
Code Sandbox

Categories