I am building an app with React. I am hiding a file input element (<input type="file"/>) "behind" a react-bootstrap Button to be able to control styling. So, when the button is clicked I turn around and fire a synthetic click event on the text input element, as shown below.
class OpenFileButton extends React.Component {
...
clickHandler() {
this.refs['input'].click();
}
render() {
return (
<ButtonGroup>
<div>
<input type="file" onChange={this.props.someCallback}
ref="input" style={{display: 'none'}}/>
<Button onClick={this.clickHandler}>Open File</Button>
</div>
</ButtonGroup>
);
}
}
I want to be able to test this with Jest/Enzyme. However, while I can simulate a click event on the button, I haven't figured out how to detect a synthetic click event on the file input element.
I have tried using Jest/Enzyme to mock the click method on the input element.
const component = mount(<OpenFileButton/>);
const fileInput = component.find('input');
const button = component.find('Button');
fileInput.click = jest.fn();
button.simulate('click');
expect(fileInput.click).toHaveBeenCalled();
However, mocking the click method this way does not work. I also can't add an onClick attribute, i.e. fileInput.props().onClick = jest.fn() does not work.
This question is about detecting synthetic click events in the code itself, not in the test code, and so is not relevant.
So, how can I detect a (synthetic) click event on a DOM element using Jest/Enzyme?
<input /> or this.refs.input is an instance of HTMLInputElement.
Then you can test if HTMLInputElement.prototype.click is called .
Using sinon you will have :
import sinon from 'sinon';
import {mount} from 'enzyme';
const clickInputSpy = sinon.spy(HTMLInputElement.prototype, 'click')
const component = mount(<OpenFileButton/>);
const button = component.find('Button');
button.simulate('click');
expect(clickInputSpy.called).toBeTruthy();
clickInputSpy.restore();
const clickInputSpy = sinon.spy(HTMLInputElement.prototype, 'click');
console.log(
'Is <input /> clicked ? ', clickInputSpy.called
);
document.querySelector('input').click();
console.log(
'Is <input /> clicked ? ', clickInputSpy.called
);
<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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sinon.js/1.15.4/sinon.min.js"></script>
<input />
The solution here involves spying on the click method of the particular file input element that I'm interested in. I can thus check to see if this file-input-element-click-spy was called after a click is simulated on the button element, as follows:
const openFileButtonWrapper = mount(<OpenFileButton/>);
const buttonWrapper = openFileButtonWrapper.find(Button);
const fileInputWrapper = openFileButtonWrapper.find('input [type="file"]');
const fileInput = fileInputWrapper.get(0);
const clickInputSpy = spyOn(fileInput, 'click');
buttonWrapper.simulate('click');
expect(clickInputSpy).toHaveBeenCalled();
The answer by #AbdennourTOUMI used Sinon's spy method which reminded me that Jest uses some Jasmine functionality, including its spyOn method, that isn't obvious from the Jest documentation. So, even though that other answer ended up spying on _all_ input elements, which is not ideal, it did send me in the right direction, so thank you, Adbennour.
Related
I have the following problem, in my web site build with nextJS and ReactJS with typescript I have products that are loaded when a button is clicked, when I click the button the items appeared and the button is scrolled down, which is the asked behavior, but when I scroll to the bottom of the page and I try to click the button the scroll remains on the same position and the items are loaded but cannot be seen, my logic is to use onFocus on the current button and when I click it to change the scroll to him, that will solve the problem when the user has scrolled down to the bottom of the page, that way it will not remain on the bottom but rather it will automatically scroll up to the button and will see the new items loaded.
The problem is that the logic to load the products are in a different component in which I am reusing the current button and right prop I am sending function to the onClick on the button. My question is how can I use onFocus. Does it has to be in the child component inside the function or in the button component. I tried to make it work on the Button component, but it doesn't work. So I am stuck for the last 4 hours and I really need a push. I would be glad if you could shine some enlargement
Here I will enter the function in the parent component for the onClick prop :
const handleLoadMoreProducts = () => {
if (!isSearchPage) {
const mappedBreadcrumbs: string[] = categoryData.breadcrumbs.map(
(crumb: BreadCrumItem) => crumb.name
);
gtmBreadcrumbTrack(mappedBreadcrumbs);
}
<LoadMoreProducts handleLoadMoreProducts={handleLoadMoreProducts} />
And here is the component that uses the Button:
interface LoadMoreProductsProps {
handleLoadMoreProducts?: (MouseEvent: React.MouseEvent<HTMLButtonElement>) => void;
Focus?: (MouseEvent: React.MouseEvent<HTMLButtonElement>) => void;
}
const LoadMoreProducts: FC<LoadMoreProductsProps> = ({ handleLoadMoreProducts }) => (
<div className="LoadMoreProducts">
<Button type="button" importance="ghost" onClick={handleLoadMoreProducts}>
Load more products
</Button>
</div>
);
I think what you want to do is to forward the ref of the element you are trying to focus in the Button component using React.forwardRef and combine it with the useImperativeHandle hook in order to gain the ability to trigger the focus with the ref outside of the Button component.
You could create a ref for the element you are trying to focus and call the focus() function for the ref on click.
More information regarding forwarding refs and the useImperativeHandle hook.
I'll try to explain this further.
I have a material-UI List component, with ListItem component that is set to button=true thus makes the whole item a button.
inside I added that inside him I have a FontAwesomeIcon.
To hide the button I put it's style to visibility: "hidden" and the Icon to visibility: "visible" so it would be available to see. (little bad practice maybe, but did not though of another way).
Now, when someone presses the ListItem anywhere without the Icon, it activates the onClick of that ListItem - as it should, and it's good! but, when pressing the area where the Icon is, both OnClick events of the "Icon button" and the ListItem is called - as it should, but I don't want it to be that way.
Now, is there a way to make the small "nested" button to be "on top" of the parent button so only it's event would be called?
If not, is there a way to know from the parent onClick that it's pressed on the area without the Icon so I would call different functions based on the click area?
Also, any other idea will be gladly received as I am new to react and web in general and I'd want to have the best practices solutions.
Many thanks :)
This is unrelated to React. In JavaScript you can use event.stopPropagation() method to stop the propogation of event at any level.
https://www.w3schools.com/JSREF/event_stoppropagation.asp#:~:text=Definition%20and%20Usage,capturing%20down%20to%20child%20elements.
Here is the example of how you would do it in React
import React from "react";
import "./styles.css";
export default function App() {
const parentButtonHandler = () => {
console.log("parent");
};
const childButtonHandler = (e) => {
console.log("child");
e.stopPropagation();
};
return (
<div className="App">
<button onClick={parentButtonHandler}>
Hello CodeSandbox
<button onClick={childButtonHandler}>
Start editing to see some magic happen!
</button>
</button>
</div>
);
}
If I understand your question correctly, you got that issue because the event is bubbled.
You can read this for more information: https://javascript.info/bubbling-and-capturing
To solve it, you can use event.stopPropagation() in the event handler for click event on "Icon button", so the event wont be bubbled to the parent element which is the ListItem
I think it's bad idea to make nested buttons. it's harder to support and it makes your layout messy.
In your case you can do it based on few ideas:
You have two separate buttons in your ListItem
export const Comp = () => {
return (
<ListItem>
<button onClick={handleOnMainClick}>mainButton</button>
<button onClick={handleOnSecondClick}>secondButton</button>
</ListItem>
)
}
But it works if your buttons on left side or right side only.
If you want to place your functional button whatether you want you can place it by position
export const Comp = () => {
return (
<ListItem styles={{position: 'relative'}}>
<button onClick={handleOnMainClick}>mainButton</button>
<button
styles={{position: 'absolute', top: '50%', left: '50%'}}
onClick={handleOnSecondClick}>
secondButton
</button>
</ListItem>
)
}
I would like to trigger the input box to focus (and thus keyboard to pop up) when a Vue component appears.
It does not to work on iOS.
I tried using Vue's example directive (here), and HTML5 autoFocus but neither worked.
I created this example in a sandbox (https://codesandbox.io/s/lw321wqkm).
FWIW, I do not believe it is a JS limitation, as I've seen example work (such as React Native Web using autoFocus- see example)
Parent component
<template>
<div>
<TextComp v-if='showText' />
<button #click='showText = !showText'> Show/hide input </button>
</div>
</template>
...
Child component
<template>
<div>
<input ref='focusMe' type='text'/>
</div>
</template>
<script>
export default {
name: 'TextComp',
mounted () {
this.$refs.focusMe.focus()
}
}
</script>
I hope you must have found a solution by now.
But I just wanted to add this for other new users like me.
mounted () {
this.$refs.focusMe.focus() // sometime this doesn't work.
}
Try adding this instead.
this.$nextTick(() => this.$refs.focusMe.focus())
For more info check this
Edit: 14/06/2022
Prashant's answer also helped me understand the nextTick in more depth.
nextTick allows you to execute code after you have changed some data and Vue.js has updated the virtual DOM based on your data change, but before the browser has rendered that change on the page.
You can create a fake input field and focus it during the click event.
<template>
<div>
<TextComp v-if='showText' />
<button #click='onShowText'> Show/hide input </button>
<input type="text" ref="dummykeyboard" style="opacity:0">
</div>
</template>
<script>
export default {
methods:{
onShowText() {
this.showText = !this.showText;
this.$refs.dummykeyboard.focus();
}
}
}
</script>
I would suggest triggering a click event on that field instead of focus
this.showText = !this.showText;
this.$nextTick(function () {
this.$refs.dummykeyboard.click();
})
I would go with this solution and it works for me. Just add set timeout and put focus method inside it
<template>
<div>
<input ref='focusMe' type='text'/>
</div>
</template>
<script>
export default {
name: 'TextComp',
mounted () {
setTimeout(() => {
this.$refs.focusMe.focus();
}, 500);
}
}
</script>
EDIT:
Ideally you can use $nextTick to wait until the DOM fully loaded, but sometimes it didn't work. So using setTimeout is just quick workaround to me.
https://v2.vuejs.org/v2/guide/custom-directive.html
In vue's official guide it says auto focus does not work in mobile safari. When the page loads, that element gains focus (note: autofocus doesn’t work on mobile Safari).
In iOS mobile safari, focus() only works when responding to a user interaction, like a click event. See: Mobile Safari: Javascript focus() method on inputfield only works with click?
My guess why it does not work in Vue:
In your Vue example, when you click on the button, it merely inserts a watcher into a batcher queue. A watcher has the information about what needs to update. You can see it as an update action, or update event. And later (almost immediately, at the next tick), Vue reads it (watcher) from the queue, and update the virtual dom / dom subsequently. However, this means, your code focus() is not "inside" a click event handler, rather, it is executed after the click event handler finishes.
I don't know the internal implementation of React though, so cannot explain why it works in the React example.
Still focus doesn't works for you???
Here is the solution :
setTimeout(() => {
this.$refs["input-0"].focus();
}, 1000);
The trick is to use setTimeout.
I have a hard time understanding react synthetic events.
The way I debug with native javascript events is something like this
window.addEventListener('mousemove',
function(event){
console.log(event);
})
If I move my mouse, and check the console, I can see the events properties such as target and other useful properties such as innerHTML, nextElementSibling, lastChild etc.
I have a simple react app below. Its an input field with a submit button. If something is entered in the input when submitted, an alert pops up. Otherwise nothing
// React Synthetic Events
class AddOption extends React.Component {
handleAddOption(e){
e.preventDefault();
console.log(e);
const option = e.target.elements.option.value; // < HOW WOULD I FIND THIS IN THE DEBUGGER?
if (option){
alert("something was entered in");
}
}
render() {
return (
<div>
<form onSubmit={this.handleAddOption}>
<input type="text" name="option" />
<button>React Submit</button>
</form>
</div>
)
}
}
ReactDOM.render(<AddOption />, document.getElementById('app'));
<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>
<div id="app"><!-- reactapp -->
What I don't get is if I inspect the page it doesn't give that useful information like I get with native events.
The issue is I referenced in my reactapp (following a tutorial) to use e.target.element.options.value. I have no idea how someone would find that out in the debugger. I don't see a reference to anything dubbed element in any of the long nested chain of prototype properties under synthetic event's target. I tried CTRL+F but I don't think chrome supports searching nested prototypes
Am I not understanding something about things happening in the virtual DOM / react in general?
per li357 comment, adding e.persist() right before the console.log(e) statement in original post shows this in the debugger. You can see the native javascript event's properties include target, element and the defined option from react
You can use e.nativeEvent to get access to the native event and from there on to the elements.option.value property:
// React Synthetic Events
class AddOption extends React.Component {
handleAddOption(e){
e.preventDefault();
console.log(e);
const option = e.target.elements.option.value; // < HOW WOULD I FIND THIS IN THE DEBUGGER?
// > YOU CAN USE e.nativeEvent TO GET ACCESS TO THE NATIVE EVENT:
console.log(e.nativeEvent);
console.log(e.nativeEvent.target.elements.option.value); // < HERE IS YOUR VALUE
if (option){
alert("something was entered in");
}
}
render() {
return (
<div>
<form onSubmit={this.handleAddOption}>
<input type="text" name="option" defaultValue={"Toy"}/>
<button>React Submit</button>
</form>
</div>
)
}
}
ReactDOM.render(<AddOption />, document.getElementById('root'));
I have a modal in React. When you click the background of the modal, the modal should close. The way I have it set up right now, if you click inside* the modal, it closes as well. Because the modal is inside the background>
handleClose(e) {
e.stopPropagation();
this.props.history.push('/business/dashboard')
}
render() {
return (
<Background onClick={e => this.handleClose(e)} name="BACKGROUND">
<Container onClick={console.log("IT CLICKED")} to={'/business/dashboard'} name="CONTAINER">
....
When I click on Container, the onClick event for Background gets called. I don't want this to happen. This is a form that users will be clicking on all the time. I need the modal to only close when you click outside the modal on Background.
I think it will work if you use stopPropagation on the Container click event instead of the Background. Just make sure that you use the onClick prop in your Container component.
class App extends React.Component {
handleClose = (e) => {
e.stopPropagation();
this.props.history.push("/business/dashboard");
};
render() {
return (
<Background onClick={this.handleClose} name="BACKGROUND">
<Container
onClick={e => e.stopPropagation()}
to={"/business/dashboard"}
name="CONTAINER"
/>
</Background>
);
}
}
EDIT: On rereading the question, the other answer is a simpler solution in this case.
The behavior you want to achieve is generally referred to as an "outside click" handler. There are a couple of decent libraries to handle this [0] and their source is pretty short and readable if you want to know how it works in detail. [1]
The general idea is to register a click event handler on the document in a HOC and check whether the event.target originates inside a React ref via Element.contains browser functionality. If is is, the handler will not be executed.
[0] https://github.com/tj/react-click-outside
[1] https://github.com/tj/react-click-outside/blob/master/index.js