Stop click on child calling method on parent - React/JavaScript - javascript

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

Related

Material UI's ClickAwayListener firing right away, which causes state to toggle twice

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.

HeadlessUI React, how to detect currently focused element?

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()
}
})

How can I focus a button on click in ReactJS

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.

How to only activate the onclick event of the top level button when having nested buttons? React.js

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>
)
}

React: onClick on Mobile (iPhone) requires 2 taps?

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.

Categories