Video demonstrating issue
I have a bunch of clickable components that, when clicked, adds a "card" to a row. On desktop, it works fine, but on mobile (tested on iPhone, does not seem to be an issue for Android tablet), it requires 2 consecutive taps of the same button to fire the onClick function.
These components also have onMouseEnter/onMouseLeave effects on them, to control a global state, which in turn decides if several components should have additional CSS applied (so I can't make it a simple CSS hover effect).
I believe that the mouse effects are interfering with the click event, but I have no idea how I could fix that. Here is the relevant code for this component:
const CardSource = ({ addCard, note, setHoveredNote, hoveredNote }) => {
return (
<Source
onClick={() => addCard(note)}
onMouseEnter={() => setHoveredNote(note)}
onMouseLeave={() => setHoveredNote(null)}
className={
hoveredNote && hoveredNote.index === note.index ? "highlight" : null
}
>
{note.letters[0]}
</Source>
);
};
Furthermore, once a button has been tapped twice, the hover effect CSS "sticks" to that button, and never moves to another button. This seems to happen on both iPhone and Android tablet. I would love to have this not happen anymore either.
I've created a working demonstration of this issue in a sandbox, which if viewed on mobile you should be able to recreate these issues: https://codesandbox.io/s/mobile-requires-2-taps-i9zri?file=/src/Components/CardSource/CardSource.js
Probably the problem with your code is, the mouse events you're using are non-bubbling. e.g. mouseenter event.
You might want to try with an event bubbling solution using onMouseOver instead of onMouseEnter, and onMouseOut instead of onMouseLeave.
const CardSource = ({ addCard, note, setHoveredNote, hoveredNote }) => {
return (
<Source
onClick={() => addCard(note)}
onMouseOver={() => setHoveredNote(note)}
onMouseOut={() => setHoveredNote(null)}
className={
hoveredNote && hoveredNote.index === note.index ? "highlight" : null
}
>
{note.letters[0]}
</Source>
);
};
Should the above NOT work, you could debug this with event type and performing event handling based on it. e.g.
const CardSource = ({ addCard, note, setHoveredNote, hoveredNote }) => {
const eventHandler = (event) => {
const { type, bubbles } = event;
switch(type) {
case "mouseover":
case "mouseenter":
setHoveredNote(note);
break;
case "mouseout":
case "mouseleave":
setHoveredNote(null);
case "click":
addCard(note);
if (bubbles) { // handle hover state
setHoveredNote(note);
}
break;
default:
break;
}
}
const onClick = (event) => eventHandler(event);
const onMouseOver = (event) => eventHandler(event);
const onMouseOut = (event) => eventHandler(event);
return (
<Source
onClick={onClick}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
className={
hoveredNote && hoveredNote.index === note.index ? "highlight" : null
}
>
{note.letters[0]}
</Source>
);
};
Also note that, providing arrow functions as props creates new instance of the function on every render. So better use bind in that case or just function references that capture the arguments.
I think I've found the problem when I use an onClick & onMouseEnter & onMouseLeave then test in the browser in mobile mode the onMouseEnter and onClick event fire with the first onClick, you can add a console log to all your events and see the same behavior. The CSS style is staying because the DOM thinks that your element still has the hover attribute. If you click off of the element in question, you will see your onMouseLeave event fire, your css will reset but the element will require two clicks again. I'm not sure what the solution is, or if its even a problem testing on an actual mobile device.
EDIT: A solution I found, is only using the onMouseEnter & onMouseLeave event, since this event fires onClick for mobile and I only want the hover effect on desktop the outcome is what I was after.
EDIT EDIT: To maintain accessibility with the keyboard I added an onKeyDown event to open/close the dropdown button (which is what I was working on)
onKeyDown={(event) => {
if (event.keyCode == 13) {
setShowChildren(showChildren === "hide" ? "show" : "hide");
}
}}
You could just use css hover rather than adding a class via onMouseEnter event, It fixes the two taps issue.
Link to sandbox
If you were to programatically use trigger for hover. You could solve the two clicks issue by using onTouchEnd event (commented in sandbox).
Hope that helps.
I remember having a similar problem. The issue was for me that the state of component does not change immediately, but only upon execution of the render() method. I believe you might have the same issue with asynchronicity for both effects you are describing.
The only render() call I see found in your code is in App.test.js, I usually place that in the respective component.tsx.
References
Submit button takes 2 clicks to call function in React
React.js events need 2 clicks to execute
Use events onTouchStart, onTouchMove, onTouchEnd for calculate touch.
Related
I have been doing research on this for days and finally have decided to ask this on here. I am using react and material-ui's ClickAwayListener component. The idea is that I have a button, which toggles the this.state.showGridPopup to the opposite of whatever it is currently is. So if the user was to click it once, the grid popup should show, click it again, the popup should disappear. There is an handleShowGrid handler attached to the button that does this:
handleShowGrid = (event) => {
const { widgetButtonEl } = this.state;
const element = widgetButtonEl === null ? event.currentTarget : null;
console.log('In handleShowGrid!!!!!');
this.setState({
showWidget: !this.state.showWidget,
widgetButtonEl: element
});
}
All this works well. And toggles the popup to show when we click on the button attached to the handler.
<ButtonBase onClick={this.handleShowGrid}>Open Swap</ButtonBase>
The issue is when I add Material-UI's ClickAwayListener. The reason for adding this is to close the grid when a user clicks outside the grid. Here is the ClickAwayListener below:
<ClickAwayListener onClickAway={this.handleCloseWidget}>
<SurveyGrid />
</ClickAwayListener>
And the handleCloseWidget handler:
handleCloseGrid = (e) => {
console.log('In handleCloseWidget!!!!!');
this.setState({
showWidget: !this.state.showWidget,
widgetButtonEl: null
});
}
So now clicking outside grid is fine and closes the Grid. However, now, clicking on the button that should toggle the Grid being show (this.state.showWidget), causes the handleCloseGrid to fire. And then afterwards the handleShowGrid would fire. Is there anyway to not have the handleCloseGrid handler to fire? It seems like an issue with propagation and event bubbling. Have tried different things like e.stopPropagation() but to no avail.
I'm trying to build a funky use case:
I have a HeadlessUI dialog, inside this dialog, there is a HeadlessUI Menu component as a nested child, I want to trigger the menu via a keyboard shortcut
I have managed to trigger the dialog, via:
// I took the useWindowEvent from the headlessUI code
useWindowEvent("keydown", (event) => {
if (event.key === "p") {
event.preventDefault()
event.stopPropagation()
selectorRef.current?.click()
}
})
and on my Menu component:
// My react component forwardRefs to the trigger button
export const ProjectSelector = forwardRef(
... some other component code
<Menu.Button ref={ref} as="div">
... other code
This works fine to trigger the Menu, but the menu also has a search bar, so whenever I press the p key, the listener triggers again and programmatically closes the Menu
I have taken a look inside the headlessUI code trying to understand the focus trap code, but it's above my head, is there any way to detect if the parent Dialog has focus? that way I can just ignore the keypress if focus is on the menu :)
Workaround:
While writing the question I stumbled upon a workaround... it is not super pretty, so maybe someone can come up with a better solution:
// I'm pretty sure I'm doing everything wrong in here
// but I couldn't find another way to remotely trigger and keep focus on the menu component
useWindowEvent("keydown", (event) => {
if (
event.key === "p" &&
document.activeElement?.getAttribute("role") !== "menu" &&
document.activeElement?.tagName !== "INPUT"
) {
event.preventDefault()
event.stopPropagation()
selectorRef.current?.click()
}
})
I want to get whether the current mouse is pressed, such as the right button.
I know that you can use mousedown to maintain a variable to determine whether it has been pressed.
But when mouseup on a disabled element, there is no way to set state to up, which leads to subsequent state errors.
Is there an api to help me get the current mouse state?
example
<button disabled>disabled</button>
let state = 'up'
document.addEventListener('mousedown', () => {
state = 'down'
console.log('mouseDown')
})
document.addEventListener('mouseup', () => {
state = 'up'
console.log('mouseup')
})
document.addEventListener('mousemove', () => {
console.log('mousemove',state)
})
according to the the HTML Spec
A form control that is disabled must prevent any click events that are queued on the user interaction task source from being dispatched on the element.
So you can hack the behaviour using Capturing pointer events
give your button an id ex: "mybutton"
button=document.getElementById('mybutton')
button.addEventListener('click',()=>{
console.log('click')
})
button.addEventListener('pointerdown', event => {
console.log('down')
});
button.addEventListener('pointerup', event => {
console.log('up')
});
OR remove the disabled attribute and use css pointer-events: none this will prevent the button from responding and also you will still be able to get the mouse events
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
I want to know when the user starts the navigation to a new page by clicking in a link located inside my Flickity slider. I have attached the jQuery click event on the links, but when the user slides and click at the same time, the click event on the <a> is triggered but the navigation to the link adress does not occur.
Demo : http://codepen.io/anon/pen/GoapaY
. To reproduce the issue : click down on the link, then slide, then release your click : the event is triggered but the navigation to example.com have not occured.
Which event/trick can I use to know when the user actually navigate to the link adress ?
Answer obtained with this issue opened on Flickity's GitHub :
This is the intended behavior. This allows users to slide the gallery using any element on the page, links, buttons, etc. It lets click events propagate. There's additional logic so that static clicks do trigger a click on the element, and allow links to go through if no sliding occurred.
Flickity's staticClick event might be what you're looking for.
This solves the issue for me:
$el.on('dragStart.flickity', () => $el.find('.slide').css('pointer-events', 'none'));
$el.on('dragEnd.flickity', () => $el.find('.slide').css('pointer-events', 'all'));
I just disable pointer events on dragStart and reinstate them on dragEnd.
Ali Klein's solution worked for me.
I'm not using jQuery so here is the code I'm using
const carousel = document.querySelector('.carousel')
const flkty = new Flickity(carousel, {
// ...options
on: {
'dragStart': () => {
carousel.style.pointerEvents = 'none'
},
'dragEnd': () => {
carousel.style.pointerEvents = 'all'
}
}
})