I am trying to create a modal and for some reason I cannot interact with any components like buttons or inputs within my modal.
I am building a React Portal, so that my modal and page are separate from each other and that the modal is not "running" in the background when it is not in use
import {useState, useLayoutEffect} from "react"
import { createPortal } from "react-dom"
const createWrapperAndAppendToBody = (wrapperId: string) => {
if(!document) return null
const wrapperElement = document.createElement("div")
wrapperElement.setAttribute('id', wrapperId)
document.body.appendChild(wrapperElement)
return wrapperElement
}
function ReactPortal({children, wrapperId}: {children: React.ReactElement; wrapperId: string}) {
const [wrapperElement, setWrapperElement] = useState<HTMLElement>()
useLayoutEffect(() => {
let element = document.getElementById(wrapperId)
let systemCreated = false
if(!element) {
systemCreated = true
element = createWrapperAndAppendToBody(wrapperId)
}
setWrapperElement(element!)
return () => {
if(systemCreated && element?.parentNode) {
element.parentNode.removeChild(element)
}
}
},[wrapperId])
if(!wrapperElement) return null
return createPortal(children, wrapperElement)
}
export default ReactPortal
I am pretty sure this is fine.
import React, {useEffect} from "react";
import ReactPortal from "./ReactPortal";
interface ConfirmationModalProps {
isOpen: boolean;
handleClose: () => void
children: React.ReactNode
}
export const ConfirmationModal =({children, isOpen, handleClose}:ConfirmationModalProps) => {
//allows to press Escape Key
useEffect(() => {
const closeOnEscapeKey = (e: KeyboardEvent) =>
e.key === 'Escape' ? handleClose() : null
document.body.addEventListener('keydown', closeOnEscapeKey)
return () => {
document.body.removeEventListener('keydown', closeOnEscapeKey)
};
},[handleClose])
//stops scrolling fuction when modal is open
useEffect(() => {
document.body.style.overflow = 'hidden';
return (): void => {
document.body.style.overflow = 'unset'
}
},[isOpen])
if(!isOpen) return null
return (
<ReactPortal wrapperId='react-portal-modal-container'>
<>
<div className='fixed top-0 left-0 w-screen h-screen z-40 bg-neutral-800 opacity-50' />
<div className='fixed rounded flex flex-col box-border min-w-fit overflow-hidden p-5 bg-zinc-800 inset-y-32 inset-x-32'>
<button className='py-2 px-8 self-end font-bold hover:bg-violet-600 border rounded'
onClick={() => console.log('Pressed')}>
Close
</button>
<div className='box-border h-5/6'>{children}</div>
</div>
</>
</ReactPortal>
)
}
This is my reusable Modal component so that I do not have to build a new modal from scratch. The close button within here is no also not working/can't event click on it via a console.log('pressed')
Here is a picture of the modal, but none of the buttons work, however, my Escape key function does work. Any thoughts would be great!
Related
i'm new in formik and react typescript.
currently, i have a screen called addScreen.tsx. inside that screen, there is component called <FormDropdown />. component FormDropdown has a child component called <Dropdown />.
how i can get the value when dropdown item changed, then i passed on onsubmit formik in addScreen ?
thanks in advance
below are the snippet codes from every screen or component
// addscreen.tsx
<FormDropdown
options={[
{
id: "what's your name?",
label: "what's your name?",
},
{
id: "where do you live?",
label: "where do you live?",
},
{
id: "what is your favorite food?",
label: "what is your favorite food?",
},
]}
name="securityQuestion"
data-testid="securityQuestion"
className="block h-full w-full border-transparent px-3 py-2 text-base leading-[1.813rem] text-nero focus:border-transparent focus:outline-none focus:ring-0"
/>;
//its formik component named FormDropdown.tsx
import { useField } from "formik";
import React, { FC, useCallback, useMemo, useState } from "react";
import { useUpdateEffect } from "#/hooks";
import Dropdown from "#/components/Dropdown";
import type { FormDropdownProps } from "../FormDropdown/types";
const FormDropdown: FC<FormDropdownProps> = (props) => {
const { name } = props;
const [, meta, helpers] = useField(name);
const [currentValue, setCurrentValue] = useState<string | number>(
meta.value || meta.initialValue
);
const hasError = useMemo(
() => meta.touched && !!meta.error,
[meta.error, meta.touched]
);
useUpdateEffect(() => {
setCurrentValue(meta.value);
}, [meta.value]);
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const text = event.target.value;
setCurrentValue(text);
helpers.setValue(text);
helpers.setError("");
},
[helpers]
);
const handleBlur = useCallback(() => {
helpers.setTouched(true);
}, [helpers]);
return (
<Dropdown
{...props}
// value={currentValue}
// hasError={hasError}
// onChange={handleChange}
// onBlur={handleBlur}
/>
);
};
export default FormDropdown;
//Dropdown.tsx
import React, { FC, Fragment, useEffect, useRef, useState } from "react";
import clsxm from "#/utils/clsxm";
import Button from "#/components/Button";
import type { DropdownProps, Option } from "./types";
import Typography from "../Typography";
const Dropdown: FC<DropdownProps> = ({
options,
isScrolled = false,
disabled = false,
hasError = false,
className,
}) => {
const [selectedOption, setSelectedOption] = useState<Option | null>(null);
const [open, setIsOpen] = useState(false);
const ref = useRef<any>();
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event: any) {
if (ref.current && !ref.current.contains(event.target)) {
setIsOpen(false);
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
const handleClick = (option: Option) => () => {
setSelectedOption(option);
handleOpenClick();
};
const handleOpenClick = () => {
setIsOpen((open) => !open);
};
return (
<>
<button
type="button"
className={clsxm(
className,
"w-full rounded-[0.938rem] border border-beluga bg-white p-[10rem] px-4 py-4 text-base font-medium text-improbable focus-within:border-improbable",
hasError ? "border-poppySurprise" : ""
)}
aria-expanded="true"
aria-haspopup="true"
onClick={() => handleOpenClick()}
>
{selectedOption ? selectedOption.label : "Please Choose"}
</button>
{!disabled && open && (
<div
ref={ref}
className={clsxm(
"absolute z-20 mt-[0.625rem] w-[14.0625rem] overflow-hidden rounded-[0.938rem] border-[0.0625rem] border-beluga bg-white py-[1.25rem]",
isScrolled && "h-[15.5rem] overflow-scroll overflow-x-hidden"
)}
>
{options.map((option) => (
<Fragment key={option.id}>
<Button
className={clsxm(
"h-[0.75rem] w-[8.125rem] rounded-none border-none bg-transparent hover:bg-white"
)}
onClick={handleClick(option)}
>
<Typography variant="p" size="text-sm" color="text-improbable">
{option.label}
</Typography>
</Button>
</Fragment>
))}
</div>
)}
</>
);
};
export default Dropdown;
I am new to React and using React 18 in this app. My problem is that if I click one button inside a map function, it reflects information about all the items. I want only that item information to show for which I clicked the button. The isShown === true part in the CountryInfo.js file is what should reflect only one item; currently clicking the show button shows all item information on the UI (I don't want this to happen). How do I do this?
Visually, this is my UI,
If you see the image above, clicking any show button returns all countries information, which should not happen.
Below is my code:
App.js
import { useState, useEffect } from 'react';
import axios from "axios";
import CountryInfo from './components/CountryInfo';
const App = () => {
const [countries, setCountries] = useState([]);
const [searchCountry, setSearchCountry] = useState("");
const handleCountryChange = event => {
setSearchCountry(event.target.value);
}
const getAllCountriesData = () => {
axios.get("https://restcountries.com/v3.1/all")
.then(response => {
setCountries(response.data);
})
}
useEffect(() => {
getAllCountriesData();
}, []);
return (
<>
<h2>Data for countries</h2>
find countries:
<input value={searchCountry} onChange={handleCountryChange} />
{searchCountry.length > 0 && <CountryInfo countries={countries} searchCountry={searchCountry} />}
</>
)
}
export default App;
CountryInfo.js
import React from "react";
import { useState } from "react";
const CountryInfo = ({ countries, searchCountry }) => {
const [isShown, setIsShown] = useState(false);
let filteredList = countries.filter(country =>
country.name.common.toLowerCase().includes(searchCountry.toLowerCase()));
const handleClick = () => {
setIsShown(true);
}
if (filteredList.length > 10) {
return <div>Too many matches, specify another filter</div>
}
else {
return filteredList.map(country => {
return (
<>
<div key={country.name.common}>
{!isShown &&
<div>
{country.name.common}
<button type="submit" onClick={handleClick}>show</button>
</div>
}
{isShown &&
<div key={country.name.common}>
<h2>{country.name.common}</h2>
<p>
Capital: {country.capital}
{'\n'}
Area: {country.area}
</p>
Languages:
<ul>
{
Object.values(country.languages)
.map((language, index) => <li key={index}>{language}</li>)
}
</ul>
<img src={country.flags.png} alt={`${country.name.common} flag`} height={150} />
</div>
}
</div>
</>
)
})
}
}
export default CountryInfo;
I made a menu to display options. The code was working fine before. I copied it from a udemy course. He used the same code. Now all of sudden it is not working anymore. It keeps on hiding the menu whenever I try to open it. I cant seem to find the bug. I have added some css classes like animate-scale for animation.
Header.js
import React, { useEffect, useRef, useState } from "react";
import { AiOutlinePlus } from "react-icons/ai";
import {
BsFillNodePlusFill,
BsFillSunFill,
BsNodeMinusFill,
} from "react-icons/bs";
import { useTheme } from "../hooks";
export default function Header({ onAddMovieClick, onAddActorClick }) {
const [showOptions, setShowOptions] = useState(false);
const { toggleTheme } = useTheme();
const options = [
{
title: "Add Movie",
onClick: onAddMovieClick,
},
{
title: "Add Actor",
onClick: onAddActorClick,
},
];
return (
<div className="flex justify-between items-center relative">
<input
type="text"
className="border-2 border-light-subtle dark:border-dark-subtle outline-none bg-transparent focus:border-primary dark:border-white transition p-1 rounded"
placeholder="Search Movies..."
/>
<div className="flex items-center space-x-3">
<button
onClick={toggleTheme}
className="dark:text-white text-light-subtle p-1 rounded "
>
<BsFillSunFill size={24} />
</button>
{/* <div className="relative"> */}
<button
onClick={() => setShowOptions(true)}
className="flex space-x-2 items-center border-secondary text-secondary border-2 font-semibold text-lg px-3 rounded"
>
<span>Create</span>
<AiOutlinePlus />
</button>
</div>
<CreateOptions
visible={showOptions}
onClose={() => setShowOptions(false)}
// onClose={() => setShowOptions(false)}
options={options}
/>
</div>
// </div>
);
}
const CreateOptions = ({ visible, onClose, options }) => {
const container = useRef();
const containerID = "option-container";
useEffect(() => {
const handleClose = (e) => {
if (!visible) return;
const { parentElement, id } = e.target;
if (parentElement.id === containerID || id === containerID) return;
container.current.classList.remove("animate-scale");
container.current.classList.add("animate-scale-reverse");
};
document.addEventListener("click", handleClose);
return () => {
document.removeEventListener("click", handleClose);
};
}, [visible]);
const handleAnimationEnd = (e) => {
if (e.target.classList.contains("animate-scale-reverse")) {
console.log("triggered");
onClose();
}
e.target.classList.remove("animate-scale");
};
if (!visible) return null;
return (
<div
id={containerID}
ref={container}
onAnimationEnd={handleAnimationEnd}
className="absolute top-12 right-1 drop-shadow-lg bg-white flex flex-col p-5 space-y-3 dark:bg-secondary animate-scale"
>
{options.map(({ title, onClick }) => (
<Option key={title} onClick={onClick}>
{title}
</Option>
))}
{/* <Option>Add Movie</Option> */}
{/* <Option>Add Actor</Option> */}
</div>
);
};
const Option = ({ children, onClick }) => {
return (
<button onClick={onClick} className="text-secondary dark:text-white">
{children}
</button>
);
};
index.css
.animate-scale {
transform-origin: top;
animation: scale 0.2s;
}
.animate-scale-reverse {
transform-origin: top;
animation: scale 0.2s reverse forwards;
}
#keyframes scale {
0% {
transform: scaleY(0);
}
100% {
transform: scaleY(1);
}
}
I have shared it on codeSandBox. Just so one can see the bug.
https://codesandbox.io/s/practical-jackson-6xut0y?file=/src/App.js
I did some fixes in your codesandbox. Let me explain what I did.
The problem is that a click on button propagates down to your custom component and it catches a click event, which closes your modal. To fix that, use e.stopPropagation(); on your button.
Also, seems like you were trying to close the modal, when clicking outside of it (where you checked parent id). A better way to catch a click outside of a component is by using container.current && !container.current.contains(e.target), where container is the ref, that you've created. In this condition check, you are checking, if the clicked target is inside of your component or not.
I took a look at your codesandbox link and it looks like this line in your useEffect is causing the problem:
container.current.classList.add("animate-scale-reverse")
By the looks of how your function is set up, that function is being called each time you click the button its automatically adding the reverse class which will close the modal.
I think that your problem occurs in useEffect in CreateOptions component, You are setting an event listener on the whole document for closing the options dropdown but keep in mind that the button that opens that dropdown is also part of the document so as soon as you click to open the options dropdown, the document also sees that there is a click and a click listener for the whole document and ultimately closes the options dropdown
I hope you can understand what I am trying to say
Here is a hook that I found on the internet that can detect click outside of an element > https://usehooks.com/useOnClickOutside/
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
},
[ref, handler]
);
}
You can use the hook like so,
useOnClickOutside(ref, onClickHander)
the ref is for the element that you want to detect click outside of, In your case this would be the options dropdown
the the click hander is the function which runs when a click is detected, In your case this would be handleClose
Hope my answer was helpful to you, If not then my apologies for wasting your time,
Ayan Ali
I am trying to identify on scroll if the div is visible on viewport. I am shring the code below:
<div id="parent">
data.map(item => {
<div id={item.id}>data.title</div>
}
<div>
Now I want to get the list of divs inside of #parent which are visible on viewport on scroll.
You can install the 'react-intersection-observer' module from the npm to do the trick. Read more from here. They have the 'inView' hook that could solve your problem.
First import the hook.
import { useInView } from "react-intersection-observer";
const [ref, inView] = useInView({
/* Optional options */
triggerOnce: true,
rootMargin: '0px 0px',
})
here, we can ref our element by
<div ref={ref}></div>
and the inView returns true when it is visible on the viewport and false when it is not.
React-Intersection-Observer using with Multiple Divs
Install package npm i react-intersection-observer
Create new file MultipleObserver.js
// MultipleObserver.js
import { useInView } from 'react-intersection-observer';
const MultipleObserver = ( {children} ) => {
const { ref, inView } = useInView({ triggerOnce: true });
return (
<div ref={ref}>
{ inView ? <span>{children}</span> : 'Loading...' }
</div>
)
}
export default MultipleObserver;
Now you can use multiple divs in the same components see example MyComponent.js
// MyComponent.js
import React from 'react';
import MultipleObserver from "MultipleObserver.js";
const MyComponent = ( props ) => {
return (
<div className="main">
<h2>Page Content</h2>
<MultipleObserver>
<img src="large-image.png" />
</MultipleObserver>
<MultipleObserver>
<iframe src="domain.com/large-video.mp4"></iframe>
</MultipleObserver>
<p>Another content</p>
</div>
}
export default MyComponent
This helped me:
<div id="parent">
data.map(item => {
<div id={`id_${item.id}`}>data.title</div>
}
<div>
and
const displayIds = (target) => {
console.log(target.replace("id_", ""));
}
const myScrollHandler = debounce(() => {
data.map(
(item: any) => {
const target =
document.querySelector(`#id_${item.id}`);
const top = target?.getBoundingClientRect().top || 0;
top >= 0 && top <= window.innerHeight ? displayIds(target);
}
);
}, 500);
useEffect(() => {
document.addEventListener("scroll", myScrollHandler);
return () => document.removeEventListener("scroll", myScrollHandler);
}, [data]);
Now for every scroll I have list of id's associated with div's which is visible on viewport.
I'm facing an issue triggering the onBlur on the color input tag. I have the parent component in which I toggle the visibility of the children by using a custom hook, but the thing is when I select a color with the input color from the children, when I click outside, the onBlur it's not triggered and I have no idea why.
I need to use this specific behavior with the useClickOutside + onBlur to trigger my future actions so I would like to know is there is a way to make it work?
custom hook:
import { useRef, useEffect } from 'react';
const useClickOutside = <T extends HTMLElement = HTMLDivElement>(callback: (event?: MouseEvent) => void) => {
const elementRef = useRef<T>(null);
useEffect(() => {
const handler = (event: MouseEvent) =>
elementRef.current && !elementRef.current.contains(event.target as Node) && callback(event);
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [callback]);
return elementRef;
};
export default useClickOutside;
parent component:
// other code before this part is not relevant for the question
const [toggle, setToggle] = useState(false);
const tagWidgetRef = useClickOutside(() => {
setToggle(false);
updateLabelsCallback();
});
return(
<div>
{/* other html here too... */}
{toggle && (
<LabelWidgetUpdate
className='right-20 top-12 z-50'
ref={tagWidgetRef}
setNewLabel={setUpdateTaskObject}
/>
)}
</div>
)
children component:
export const LabelWidgetUpdate = React.forwardRef<HTMLDivElement, LabelWidgetUpdateProps>(
({ setNewLabel, className }, ref) => {
const [newColor, setNewColor] = useState('');
return (
<div ref={ref} className={`absolute z-50 mt-4 w-56 rounded-md bg-white p-4 ${className}`}>
<input
onChange={e => setNewColor(e.target.value)}
onBlur={() => console.log('Blur triggered')}
type='color'
className='h-6 w-6 cursor-pointer rounded-md'
/>
{/* other html here too... */}
</div>