Why is onBlur not triggered when I click outside of the component? - reactjs

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>

Related

how to integrate custom dropdown component and formik in react typescript

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;

Rerender FunctionComponent when prop gets set with same value

I'm currently trying to implement some kind of modal (I'm aware that there is a bunch of libraries for that). The real code is much more complex because of a bunch of animation stuff, but it boils down to this (also see this Stackblitz):
const Modal: React.FunctionComponent<{ visible?: boolean }> = ({
visible,
}) => {
const [isVisible, setIsVisible] = React.useState(visible);
React.useEffect(() => setIsVisible(visible), [visible]);
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => setIsVisible(false)}>Close</button>
</div>
);
};
const App: React.FunctionComponent = () => {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Show modal</button>
<Modal visible={showModal} />
</div>
);
}
The first time the parent component sets the visible property it works without a problem. But when I close the "modal" and want to set the property again it does not show up again, because the property from the point of view of the "modal" didn't actually change.
Is there a way to always rerender a FunctionComponent when a property gets touched even if the value didn't change?
Have you try this:
const Modal: React.FunctionComponent<{ visible?: boolean }> = ({
visible,
setIsVisible
}) => {
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => setIsVisible(false)}>Close</button>
</div>
);
};
const App: React.FunctionComponent = () => {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Show modal</button>
<Modal visible={showModal} setIsVisible={setShowModal} />
</div>
);
}
It will then re-render also your parent component, because they share the same state
you're trying changing the value in the child element, this does not get reflected in the parent
My suggestion is that to close the modal from parent itself
which reduces the code complexity and there is only single source of data here
export const Modal: React.FunctionComponent<{ visible?: boolean , onClose }> = ({
visible,onClose
}) => {
const [isVisible, setIsVisible] = React.useState(visible);
React.useEffect(() => setIsVisible(visible), [visible]);
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => onClose()}>Close</button>
</div>
);
};
<Modal visible={showModal} onClose={()=>setShowModal(false)} />
working example https://stackblitz.com/edit/react-ts-heiqak?file=Modal.tsx,App.tsx,index.html

Focus Trap React and a few component's in 2 popup's

I have 2 popup's(I reuse CloseButton(component) and Modal(component) in 2 popup's) and need to do focus trap at all. I lf answer 4 better way.
1 popup Screen, components: ModalLogin-Modal-CloseButton.
I read about some hooks: useRef() and forwardRef(props, ref)
but i don't undestand why it's not work in my case. I am trying to find a solution. I need help :)
In ModalLogin, I try to do a focus trap. To do this, I mark what should happen with focus when moving to 1 and the last element. I need to pass my ref hook obtained via Modal-CloseButton. I read that you can't just transfer refs to functional components. I try to use the forwardref hook in the necessary components where I transfer it, here's what I do:
All links without focus-trap and hook's!.
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/form-login/modal-login.jsx [Modal-login full]
const ModalLogin = () => {
const topTabTrap* = useRef();
const bottomTabTrap* = useRef();
const firstFocusableElement = useRef();
const lastFocusableElement = useRef();
useEffect(() => {
const trapFocus = (event) => {
if (event.target === topTabTrap.current) {
lastFocusableElement.current.focus()
}
if (event.target === bottomTabTrap.current) {
firstFocusableElement.current.focus()
}
}
document.addEventListener('focusin', trapFocus)
return () => document.removeEventListener('focusin', trapFocus)
}, [firstFocusableElement, lastFocusableElement])
return (
<Modal onCloseModal={() => onCloseForm()} ref={lastFocusableElement}>
<form >
<span ref={topTabTrap} tabIndex="0" />
<Logo />
<Input id="email" ref={firstFocusableElement} />
<Input id="password" />
<Button type="submit" />
<span ref={bottomTabTrap} tabIndex="0"/>
</form>
</Modal>
);
};
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/modal/modal.jsx [Modal full]
const Modal = forwardRef(({ props, ref }) => {
const { children, onCloseModal, ...props } = props;
const overlayRef = useRef();
useEffect(() => {
const preventWheelScroll = (evt) => evt.preventDefault();
document.addEventListener('keydown', onEscClick);
window.addEventListener('wheel', preventWheelScroll, { passive: false });
return () => {
document.removeEventListener('keydown', onEscClick);
window.removeEventListener('wheel', preventWheelScroll);
};
});
const onCloseModalButtonClick = () => {
onCloseModal();
};
return (
<div className="overlay" ref={overlayRef}
onClick={(evt) => onOverlayClick(evt)}>
<div className="modal">
<CloseButton
ref={ref}
onClick={() => onCloseModalButtonClick()}
{...props}
/>
{children}
</div>
</div>
);
});
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/close-button/close-button.jsx [CloseButton full]
const CloseButton = forwardRef(({ props, ref }) => {
const {className, onClick, ...props} = props;
return (
<button className={`${className} close-button`}
onClick={(evt) => onClick(evt)}
tabIndex="0"
ref={ref}
{...props}
>Close</button>
);
});
And now i have a lot of errors just like: 1 - Cannot read properties of undefined (reading 'children') - Modal, 2 - ... className undefined in CloseButton etc.
2 popup Screen, components: Modal(reuse in 1 popup) - InfoSuccess- CloseButton(reuse in 1 popup)
I have only 1 interactive element - button (tabindex) and no more. Now i don't have any idea about 2 popup with focus-trap ((
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/success-modal/success-modal.jsx [SuccessModal full]
const SuccessModal = ({ className, onChangeVisibleSuccess }) => {
return (
<Modal onCloseModal={() => onChangeVisibleSuccess(false)}>
<InfoSuccess className={className} />
</Modal>
);
};
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/info-block/info-block.jsx [Infoblock full]
const InfoBlock = ({ className, title, desc, type }) => {
return (
<section className={`info-block ${className} info-block--${type}`}>
<h3 className="info-block__title">{title}</h3>
<p className="info-block__desc">{desc}</p>
</section>
);
};
const InfoSuccess = ({ className }) => (
<InfoBlock
title="Спасибо за обращение в наш банк."
desc="Наш менеджер скоро свяжется с вами по указанному номеру телефона."
type="center"
className={className}
/>
);
I know about 3 in 1 = 1 component and no problem in popup with Focus-Trap. But i want understand about my case, it's real to life or not and what best practice.

Material UI Autocomplete - Disable listbox after the item selection

I'm creating an Autocomplete component in React.js with the help of Material-UI headless useAutoComplete hook. The component is working properly. When the user tries to type any character, the Listbox will automatically open.
But the problem is that when the user selects anything and pays attention to the input element, the ListBox reopens. How can I prevent this?
Codesandbox:
Code:
import React from 'react';
import useAutocomplete from '#material-ui/lab/useAutocomplete';
function AutocompleteComponent(props) {
const { data } = props;
const [value, setValue] = React.useState('');
const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = function () {
if (value.length > 0) {
setIsOpen(true);
}
};
const handleInputChange = function (event, newInputValue) {
setValue(newInputValue);
if (newInputValue.length > 0) {
setIsOpen(true);
} else {
setIsOpen(false);
}
};
const {
getRootProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions
} = useAutocomplete({
id: 'form-control',
options: data,
autoComplete: true,
open: isOpen, // Manually control
onOpen: handleOpen, // Manually control
onClose: () => setIsOpen(false), // Manually control
inputValue: value, // Manually control
onInputChange: handleInputChange, // Manually control
getOptionLabel: (option) => option.name
});
const listItem = {
className: 'form-control__item'
};
return (
<div className="app">
<div className="form">
<div {...getRootProps()}>
<input
type="text"
className="form-control"
placeholder="Location"
{...getInputProps()}
/>
</div>
{groupedOptions.length > 0 && (
<ul
className="form-control__box"
aria-labelledby="autocompleteMenu"
{...getListboxProps()}
>
{groupedOptions.map((option, index) => {
return (
<li {...getOptionProps({ option, index })} {...listItem}>
<div>{option.name}</div>
</li>
);
})}
</ul>
)}
</div>
</div>
);
}
export default AutocompleteComponent;
You can store the selectedItem in a state and use it in handleOpen to decide whether the list has to be displayed.
For setting selectedItem you can modify the default click provided by getOptionProps
import React from 'react';
import useAutocomplete from '#material-ui/lab/useAutocomplete';
function AutocompleteComponent(props) {
const { data } = props;
const [value, setValue] = React.useState('');
const [isOpen, setIsOpen] = React.useState(false);
const [selectedItem, setSelectedItem] = React.useState('');
const handleOpen = function () {
if (value.length > 0 && selectedItem !== value) {
setIsOpen(true);
}
};
const handleInputChange = function (event, newInputValue) {
setValue(newInputValue);
if (newInputValue.length > 0) {
setIsOpen(true);
} else {
setIsOpen(false);
}
};
const {
getRootProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions
} = useAutocomplete({
id: 'form-control',
options: data,
autoComplete: true,
open: isOpen, // Manually control
onOpen: handleOpen, // Manually control
onClose: () => setIsOpen(false), // Manually control
inputValue: value, // Manually control
onInputChange: handleInputChange, // Manually control
getOptionLabel: (option) => option.name
});
const listItem = {
className: 'form-control__item'
};
return (
<div className="app">
<div className="form">
<div {...getRootProps()}>
<input
type="text"
className="form-control"
placeholder="Location"
{...getInputProps()}
/>
</div>
{groupedOptions.length > 0 && (
<ul
className="form-control__box"
aria-labelledby="autocompleteMenu"
{...getListboxProps()}
>
{groupedOptions.map((option, index) => {
return (
<li
{...getOptionProps({ option, index })}
{...listItem}
onClick={(ev) => {
setSelectedItem(option.name);
getOptionProps({ option, index }).onClick(ev);
}}
>
<div>{option.name}</div>
</li>
);
})}
</ul>
)}
</div>
</div>
);
}
export default AutocompleteComponent;
You can modify the props passed to the inputbox, before applying them. That way you can delete the event that happens when the input is clicked.
var newProps = getInputProps();
delete newProps.onMouseDown; //delete the extra MouseDown event
return (
...
<input
...
placeholder="Location"
{...newProps} //use newProps rather than calling getInputProps
And it will stop showing that popup when you click it.
You can check the working code here

react: manually trigger input component synthetic change event

I'm trying to build an input component with a clear button using react#17
import { useRef } from 'react';
const InputWithClear = props => {
const inputRef = useRef();
return (
<div>
<input
ref={inputRef}
{...props}
/>
<button
onClick={() => {
inputRef.current.value = '';
inputRef.current.dispatchEvent(
new Event('change', { bubbles: true })
);
}}
>
clear
</button>
</div>
);
};
using this component like:
<InputWithClear value={value} onChange={(e) => {
console.log(e); // I want to get a synthetic event object here
}} />
but the clear button works once only when I did input anything first, and stop working again.
if I input something first and then click the clear button, it does not work.
why not using?
<button
onClick={() => {
props.onChange({
target: { value: '' }
})
}}
>
clear
</button>
because the synthetic event object will be lost
So, how do I manually trigger a synthetic change event of a react input component?
Try this approach,
Maintain state at the parent component level (Here parent component is App), onClear, bubble up the handler in the parent level, and update the state.
import React, { useState } from "react";
import "./styles.css";
const InputWithClear = (props) => {
return (
<div>
<input {...props} />
<button onClick={props.onClear}>clear</button>
</div>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<InputWithClear
value={value}
onChange={(e) => {
console.log(e); // I want to get a synthetic event object here
setValue(e.target.value);
}}
onClear={() => {
setValue("");
}}
/>
</div>
);
}
Working code - https://codesandbox.io/s/youthful-euler-gx4v5?file=/src/App.js
you should use state to control input value rather than create useRef, that's the way to go. you can use a stopPropagation prop to control it:
const InputWithClear = ({value, setValue, stopPropagation = false}) => {
const onClick = (e) => {
if(stopPropagation) e.stopPropagation()
setValue('')
}
return (
<div>
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
<button
onClick={onClick}
>
clear
</button>
</div>
);
};
export default function App() {
const [value, setValue] = useState('')
return (
<div className="App">
<InputWithClear value={value} setValue={setValue} stopPropagation />
</div>
);
}

Resources