<parent onBlur={function}>
<child>
<child>
</child>
</child>
</parent>
In a structure like this, I want to catch if parent element loses focus or not. I want to fire that behavior when clicked outside of the parent div. However, parent element loses its focus when I click child element as well. You can think this structure as dropdown menu. Is there a way to achieve this inside of the stateless react component? I cannot use jquery or other libraries.
This can be achieved with mouse events.
If the user clicks on the screen we can check if the click is inside or outside the component and treat it as Focus event to blur event.
Click Inside - Focus
Click Outside - Blur
function Parent(props){
const ref = useRef();
useEffect(()=>{
document.addEventListener("mousedown", checkFocus.bind(this))
return ()=>{
document.removeEventListener("mousedown", checkFocus.bind(this))
}
},[])
const checkFocus = (event)=>{
if(ref.current && !ref.current.contains(event.target) && props.onBlur)
props.onBlur()
}
return (
<div ref={ref}>
{props.children}
</div>
);
}
function Child(){
return (
<h1>child</h1>
);
}
function Main(){
return (
<Parent onBlur={()=>{ console.log("focus lost.") }}>
<Child>
<Child>
</Child>
</Child>
</Parent>
);
}
Related
I'm using the material-ui lib and I need to have an autocomplete where each item inside that autocomplete is clickable and opens a modal.
The structure in general is:
const ModalBtn = () => {
...
return (
<>
<button ... (on click set modal to open)
<Modal ...
</>
);
}
const AutoCompleteWithBtns = () => {
return (
<Autocomplete
renderTags={(value, getTagProps) =>
value.map((option, index) => <ModalBtn />)
}
...
/>
);
}
Note that the ModalBtn is a component that cannot be divided into two components of Button and Modal.
The issue is that when you click on the button inside the modal - the focus is kept inside the autocomplete, and the modal will never gets the focus (if I have an input inside the modal - I can't write anything inside).
Tried all the standard autocomplete/modal focus-related props (disableEnforceFocus, disableEnforceFocus, etc...) but nothing works.
Here is a working codesandbox example. As you can see - if you click on the button that is not inside the autocomplete component - everything works (you can add text inside the input field). If you click on the button that is inside the autocomplete - the input field inside the modal is not editable (you lose focus).
Here is an example of the issue:
The problem with having the Modal rendered from within the Autocomplete is that events propagate from the Modal to the Autocomplete. In particular, click and mouse-down events are both handled by Autocomplete in a manner that causes problems in your case. This is primarily logic intended to keep focus in the right place as you interact with different parts of the Autocomplete.
Below (from https://github.com/mui-org/material-ui/blob/v4.9.11/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js#L842) is the portion of the Autocomplete code that is getting in your way:
// Prevent input blur when interacting with the combobox
const handleMouseDown = (event) => {
if (event.target.getAttribute('id') !== id) {
event.preventDefault();
}
};
// Focus the input when interacting with the combobox
const handleClick = () => {
inputRef.current.focus();
if (
selectOnFocus &&
firstFocus.current &&
inputRef.current.selectionEnd - inputRef.current.selectionStart === 0
) {
inputRef.current.select();
}
firstFocus.current = false;
};
The default browser behavior when a mouse down event occurs on a focusable element is for that element to receive focus, but the mouse-down handler for Autocomplete calls event.preventDefault() which prevents this default behavior and thus prevents a focus change from the mouse-down event (so focus stays on the Modal itself as indicated by its blue focus outline). You can however successfully move focus to the Modal's TextField using the tab key, since nothing is preventing that mechanism of focus change.
The Autocomplete click handler is keeping focus on the input of the Autocomplete even if you click some other part of the Autocomplete. When your Modal is open, the effect of this is that when you click in the Modal, focus is moved briefly to the Autocomplete input element, but the focus is immediately returned to the Modal due to its "enforce focus" functionality. If you add the disableEnforceFocus property to the Modal, you'll see that when you click in the Modal (e.g. on the TextField) the cursor remains in the input of the Autocomplete.
The fix is to make sure that these two events do NOT propagate beyond the Modal. By calling event.stopPropagation() for both the click and mouse-down events on the Modal, it prevents the Autocomplete functionality for these two events from being executed when these events occur within the Modal.
<Modal
onClick={event => event.stopPropagation()}
onMouseDown={event => event.stopPropagation()}
...
Related answer: How can I create a clickable first option in Material UI Labs Autocomplete
The problem in your code was that the Modal was rendered from within the tag of the AutoComplete component, which was not ok because of the visibility of the components, the hierarchy of the components was the issue.
The fix is to move the Modal within the FixedTags component and pass the open handler to the ModalBtn in the renderTags prop;
I've updated your sandbox with a working variant HERE
The changes are below
const ModalBtn = ({ handleOpen }) => (
<button type="button" onClick={handleOpen}>
Open Modal (not working)
</button>
);
const FixedTags = function() {
const classes = useStyles();
const [modalStyle] = React.useState(getModalStyle);
const [open, setOpen] = React.useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<>
<Autocomplete
multiple
options={autoCompleteItems}
getOptionLabel={option => option.title}
defaultValue={[autoCompleteItems[1], autoCompleteItems[2]]}
renderTags={(value, getTagProps) =>
value.map((option, index) => <ModalBtn handleOpen={handleOpen} />)
}
style={{ width: 500 }}
renderInput={params => (
<TextField
{...params}
label="Fixed tag"
variant="outlined"
placeholder="items..."
/>
)}
/>
<Modal open={open} onClose={handleClose}>
<div style={modalStyle} className={classes.paper}>
<h2 style={{ color: "red" }}>This one doesn't work</h2>
<p>Text field is not available</p>
<TextField label="Filled" variant="filled" /> <br />
<br />
<br />
<FixedTags label="Standard" />
</div>
</Modal>
</>
);
};
I am using Preact with hooks. I have following button component:
export function Button(props) {
return (
<button class={props.class}>{props.children}</button>
);
}
I have another parent component where I need to access actual DOM element button for animation purpose.
export function Parent(props) {
const buttonElm = useRef(null);
useEffect(() => {
console.log(buttonElm.current);
// Animate button using popmotion or similar
});
return (
<div>
<Button ref={buttonElm}>Click me to animate</Button>
</div>
);
}
However, there is a problem. The buttonElm.current points to JSX object i.e. Button but not the DOM element button. I need buttonElm to point to actual DOM element. How do I do that?
Should I go ahead and use buttonElm.current.base property? But that does not feel idiomatic with hooks.
Also, I have two questions.
How does ref behave when I am setting it on a Preact component that returns multiple elements using <Fragment />.
Second, is accessing the children's DOM element for animation purpose acceptable/correct practice in Preact/React? (I can wrap my component in another wrapper div but that causes more animation headaches than solving the problem)
You need to pass ref as props to your child component. By doing this buttonElm will point to actual Button DOM element.
export function Button(props) {
return (
<button class={props.class} ref={props.buttonElm}>{props.children}</button>
);
}
export function Parent(props) {
const buttonElm = useRef(null);
useEffect(() => {
console.log(buttonElm.current);
// Animate button using popmotion or similar
});
return (
<div>
<Button buttonElm={buttonElm}>Click me to animate</Button>
</div>
);
}
I have a Main component with code
changeColor = (color) => {
}
toggle = (e) => {
console.log(e.target)
}
<div>
<EditComponent changeColor={this.changeColor.bind(this)}>
<TextComonent toggle={this.toggle.bind(this)}>
</div>
Edit component is
color = (value) => {
this.props.changeColor(value)
}
<div>
<button value='red' onClick={this.color.bind(this,"red")}>Red</button>
<button value='blue' onClick={this.color.bind(this,"blue")}>Blue</button>
</div>
Text component is
toggle = (e) => {
this.props.toggle(e)
}
<div>
<p class="black-color" onClick={this.toggle.bind(this)}>Text 1</p>
<p class="black-color" onClick={this.toggle.bind(this)}>Text 2</p>
</div>
I will be clicking on Text 1 or Text 2 first and I will get the event inside toggle function. Next I will click the button Red or Blue. Then I want to change the class to either red-color or blue-color for that particular Text that i have clicked before. How can I get the event inside the parent component to find the particular text or is there any other way to to this?
I want to get the event.target inside the Parent component. I got the event object in parent but event.target is null
<div>
<EditComponent changeColor={this.changeColor.bind(this)}>
<TextComonent toggle={this.toggle}>
</div>
try this way dont bind function in parent component and try,you will get the target
You are not using "bind" correctly. You don't need to bind with an anonymous function.
class Hello extends React.Component {
render() {
return (
<div>
<p onClick={(e) => this.toggle(e)}>
Test
</p>
</div>);
}
toggle = (e) => {
console.log(e.target.innerText);
}
}
From the event variable in toggle, you can perform your changes as need be.
I found the exact solution as to add event.persist(); to get the event.target inside parent component.
In my parent component, I used a component called List as follows.
render() {
return (
<div className="experiments">
<div className="experiments-list-container">
<List rowItems={this.state.employeeData} />
</div>
</div>
);
}
}
In my List component, I am trying to change the style whenever each item of the row is clicked. So what I did is:
render() {
const dateDisplay = moment(this.props.createdAt).format('MMM YYYY');
return (
<tr
className={this.state.isExpanded ? 'testclass' : "experiment-list__row"}
//className="experiment-list__row"
onClick={this.handleRowClick}
>
<td>
{this.props.rowItems.firstName + ' ' + this.props.rowItems.lastName}
</td>
<td>{this.props.rowItems.jobTitle}</td>
<td>{'Email#Email.com'}</td>
<td>{this.props.rowItems.employmentType}</td>
</tr>
);
}
whenever I click a row in the table, it will all a function that changes the this.state.isExpanded to True. However, the style that I actually want to change is <div className="experiments"> or <div className="experiments-list-container">. But I am not sure how to change the style of the upper-level component. Please help.
EDIT
Thanks for the reply. What I have tried is,
const List = props => {
return (
<table className="experiment-list">
<tbody>
<ListHeader />
{props.rowItems.map((data, i) => <ListRow
key={i}
rowItems={data}
onRowClicked={props.onRowClicked} />)}
</tbody>
</table>
);
};
and
toggleEmployerInfo(e) {
alert('dd')
}
in my parent component.
Whenever I click each row, it alerts "dd" correctly.
However, what I eventually want to do is pass in the info of the row clicked.
In my parent component, I use the List by doing
<div className="experiments-list-container">
<List
rowItems={this.state.employeeData}
onRowClicked={this.toggleEmployerInfo.bind(this)}
/>
</div>
This does render all data into each row correctly, but how can I make each row correctly read the id of the item that the row has?
You could pass an event handler to the List component and call it whenever a row is clicked. Here I've defined handleRowClick in the parent component as an ES6 arrow function. Then I pass this function as a callback to the child component via the onRowClicked prop.
// parent.jsx
handleRowClick = (id) => {
// Handle click event, update state, etc.
console.log(id);
}
render() {
return (
<div className="experiments">
<div className="experiments-list-container">
<List rowItems={this.state.employeeData} onRowClicked={this.handleRowClick} />
</div>
</div>
);
}
And then call the onRowClicked function on the onClick event for each element you want to react to.
// list.jsx
render() {
return (
// Extremely simplified example...
<div onClick={() => this.props.onRowClicked('row-id-goes-here')}>row content</div>
);
}
The arrow function syntax here allows us to specify parameters beside the default event parameter that you would get if you just used onClick={this.props.onRowClicked}.
Here is a post explaining this approach better than I can: https://medium.com/#machnicki/handle-events-in-react-with-arrow-functions-ede88184bbb
I'm trying to handle key event when load page component.
First, I have a router:
<Router>
<Route exact path="/" component={Home} />
</Router>
In home component, I try to bind onKeyPress in div element but it's not work. I bind it on input element, it's worked.
return (
<div onKeyDown={this.__handleKeyDown} className="container" style={{ backgroundImage: `url(${this.state.backgroundbanner})` }}>
<input
className="hidden"
onKeyDown={this.__handleKeyDown}
ref={(input) => { this.dummyInput = input; }}
/>
<div className="container-shadow">
<h1 className="main-title">{this.state.title}</h1>
<h3 className="main-description">{this.state.description}</h3>
<ListMovie cursor={ cursor } />
</div>
</div>
)
How to bind event onKeyDown on div element or how to bind key event when load a page component in Route. Because, input element can be out-focus and this key event cannot be excute.
Thanks.
The reason as to why it doesn't work is that the div element requires the tabIndex attribute in order to be focusable and to handle keyDown events.
<div tabIndex="1" onKeyDown={this.__handleKeyDown}></div>
Approach 1:
For the event to trigger, your div needs to be selected. To do this you need to focus it in the componentDidMount event. And to do this you need a ref to your div.
Step 1: get a ref to your div
<div onKeyDown={this.__handleKeyDown} ref={(c) => {this.div = c;}}>
Step 2: Focus it on load
componentDidMount() {
this.div.focus();
}
Approach 2:
Listen to events on the entire document
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
}