I want to perform a function after a div element has left focus.
I'm using tabIndex and onBlur function inside the div. And its working fine when i manually put focus by clicking on any of the elements inside the div. But by default when no item is clicked inside the div, its not working.
My component is a Functional Component & the div is rendered dynamically so also I'm unable to set focus using useRef.
const renderHeaderCell = (header, headerKey) => {
return (
<div className="DataTable__header-cell-wrapper">
{filterable ? (
<IconButton onClick={() => toggleFilterPanel(headerKey)}>
<i className="material-icons">filter_list</i>
</IconButton>
) : null}
{activeFilterHeader === headerKey ? (
<div
tabIndex={0}
onFocus={e => {
console.log("DIV", "focus");
}}
onBlur={e => {
console.log("DIV", "blur");
}}
style={{ border: "1px solid blue" }}
>
DIV container
<input
type="text"
onFocus={e => {
console.log("input", "focus");
}}
onBlur={e => {
e.stopPropagation();
console.log("input", "blur");
}}
placeholder="Inside Textbox"
/>
Click outside
</div>
) : null}
{sortedByColumn === headerKey ? renderSortIcon() : null}
</div>
);
};
Code after i click the icon to show the DIV
const toggleFilterPanel = headerKey => {
if (activeFilterHeader === headerKey) {
setActiveFilterHeader("");
} else {
setActiveFilterHeader(headerKey);
setUniqueItemsForFilter(getUniqueItemsForFilter(rows, headerKey));
}
};
Code after onBlur is called
const onBlur = () => {
console.log("Blured");
};
So how shall i make onBlur to work on a div element?
Following image shows current focus
blur event may be very trick especially if you have focusable elements inside focusable element.
Open console and play with that piece of code a little bit in order to understand better how 'blur' and 'focus' events work.
class BlurExample extends React.Component {
render() {
const {title} = this.props;
return (
<div
tabIndex={ 0 }
onFocus={ () => {console.log('main', 'focus');} }
onBlur={ () => {console.log('main', 'blur');} }
style={ { border: '1px solid coral', padding: '10px', fontFamily: 'sans-serif' } }
>
Click here 1
<input
type="text"
onFocus={ () => {console.log('input', 'focus');} }
onBlur={ () => {console.log('input', 'blur');} }
placeholder="Click here 2"
style={ { margin: '0 10px', padding: '10px' } }
/>
Click here 3
</div>
);
}
}
ReactDOM.render(
<BlurExample />,
document.body
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
blur event won't happen if you click inside focused element. Exception if you have another focusable element inside and you click on it. But Notice that after main blur you will see input focus and main focus and if you will click outside of the main element you will also see two blur events: input blur and after that main blur
Related
So, I am creating a dropdown menu in React and if I click outside the dropdown menu, it should close. For that, I am currently using click eventListeners. Is there any other way that can be used instead of using eventListeners? I tried with onFocus and onBlur, but that doesn't seem to work.
Here's the code snippet:
const [showMenu, setShowMenu] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
//hiding the dropdown if clicked outside
const pageClickEvent = (e: { target: unknown }) => {
if (dropdownRef.current !== null && !dropdownRef.current.contains(e.target)) {
setShowMenu(!showMenu);
}
};
//if the dropdown is active then listens for click
if (showMenu) {
document.addEventListener("click", pageClickEvent);
}
//unsetting the listener
return () => {
document.removeEventListener("click", pageClickEvent);
};
}, [showMenu]);
<Button onClick = {() => setShowMenu(!showMenu)} />
{showMenu ? (
<div className="dropdown-content" ref={dropdownRef} >
<a>
...
<a>
</div>
) : null}
Yes there is. Use an overlay under the menu.
function MyComponent() {
const [menuVisible, setMenuVisible] = useState(false);
return (
<div>
<button className='dropdown-button' onClick={() => setMenuVisible(true)}>Click me</button>
{menuVisible ? (
<ul className='dropdown-menu'>
{/* items go here */ }
</ul>
) : null}
{/* now the important part */}
{menuVisible ? (<div className='overlay' onClick={() => setMenuVisible(false)} />) : null}
</div>
)
}
CSS
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0, 0, 0, 0.01);
}
<DragAndDropCalendar
selectable
localizer={localizer}
events={events}
style={{ height: 1550 }}
onSelectSlot={(e) => handleSelect(e)}
onSelectEvent={(e) => handleSelectedEvent(e)}
/>
here's the function:
function handleSelectedEvent (e) {
<div className="modal">
{console.log(e)}
</div>
}
The issue:
The modal wont show up, it does show in console log but then I tried to put it in a modal, it just does not render. I have tried react-responsive-modal and also other bootstrap modals but it just does not render.
import React, { useState} from 'react'
function Calendar() {
const [selectedEvent, setSelectedEvent] = useState(undefined)
const [modalState, setModalState] = useState(false)
const handleSelectedEvent = (event) => {
setSelectedEvent(event)
setModalState(true)
}
const Modal = () => {
return (
<div className={`modal-${modalState == true ? 'show' : 'hide'}`}>
// Here you define your modal, what you want it to contain.
// Event title for example will be accessible via 'selectedEvent.title'
</div>
)
}
return (
<div>
{selectedEvent && <Modal />}
<Calendar
selectable
localizer={localizer}
events={events}
style={{ height: 1550 }}
onSelectSlot={(e) => handleSelect(e)}
onSelectEvent={(e) => handleSelectedEvent(e)}
/>
</div>
)
}
And then, in css, you have to do:
.modal-show {
display: block;
}
.modal-hide {
display: none;
}
I'm trying to add a white outline via css box-shadow, but whenever I click on any of the buttons, they all get the outline instead of just the actual button I clicked.
Is there a way so only the button component I click on gets the outline and then toggles off if I click it again?
Here is my current code:
const [selectState, setSelectState] = useState(false);
const Button = ({ selected, text }) => {
function handleClick() {
setSelectState(true);
}
return (
<span
onClick={handleClick}
className={`btn-style ${selected ? "selected" : ""}`}
>
{text}
</span>
);
};
export default function Hello() {
return (
<Button selected={selectState} text='Blue'/>
<Button selected={selectState} text='Red'/>
<Button selected={selectState} text='Green'/>
);
}
.selected css:
.selected {
box-shadow: rgb(17 206 101) 0px 0px 0px 2px inset !important;
}
If you want to track the selected state of individual elements, you'd need to handle the onClick method and make corresponding state change in parent element.
const Button = ({ selected, text, onClick }) => {
return (
<span
onClick={onClick}
className={`btn-style ${selected ? "selected" : ""}`}
>
{text}
</span>
);
};
export default function Hello() {
const [selectState, setSelectState] = React.useState(0);
return (
<React.Fragment>
<Button
onClick={() => setSelectState(1)}
selected={selectState === 1}
text="Blue"
/>
<Button
onClick={() => setSelectState(2)}
selected={selectState === 2}
text="Red"
/>
<Button
onClick={() => setSelectState(3)}
selected={selectState === 3}
text="Green"
/>
</React.Fragment>
);
}
You can have the click handler tell the parent component to save the clicked button index in state, and pass that state down to determine whether the selected class is needed:
const Button = ({ selected, text, onClick }) => {
return (
<span
onClick={onClick}
className={`btn-style ${selected ? "selected" : ""}`}
>
{text}
</span>
);
};
export default function Hello({ texts }) {
const [selectedIndex, setSelectedIndex] = useState(-1);
return (<>
{
texts.map((text, i) => <Button selected={i === selectedIndex} text={text} onClick={() => setSelectedIndex(i)}} />)
}
</>);
}
ReactDOM.render(<Hello texts={['Blue', 'Red', 'Green']} />, document.querySelector('.react'));
I want to click X Button in Card extra to visible "Confirm Remove Todo modal".
UI:
But...
the reality when I click X Button then it visible "Edit Todo modal" from Card event instead.
how can I fix it?
Code:
{todos.map(todo => (
<Card
className={styles.CardTodo}
headStyle={{ textAlign: 'left' }}
bodyStyle={{ textAlign: 'left' }}
key={todo._id}
title={todo.title}
onClick={() => handleSelectTodo(todo._id)}
extra={
<Button
type="danger"
shape="circle"
style={{ color: 'white', zIndex: 10 }}
onClick={() => handleRemoveTodo(todo._id)}
>
X
</Button>
}
>
{todo.description}
</Card>
))}
.
.
Thanks very much, guys
e.stopPropagation() is useful for me.
And then I found another problem.
It is handleRemoveTodo() is the function that opens another modal.
But that modal didn't get "Todo object"
when I remove e.stopPropagation(), the modal will get Todo Object again
Code:
Todo component
const handleRemoveTodo = () => {
setModalConfirmRemoveVisible(true)
}
const handleConfirmRemove = async todoId => {
console.log('Hello', todoId)
setIsRemoveLoading(true)
try {
await axios.delete(`/todos/${todoId}`, apiConfig)
} catch (err) {
console.error(err)
console.error(err.response.data)
}
await fetchTodos()
setModalConfirmRemoveVisible(false)
setIsRemoveLoading(false)
}
return (
{modalConfirmRemoveVisible && (
<ModalConfirmRemoveTodo
visible={modalConfirmRemoveVisible}
todo={todo}
isRemoveLoading={isRemoveLoading}
onConfirmRemoveTodo={handleConfirmRemove}
onCancel={() => setModalConfirmRemoveVisible(false)}
onConfirmRemove={handleConfirmRemove}
/>
)}
)
Modal component
const ModalConfirmRemoveTodo = props => {
const { visible, isRemoveLoading, onCancel, todo, onConfirmRemove } = props
console.log('ModalConfirmRemoveTodo', todo)
return (
<>
<Modal
visible={visible}
title={<Title level={3}>Remove Todo</Title>}
okButtonProps={{ loading: isRemoveLoading, disabled: isRemoveLoading }}
okText="Remove"
okType="danger"
onOk={() => onConfirmRemove(todo._id)}
onCancel={onCancel}
>
Want delete {todo.title} ?
</Modal>
</>
)
}
This is called Event Bubbling. When an event happens on an element, it first runs the handlers on it, then on its parent, then all the way up on other ancestors.
Please refer to this article for details: https://javascript.info/bubbling-and-capturing#bubbling
Below is my solution to your problem. Instead of opening a modal, I just use a simple alert to simulate it.
Your current problem: https://codesandbox.io/s/event-bubbling-bojvq
You will see that the Chrome alert will pop up twice. The former is from the onClick of extra, the latter is from onClick of Card.
Solution: https://codesandbox.io/s/prevent-bubbling-zkxk6
Just add a simple e.stopPropagation() to prevent the bubbling inside extra Button onClick. Please refer to this: https://javascript.info/bubbling-and-capturing#stopping-bubbling for more information.
Back to your code, just simply update your Button's onClick like this:
onClick={e => { e.stopPropagation(); handleRemoveTodo(todo._id)}}
Use stopPropagation() method on your event:
<Button
type="danger"
shape="circle"
style={{ color: 'white', zIndex: 10 }}
onClick={e => { e.stopPropagation(); handleRemoveTodo(todo._id)}}
>
X
</Button>
The useState's value will change when the current button is clicked, which will call the child component.
Child component is Modal.
However, when the parent's button is repressed, the changed value is not changed again and will not be recalled.
https://codesandbox.io/s/patient-snowflake-wdm78
You can send the callback function to the Child Component like this.
StepComp.js
const StepComp = () => {
const [isModalVisible, setIsModalVisible] = useState(false);
const btnStyled = {
margin: "10rem"
};
const setVisible = visible => {
setIsModalVisible(visible);
};
return (
<div>
<Button onClick={() => setIsModalVisible(true)} style={btnStyled}>
jump
</Button>
<LoginModal open={isModalVisible} setVisible={setVisible} />
</div>
);
};
LoginModal.js
const LoginModal = ({ open, setVisible }) => {
const inputStyled = {
borderColor: "none",
borderBottom: "1px solid #EBEBEB",
marginBottom: "1rem"
};
return (
<div>
<Modal
title="login"
centered
visible={open}
onOk={() => setVisible(false)}
onCancel={() => setVisible(false)}
>
<div>
<input type="text" style={inputStyled} /> <br />
<input type="text" style={inputStyled} />
</div>
</Modal>
</div>
);
};
You're using isModelVisible and visible state properties, and initializing them a a boolean, then setting them to the opposite. This works, but only the first time.
What you need to do is setVisible(!visible) and setIsModalVisible(!isModalVisible) respectively.
While this will make it work, what I suggest doing instead is actually only having one state property, in the parent (StepComp), which then is passed as a callback to the child (Modal) along with the property whether it is open, then you render the child, if it's true, and use the same setState function in the child to close it.
This approach keeps all the logic for opening the Modal in one component.