I have created a form with a custom drop down component, and I’ve spent the day trying to find out what is causing this.
I’ve tried using useRef and use defaultValue SO answers.
The ‘Menu Name’ text input can be changed multiple times until I click my <DropDownComponent/>.
After I selected a few days, and close the popup, my input becomes unusable.
After opening and closing the popup, I lose the ability to interact with the text input
Why is this happening? I feel like <input onChange=()=>{}/> is somehow being lost on the DOM possibly?
MenuForm.tsx
import { ChangeEvent, useEffect, useState } from "react";
import { .......... } from '../../../styling/styled-components';
import DropDownComponent from "../../UI/DropDownComponent/DropDownComponent";
import useImgUpload from "../../../hooks/useImgUpload";
import Icon from "../../../assets/icons/Icons";
const MenuForm = () => {
const {imgUrl, uploadFn} = useImgUpload();
const INPUT_LIST = ["Mon", "Tue", "Wed", "Thur", "Fri", "Sat", "Sun"];
const [nameInput, setNameInput] = useState<string>('');
const [availableDays, setAvailableDays ] = useState<string[]>([]);
useEffect(() => {
console.log(availableDays);
},[availableDays])
const anyTimeCheckBox = (event: ChangeEvent<HTMLInputElement>) => {
console.log(event.target.checked);
console.log(event.target.id);
}
const showUploadedImg = (event: ChangeEvent<HTMLInputElement>) => {
console.log(event);
uploadFn(event);
}
const submitHandler = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(`${nameInput}, ${availableDays}`);
}
const selectedDayHandler = (arr:string[]) => {
setAvailableDays( arr );
console.log('Days Set');
}
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setNameInput((currState:string) => {return currState = e.target.value});
}
console.log('Menu Form rendered');
return(
<MenuFormStyle onSubmit={submitHandler}>
<MenuImgWrapper>
<ImgInput
type='file'
alt='Add Image'
accept="image/png, image/jpeg"
onChange={showUploadedImg}
/>
<ImgDisplay>
<LoadedImgCont>
{
imgUrl
?
<LoadedImg src={imgUrl} alt='uploaded-img'></LoadedImg>
:
<>
<NoImg src={Icon.AddImage} alt='icon-placeholder'></NoImg>
<NoImgTxt>Add Image</NoImgTxt>
</>
}
</LoadedImgCont>
</ImgDisplay>
</MenuImgWrapper>
<MenuNameInput
type='text'
placeholder= 'Menu name'
name='menuName'
onChange={handleInputChange}
defaultValue={nameInput}
/>
{/* Day + Time selections */}
<DropDownComponent list={INPUT_LIST} setSelectedDays={selectedDayHandler}/>
<TimeStartInput
type='time'
placeholder='Start'
/>
<TimeEndInput
type='time'
placeholder="End"
/>
<TimeCheckBoxWpr>
<TimeCheckBox
type='checkbox'
name='24/7'
id='isAlwaysAvailable'
onChange={anyTimeCheckBox}/>
<label htmlFor='24/7'>Anytime</label>
</TimeCheckBoxWpr>
{/*Extra Settings */}
<FieldSetMenu>
<legend>Menu Settings:</legend>
<div>
<input
type='checkbox'
name='apples'
id='apples'
/>
<label htmlFor='apples'>Hide menu</label>
</div>
</FieldSetMenu>
<MenuSaveButton>Save</MenuSaveButton>
</MenuFormStyle>
);
}
export default MenuForm;
DropDownComponent
import React, { useEffect, useMemo, useRef, useState } from "react";
import Icon from "../../../assets/icons/Icons";
import {
ArrowDownIcon,
CloseIcon,
CustomListItem,
DaySelectInput,
DaySelectPopUp,
DisplaySelectedDays,
SelectedText,
UList,
} from "../../../styling/styled-components";
interface IProps {
list: string[];
setSelectedDays: (arr: string[]) => void;
multiSelect?: boolean;
}
interface ListProps {
list: DropDownProps[];
updateSelection: (e: React.MouseEvent<HTMLElement>) => void;
closeDropDown: () => void;
}
interface DropDownProps {
value: string;
selected: boolean;
}
const DropDownComponent = (props: IProps) => {
const [displayDropDown, setDisplayDropDown] = useState(false);
const [selectedItems, setSelectedItems] = useState<string[]>([]);
const [customListItems, setCustomListItems] = useState<DropDownProps[]>();
const { list, setSelectedDays } = props;
useMemo(() => {
let buildListArray: DropDownProps[] = list.reduce(
(previous, current) => [...previous, { value: current, selected: false }],
[{ value: "Reset", selected: false }]
);
setCustomListItems((currState) => {
return (currState = buildListArray);
});
console.log("DropDownComponent");
}, []);
useEffect(() => {
setSelectedDays(selectedItems);
},[selectedItems, setSelectedDays]);
const captureSelected = (event: any) => {
//updates state of custom drop down menu
let dropDownItem = customListItems![event.target.value];
dropDownItem.selected = !dropDownItem.selected;
let updatedSelectState = dropDownItem.selected;
if (updatedSelectState && dropDownItem.value === "Reset") {
console.log('Reset Triggered');
setSelectedItems([]);
customListItems?.forEach((item) => {
item.selected = false;
});
return;
} else if (updatedSelectState && !existsInList()) {
console.log('Added to SelectedItems');
setSelectedItems((currState) => {
return [...currState, dropDownItem.value];
});
} else if (!updatedSelectState && existsInList()) {
console.log('Removed from selected items');
let updatedArray = selectedItems.filter(
(item) => item !== dropDownItem.value
);
setSelectedItems(updatedArray);
}
function existsInList(): boolean {
return selectedItems.includes(dropDownItem.value);
}
};
const displaySelection = (items: string[]): string => {
return items.join(", ");
};
const toggleDropDown = () => {
console.log(selectedItems);
setDisplayDropDown(currState => !currState);
console.log('toggle');
};
return (
<DaySelectInput>
<DisplaySelectedDays>
<SelectedText>
{
!selectedItems.length
?
"Availability"
:
displaySelection(selectedItems)
}
</SelectedText>
{
displayDropDown
?
<CloseIcon src={Icon.Close} alt='dummy-icon'/>
:
<ArrowDownIcon
src={ Icon.ArrowDown }
alt='icon-down'
onClick={toggleDropDown}
/>
}
</DisplaySelectedDays>
{
displayDropDown
&&
<DropDownList
list={customListItems!}
updateSelection={captureSelected}
closeDropDown={toggleDropDown}
/>
}
</DaySelectInput>
);
};
const DropDownList = (props: ListProps) => {
const elRef = useRef<HTMLDivElement | null>(null);
const { list, closeDropDown } = props;
useEffect(() => {
console.log('ClickOutside Effect')
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
function handleClickOutside(event: any) {
event.preventDefault();
if (elRef.current && !elRef.current?.contains(event.target)) {
closeDropDown();
}
}
}, [elRef, closeDropDown]);
return (
<DaySelectPopUp ref={elRef}>
<UList>
{list?.map((listItem: DropDownProps, index:number) => {
return (
<CustomListItem
key={`li-${listItem.value}`}
value={index}
onClick={props.updateSelection}
userSelected={listItem.selected}
>
{listItem.value}
</CustomListItem>
);
})}
</UList>
</DaySelectPopUp>
);
};
export default DropDownComponent;
styled components file incase you want to try for yourself
import styled from 'styled-components/macro';
const primaryMain = '#003332';
const primaryLight = '#305d5b';
const primaryDark = '#000e0a';
const baseBackground = '#EBE9E9';
const secondaryMain = '#003332';
const secondaryLight = '#305d5b';
const secondaryDark = '#000e0a';
const textLight = 'white';
const textDark = 'black';
//Texts
const StandardText = styled.p`
color: ${textLight};
`;
//UI
const BaseCard = styled.div`
display: inline-flex;
justify-content: center;
align-items: center;
background: ${baseBackground};
padding: 5px;
margin: 5px;
`;
const GreenCard = styled(BaseCard)`
height: 50px;
margin: 5px 0;
border-radius: 5px;
background: ${primaryMain};
`
//NavigationComponent
const NavBackDrop = styled.div`
width: 100%;
background: ${primaryDark};
padding: 10px 0px 5px 0px;
`;
const NavWrapper = styled.div`
width: 100%;
background: ${primaryMain};
padding: 5px 0px;
display: inline-flex;
align-items: center;
`;
const NavLogoWrapper = styled.div`
width: 50px;
height: 40px;
background: ${primaryLight};
margin-left: 10px;
`;
const NavLinkWrapper = styled.div`
width: 100%;
display: inline-flex;
justify-content: flex-start;
align-items: center;
& > a {
min-width: 100px;
height: fit-content;
text-align: center;
margin: 0px 10px;
color: white;
text-decoration: none;
cursor: pointer;
}
& > a.active {
background: ${primaryLight};
padding: 0px 10px;
border-radius: 5px;
max-width: 100px;
& > p {
color: white;
font-weight: 700;
transform: scale(1.05);
margin: 10px 0px;
}
}
`;
const ProfileImgWrapper = styled.div`
width: 40px;
height: 40px;
background: ${primaryLight};
border-radius: 50%;
float: right;
margin-right: 10px;
`;
const DashBoardGrid = styled.div`
min-height: 80vh;
height: fit-content;
display: grid;
grid-template-columns: 30vw 30vw 35vw;
grid-gap: 10px;
padding: 10px;
justify-content: center;
& > div {
border-radius: 5px;
box-shadow: 1px 1px 2px 1px #00000038;
}
`;
const DashSalesGridWrapper = styled(BaseCard)`
grid-column: 1 / span 2;
`;
const DashMenuWrapper = styled(BaseCard)`
grid-column: 3 ;
grid-row: 1 / span 2;
display: grid;
grid-template-rows: 50px;
grid-template-columns: 100%;
`;
const DashKpiWrapper = styled(BaseCard)`
grid-column: 1 / span 2 ;
grid-row: 2 ;
`;
const MenuWrapper = styled.div`
width: 100vw;
display: inline-flex;
justify-content: space-around;
`
const Grid = styled(BaseCard)`
width: 45vw;
display: grid;
grid-template-columns: 100%;
grid-auto-rows: minmax(50px, auto);
align-items: flex-start;
border-radius: 3px;
height: fit-content;
box-shadow: 1px 1px 2px 1px #00000054;
`;
const CreateNewButton = styled.button`
height: 50px;
border: 3px solid ${primaryLight};
border-radius: 5px;
background: white;
cursor: pointer;
display: inline-flex;
justify-content: center;
align-items: center;
&:hover{
transform: scale(1.01);
box-shadow: 1px 1px 2px 1px #00000054;
font-size: 0.9rem;
&>img{
transform: scale(1.2);
}
}
`;
const IconImg = styled.img`
width: 30px;
height: 30px;
margin-right: 5px;
`;
const CustomListItem = styled.li<{userSelected:boolean}>`
color: ${ props => props.userSelected ? 'white': 'none'};
background: ${ props => props.userSelected ? primaryLight : 'none'};
cursor: pointer;
min-width: 28%;
margin: 2px;
border-radius: 5px;
border: 3px solid ${primaryLight};
font-weight: 500;
text-align: center;
&:first-of-type {
width: 100%;
}
&:hover{
background: white;
color: black;
}
`;
const DaySelectInput = styled.div`
grid-column: 3/ span 2;
grid-row: 2;
position: relative;
`;
const DaySelectPopUp = styled.div`
width: 100%;
position: absolute;
top: 100%;
background: #d6d9d9;
border: 2px solid ${primaryLight};
padding: 5px;
margin-top: 3px;
`;
const UList = styled.ul`
display: flex;
flex-flow: wrap;
list-style-type: none;
padding: 0px;
justify-content: flex-start;
`;
const MenuFormStyle = styled.form`
display: grid;
justify-content: center;
align-items: flex-end;
grid-template-columns: 27% 27% 20% 20%;
grid-gap: 5px;
color: #444;
border: 3px solid #305d5b;
border-radius: 10px;
padding: 10px;
margin-top: 2px;
`;
const MenuImgWrapper = styled.div`
grid-column: 1/span 2;
grid-row: 1/ span 2;
height: 200px;
width: 200px;
position: relative;
`;
const ImgInput = styled.input`
width: 100%;
height: 100%;
opacity: 0;
z-index: 1;
position: absolute;
`;
const ImgDisplay = styled.div`
width: 100%;
height: 100%;
position: absolute;
top: 0;
z-index: 0;
border: 3px solid #003332;
background: #d6d9d9;
`;
const MenuNameInput = styled.input`
grid-column: 3/span 2;
grid-row: 1;
min-height: 30px;
border-radius: 5px;
border: 3px solid ${primaryLight};
`;
const TimeStartInput = styled.input`
grid-column: 3;
grid-row: 3;
`;
const TimeEndInput = styled.input`
grid-column: 4;
grid-row: 3;
`;
const TimeCheckBox = styled.input`
border-radius: 50%;
`;
const TimeCheckBoxWpr = styled.div`
grid-column: 4;
grid-row: 4;
grid-column: 2;
grid-row: 3;
margin-left: auto;
width: fit-content;
`;
const FieldSetMenu = styled.fieldset`
grid-row: 5/span 2;
grid-column: 1/span 4;
`;
const MenuSaveButton = styled.button`
grid-row: 9;
grid-column: 4;
`;
const DisplaySelectedDays = styled.div`
width: 100%;
border: 2px solid ${primaryLight};
padding: 5px;
`;
const SelectedText = styled.span`
margin: 0px;
`;
const LoadedImg = styled.img`
width: 80%;
height:auto;
`;
const NoImg = styled(LoadedImg)`
width: 40px;
margin-right: 5px;
`;
const NoImgTxt = styled.p`
font-size: 20px;
`;
const LoadedImgCont = styled.div`
width: 100%;
height: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
`;
const ArrowDownIcon = styled.img`
cursor: hover;
float: right;
width: fit-content;
`;
const CloseIcon = styled(ArrowDownIcon)``;
export {
ArrowDownIcon,
BaseCard,
CloseIcon,
CreateNewButton,
CustomListItem,
DashBoardGrid,
DashKpiWrapper,
DashMenuWrapper,
DashSalesGridWrapper,
DaySelectInput,
DaySelectPopUp,
DisplaySelectedDays,
FieldSetMenu,
GreenCard,
Grid,
IconImg,
ImgInput,
ImgDisplay,
LoadedImg,
LoadedImgCont,
MenuFormStyle,
MenuImgWrapper,
MenuNameInput,
MenuSaveButton,
MenuWrapper,
NavBackDrop,
NavWrapper,
NavLogoWrapper,
NavLinkWrapper,
NoImg,
NoImgTxt,
ProfileImgWrapper,
SelectedText,
StandardText,
TimeCheckBoxWpr,
TimeEndInput,
TimeStartInput,
TimeCheckBox,
UList,
}
>Solution :
In DropDownList you register some click outside listeners, but this happens in a useMemo when it should be a useEffect. Therefore the click handler is never removed when the popup closes which probably makes everything unelectable as preventDefault is called on every click.