I apologise in advance if this is a silly question. Although I have managed to get it to work, I would like to get a deeper understanding.
I am building a custom hamburger menu in react which closes whenever you click anywhere outside the unordered list or the hamburger icon itself.
I have seen answers here Detect click outside React component
And I have followed it but I couldn't understand why it wasn't working.
Firstly when it was just the hamburger icon and no click outside the menu to close option, it worked perfectly.
Then when I used the useRef hook to get a reference to the unordered list in order to only close the menu when the list is not clicked, it worked perfectly except for when I clicked the actual hamburger icon.
After a lot of amateur debugging I finally realised what was happening.
First when I opened the menu the state showMenu changed to true,
Then when I clicked the hamburger icon to close,
The parent wrapper element was firing first instead of the hamburger menu which is strange as during the bubbling phase I would expect the inner most element to fire first.
So the parent element would close the menu changing the state, causing the components to re-render. Then when the event would reach the actual icon the handleClick would once again toggle the state to true giving the impression that the hamburger click isn't working.
I managed to fix this by using event.stopPropogation() on the parent element.
But this seems very strange because I would not expect the parent element's click to fire first especially when Im using bubbling phase.
The only thing I can think of is because it is a native dom addeventlistener event it is firing first before the synthetic event.
Below is the code for the Mobile navigation which has the hamburger
The header component renders the normal Nav or the MobileNav based on screen width. I haven't put code for the higher order components to make it easier to go through, but I can provide all the code if needed:
//MobileNav.js
export default function MobileNav() {
const [showMenu, setShowMenu] = useState(false);
const ulRef = useRef();
console.log('State when Mobilenav renders: ', showMenu);
useEffect(() => {
let handleMenuClick = (event) => {
console.log('App Clicked!');
if(ulRef.current && !ulRef.current.contains(event.target)){
setShowMenu(false);
event.stopPropagation();
}
}
document.querySelector('#App').addEventListener('click', handleMenuClick);
return () => {
document.querySelector('#App').removeEventListener('click', handleMenuClick);
}
}, [])
return (
<StyledMobileNav>
<PersonOutlineIcon />
<MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />
{
(showMenu) &&
<ul ref={ulRef} style={{
backgroundColor: 'green',
opacity: '0.7',
position: 'absolute',
top: 0,
right: 0,
padding: '4em 1em 1em 1em',
}}
>
<MenuList/>
</ul>
}
</StyledMobileNav>
)
}
//MenuIcon.js
/**
* By putting the normal span instead of the MenuLine component after > worked in order to hover all div's
*/
const MenuWrap = styled.div`
width: 28px;
position: relative;
transform: ${(props) => props.showMenu ? `rotate(-180deg)` : `none` };
transition: transform 0.2s ease;
z-index: 2;
&:hover > div{
background-color: white;
}
`;
const MenuLine = styled.div`
width: 100%;
height: 2px;
position: relative;
transition: transform 0.2s ease;
background-color: ${(props) => props.showMenu ? 'white' : mainBlue};
&:hover {
background-color: white;
}
`;
const TopLine = styled(MenuLine)`
${(props) => {
let style = `margin-bottom: 7px;`;
if(props.showMenu){
style += `top: 9px; transform: rotate(45deg);`;
}
return style;
}}
`;
const MidLine = styled(MenuLine)`
${(props) => {
let style = `margin-bottom: 7px;`;
if(props.showMenu){
style += `opacity: 0;`;
}
return style;
}}
`;
const BottomLine = styled(MenuLine)`
${props => {
if(props.showMenu){
return `bottom: 9px; transform: rotate(-45deg);`;
}
}}
`;
export default function MenuIcon({showMenu, setShowMenu}) {
const handleMenuClick = (event) => {
console.log('Menu Clicked!');
console.log('State before change Icon: ', showMenu);
setShowMenu(!showMenu);
}
return (
<MenuWrap onClick={handleMenuClick} showMenu={showMenu}>
<TopLine onClick={handleMenuClick} showMenu={showMenu}></TopLine>
<MidLine onClick={handleMenuClick} showMenu={showMenu}></MidLine>
<BottomLine onClick={handleMenuClick} showMenu={showMenu}></BottomLine>
</MenuWrap>
)
}
Reading this article https://dev.to/eladtzemach/event-capturing-and-bubbling-in-react-2ffg basically it states that events in react work basically the same way as DOM events
But for some reason event bubbling is not working properly
See screenshots below which show how the state changes:
Can anyone explain why this happens or what is going wrong?
This is a common issue with competing event listeners. It seems you've worked out that the problem is that the click out to close handling and the menu button click to close handling are both triggered at the same time and cancel each other out.
Event listeners should be called in the order in which they are attached according to the DOM3 spec, however older browsers may not implement this spec (see this question: The Order of Multiple Event Listeners). In your case the click out listener (in the <MobileNav> component) is attached first (since you use addEventListener there, while the child uses the React onClick prop).
Rather than relying on the order in which event listeners are added (which can get tricky), you should update your code so that either the triggers do not happen at the same time (which is the approach this answer outlines) or so that the logic within the handlers do not overlap.
Solution:
If you move the ref'd element up a level so that it contains both the menu button and the menu itself you can avoid the overlapping/competing events.
This way the menu button is within the space where clicks are ignored so the outer click listener (the click out listener) won't be triggered when the menu button is clicked, but will be if the user clicks anywhere outside the menu or its button.
For example:
return (
<StyledMobileNav>
<PersonOutlineIcon />
<div ref={menuRef}>
<MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />
{ showMenu && (
<ul>
<MenuList/>
</ul>
)}
</div>
</StyledMobileNav>
)
Then use menuRef as the one to check for clicks outside of.
As an additional suggestion, try putting all the menu logic into a single component for better organization, for example:
function Menu() {
const [showMenu, setShowMenu] = React.useState(false);
// click out handling here
return (
<div ref={menuRef}>
<MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />
{ showMenu && (
<ul>
<MenuList/>
</ul>
)}
</div>
)
}
Related
I'd like to detect a click inside or outside a div area. The tricky part is that the div will contain other elements and if one of the elements inside the div is clicked, it should be considered a click inside, the same way if an element from outside the div is clicked, it should be considered an outside click.
I've been researching a lot but all I could find were examples in jquery and I need pure javascript.
Any suggestion will be appreciated.
It depends on the individual use case but it sounds like in this example there are likely to be other nested elements inside the main div e.g. more divs, lists etc. Using Node.contains would be a useful way to check whether the target element is within the div that is being checked.
window.addEventListener('click', function(e){
if (document.getElementById('clickbox').contains(e.target)){
// Clicked in box
} else{
// Clicked outside the box
}
});
An example that has a nested list inside is here.
You can check if the clicked Element is the div you want to check or not:
document.getElementById('outer-container').onclick = function(e) {
if(e.target != document.getElementById('content-area')) {
console.log('You clicked outside');
} else {
console.log('You clicked inside');
}
}
Referring to Here.
you can apply if check for that inside your click event
if(event.target.parentElement.id == 'yourID')
In Angular 6 and IONIC 3, I do same as here:
import {Component} from 'angular/core';
#Component({
selector: 'my-app',
template: `
<ion-content padding (click)="onClick($event)">
<div id="warning-container">
</div>
</ion-content>
`
})
export class AppComponent {
onClick(event) {
var target = event.target || event.srcElement || event.currentTarget;
if (document.getElementById('warning-container').contains(target)){
// Clicked in box
} else{
// Clicked outside the box
}
}
}
This working fine on web/android/ios.
It might be helpful for someone, Thanks.
Try this solution it uses pure javascript and it solves your problem. I added css just for better overview... but it is not needed.
document.getElementById('outer-div').addEventListener('click', function(){
alert('clicked outer div...');
});
document.getElementById('inner-div').addEventListener('click', function(e){
e.stopPropagation()
alert('clicked inner div...');
});
#outer-div{
width: 200px;
height: 200px;
position: relative;
background: black;
}
#inner-div{
top: 50px;
left: 50px;
width: 100px;
height: 100px;
position: absolute;
background: red;
}
<div id="outer-div">
<div id="inner-div">
</div>
</div>
I came up with a hack for this that's working well for me and that might help others.
When I pop up my dialog DIV, I simultaneously display another transparent DIV just behind it, covering the whole screen.
This invisible background DIV closes the dialog DIV onClick.
This is pretty straightforward, so I'm not going to bother with the code here. LMK in the comments if you want to see it and I'll add it in.
HTH!
closePopover () {
var windowBody = window
var popover = document.getElementById('popover-wrapper') as HTMLDivElement;
windowBody?.addEventListener('click', function(event){
if(popover === event.target) {
console.log("clicked on the div")
}
if(popover !== event.target) {
console.log("clicked outside the div")
}
})
}
}
I recently needed a simple vanilla JS solution which solves for:
Ignoring specific selectors including whether a parent contains one of these selectors
Ignoring specific DOM nodes
This solution has worked quite well in my app.
const isClickedOutsideElement = ({ clickEvent, elToCheckOutside, ignoreElems = [], ignoreSelectors = [] }) => {
const clickedEl = clickEvent.srcElement;
const didClickOnIgnoredEl = ignoreElems.filter(el => el).some(element => element.contains(clickedEl) || element.isEqualNode(clickedEl));
const didClickOnIgnoredSelector = ignoreSelectors.length ? ignoreSelectors.map(selector => clickedEl.closest(selector)).reduce((curr, accumulator) => curr && accumulator, true) : false;
if (
isDOMElement(elToCheckOutside) &&
!elToCheckOutside.contains(clickedEl) &&
!didClickOnIgnoredEl &&
!didClickOnIgnoredSelector
){
return true;
}
return false;
}
const isDOMElement = (element) => {
return element instanceof Element || element instanceof HTMLDocument;
}
In React you can use useClickOutside hook from react-cool-onclickoutside.
Demo from Github:
import { useClickOutside } from 'use-events';
const Example = () => {
const ref1 = React.useRef(null);
const ref2 = React.useRef(null);
const [isActive] = useClickOutside([ref1, ref2], event => console.log(event));
return (
<div>
<div ref={ref1} style={{ border: '1px dotted black' }}>
You are {isActive ? 'clicking' : 'not clicking'} outside of this div
</div>
<br />
<div ref={ref2} style={{ border: '1px dotted black' }}>
You are {isActive ? 'clicking' : 'not clicking'} outside of this div
</div>
</div>
);
};
Live demo
Hello using a table kind of like this
https://jsfiddle.net/vw19pbfo/24/
how could i make a trigger onClick that removes borders on first click and on second click add them back but that should only happen on the row that is being clicked on and not affect the other. I have tried to have a conditional css on the first and last <td> but that affected every border but i only want to affect the clicked one
function removeBorders(e){
var target = e.target || e.srcElement;
target.parentElement.classList.toggle('without-border');
};
Working JSFiddle: https://jsfiddle.net/andrewincontact/su86fhxo/9/
Changes:
1) to css:
.my-table-row.without-border td {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
2) to html:
onclick=removeBorders(event) instead onClick=this.removeBorders()
One way would be to check the <td> element's parent and add/remove a custom class, like so:
function removeBorders(e) {
var row = e.parentElement;
if (row.className.indexOf("has-borders") === -1) {
row.classList.add("has-borders");
} else {
row.classList.remove("has-borders");
}
};
Working JSFiddle: https://jsfiddle.net/d380sjrh/
I also changed onClick=this.removeBorders() to onclick="removeBorders(this);".
JSFiddle: https://jsfiddle.net/4qdstec7/
Use React's state to set and unset the selected class.
const Row = ({ children }) => {
const [selected, setSelected] = useState(false);
const onClick = e => setSelected(!selected);
return (
<tr
className={selected && 'selected'}
onClick={onClick}
>
{children}
</tr>
)
}
Developing in React requires a shift in thinking from traditional web development. Please take some time to read this excellent post from the React team.
I am working with React. I want to know about the easiest way on how to to blur parent component when child page (popup page) is opened.
Here is my Fiddle Workspace Demo
Even if I blur it the parent should still be clickable
Can anyone show how to achieve this functionality?
We can use CSS to blur the parent component. Something like this:
We will wrap the child component into a div with the class name as overlay
Apply CSS effect to blur the parent component
[Note: You can also add the click event to the blur areas by adding the event to the overlay div]
{this.state.childOpen && (
<div className="overlay">
<div className="overlay-opacity" />
<Child data={data} applyFilter={this.applyFilter} />
</div>
)}
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.overlay-opacity {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: black;
opacity: 0.5;
}
Here is the live demo
To hide the child component onclick of the blur area, we can add a event like this
hideChild() {
this.setState({
childOpen: false
});
}
Here is the live demo
Hope it helps :)
Since you are working on react, I would like to suggest you to use react-responsive-modal library. You can install this using npm.
It is easily customizable via props.
Please find the example below:
https://codesandbox.io/s/9jxp669j2o
What i understood is that you want to blur the table when child is opened
Here is code you need to add
formatStyle = () => {
if (this.state.childOpen == true) return { filter: "blur(5px)" };
else return {};
};
//add formatStyle in main app
styleData={this.formatStyle()} // in props on table
style = {this.props.styleData} //add this attribute in main container of table component
styleData={this.formatStyle()} // Pass this in ReactTable in main app
The feature you are looking for is already in Semantic-UI-React Modal. This is very easy to use, you only have to pass some props to get above result.
demo https://react.semantic-ui.com/modules/modal/#variations-dimmer
From the docs of react portal:
A typical use case for portals is when a parent component has an overflow: hidden or z-index style, but you need the child to visually “break out” of its container. For example, dialogs, hovercards, and tooltips.
The suggested solution is to use:
// this line is rendered in `Portal1` component,
// which is rendered in `Parent1` component.
ReactDOM.createPortal(Child1, Container1)
I don't understand what does it solves. Why making Child1 child of Container1 instead of Parent1 helps?
My question maybe not clear so if it doesn't -> How does this solution differ from other solutions for creating "dialogs, hovercards, and tooltips"?
When you initialise a React application, ReactDOM tells one DOM container that all its React components will be rendered under this DOM. This makes React do all rendering processing.
Sometimes you need to control a React Component to render as a child to a different DOM element, and continue to interact with your React application. This is why we use React Portals
As React creates virtual elements under the hood, you cannot convert the into DOM elements and insert them directly into the DOM. React Portals allows to you pass a React Elements and specify the container DOM for the React Element
Here is an example:
You have a Modal component which renders a div element in the center.
function Modal() {
return (
<div style={{ position: 'absolute', left: '50%'}}>
Message
</div>
);
}
One puts your Modal component inside a div of relative position.
<div style={{ position: 'relative', left: 100 }}>
<Modal />
</div>
The problem is when Modal component is rendered, its position is relative to parent div's position but you need to show it at the centre of window.
In order to solve this problem, you can append your Modal component directly to the body element with a portal
Here is the solution with Portals.
function ModalRenderer() {
return (
React.createPortal(
<Modal />,
document.body
)
);
}
And use ModalRenderer component anywhere inside your application.
<div style={{ position: 'relative', left: 100 }}>
<ModalRenderer />
</div>
ModalRenderer has the container element for the Modal which is outside of the DOM tree, but still within the React Application tree
In React V15,we can only add children dom into the father dom.That means, if you want to have an element, you have to create a new div.Like this:
<div>
{this.props.children}
</div>
In React v16,don't need to create a new div.We can use portal to add the children element to any dom in the dom tree.
ReactDOM.createPortal(
this.props.children,
domNode
);
overflow: hidden or z-index style
If a parent component has an overflow: hidden or z-index style, and the children element type is dialogs, hovercards, tooltips and so on,these should be on the upper layer of the father element, meaning break out.But they maybe shade by the father component.
So createPortal offers a better option.It can load on the upper component of the father component.After mounting the element to another dom,it won't be sheltered.
Event and bubble up
Even the component mounted on another component, event can budde up to the father component.
One good case is to separate CSS concerns.
Here is an example:
HTML
<div id="app-root"></div>
<div id="modal-root"></div>
CSS
.app {
position: fixed;
height: 100%;
width: 100%;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.modal {
background-color: rgba(0,0,0,0.5);
}
Babel
const appRoot = document.getElementById('app-root')
const modalRoot = document.getElementById('modal-root')
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
showModal: false
}
this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
}
handleShow() {
this.setState({
showModal: true
})
}
handleHide() {
this.setState({
showModal: false
})
}
render() {
const modal = this.state.showModal ? (
<Modal>
<div className="modal">I am no longer centered!</div>
</Modal>
) : null
return (
<div className='app'>
Basic
<button onClick={this.handleShow}>
Show Modal
</button>
{modal}
</div>
)
}
}
class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount(){
modalRoot.appendChild(this.el)
}
componentWillUnmount() {
modalRoot.removeChild(this.el)
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.el
)
}
}
ReactDOM.render(<App />, appRoot)
Observations:
1. The main text has fixed position and is centered
2. When we click the button, the text popped up. Notice the text is no longer centered!
3. If we had used it to something like this:
<div className='app'>
Basic
<button onClick={this.handleShow}>
Show Modal
</button>
<div className="modal">I am centered ;(</div>
</div>
Notice the text is centered. Modal works by going to modal-root and attach that DOM element there. You can have your own CSS under modal-root hood, separate from parent component (app-root).
You are now no longer obligated to attach your "child" components under your parent's. In this case, you (app-root) are attaching it to its sibling (modal-root). You can totally attach it to document.body, or whatever element you wanted to. Another perk is, like other user mentioned, event bubbling happens as if that child component is their own child.
A Parent component in #app-root would be able to catch an uncaught, bubbling event from the sibling node #modal-root.
Source
I use react and react-modal to create an overlay over a website. This overlay contains various elements and also a form (overview below). I want to be able to guide the user through the form using TAB keys. I assigned tabindex=0 to the required elements to be tabbable in order of appearance.
My problem is: It does not work in Chrome (Version 61.0.3163.100) while it does work in Firefox. I read that this happens if any element up the DOM-tree is invisible or has height/width of 0. I made some styling changes to fix that but with no effect.
<div class="ReactModalPortal">
<div data-reactroot="" class="ReactModal__Overlay" style="position: fixed; top: 0px; left: 0px; right: 0px; bottom: 0px;">
<div class="ReactModal__Content" tabindex="-1" aria-label="Questionnaire" style="position: absolute; top: 0px; left: 0px; right: 0px; height: 100%; background: transparent none repeat scroll 0% 0%; overflow: auto;">
<!-- Some other stuff and nested elements -->
<div id="...">
<form>
<input tabindex="0">
<button tabindex="0">
</form>
</div>
</div>
</div>
As you can see one of the parent elements has tabindex="-1". When changing it through the inspect function in Chrome or programmatically with JS the problem still persists (or is it a difference if the element was rendered with this index initially?).
Update
I realized that something else was causing the issues. I was using the CSS attribute initial: all on the root node of my modal to fence my inner CSS from everything outside. For some reason this was preventing the tabindex from working. If you can help me understanding I will reward this wis the bounty. My workaround is just not using all: initial (it is not IE-compatible anyways but also there is no real good alternative I am aware of).
all: initial resets all CSS properties of the node with initial properties.
For display property, the initial value would be inline.
So, setting all: initial to the root div would set the display property to inline. An inline element does not have height or width, so these are 0x0.
This is also because the div contains only fixed, absolutely positioned elements.
React Modal checks if elements are focusable by running a loop through all the elements inside the modal. However, for an element to be focusable, it has to visible. For each element, we have to iterate till the body element to ensure it's visibility.
Here is the function that checks whether the element is visible.
function hidden(el) {
return (
(el.offsetWidth <= 0 && el.offsetHeight <= 0) || el.style.display === "none"
);
}
As you can see, our div would have no offsetHeight or offsetWidth and would be deemed as hidden. Therefore, the modal cannot not be focused.
I had the same issue and was not able to get other solutions working quicky, so I came up with brute force approach. Make a ref to the container element that holds the focusable elements that you wish to make tabbable.
const formRef = useRef();
<ReactModalTabbing containerRef={formRef}>
<form ref={formRef} onSubmit={handleSubmit} >
<input type="text" />
<input type="text" />
<input type="text" />
<input type="text" />
</form>
</ReactModalTabbing>
And this is the component
import React, { useState, useEffect } from 'react';
const ReactModalTabbing = ({ containerRef, children }) => {
const [configuredTabIndexes, setConfiguredTabIndexes] = useState(false);
const focusableElements = () => {
// found this method body here.
//https://zellwk.com/blog/keyboard-focusable-elements/
return [...containerRef?.current?.querySelectorAll(
'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]):not([type="hidden"]):not([disabled])'
)];
}
const isTabbable = (element) =>{
if(element.getAttribute('tabindex')){
return true;
}
return false;
}
const findElementByTabIndex = (tabIndex) => {
return containerRef?.current?.querySelector(`[tabindex="${tabIndex}"]`);
}
const moveFocusToTabIndex = (tabIndex) => {
findElementByTabIndex(tabIndex)?.focus();
}
const handleKeyDownEvent = (event) => {
if(!isTabbable(event.target)){
return;
}
const tabIndex = parseInt(event.target.getAttribute('tabindex'));
if(event.shiftKey && event.key === 'Tab'){
moveFocusToTabIndex(tabIndex - 1);
}else if(event.key === 'Tab'){ //should probably make sure there is no other modifier key pressed.
moveFocusToTabIndex(tabIndex + 1);
}
}
useEffect(() => {
if(!configuredTabIndexes && containerRef.current){
setConfiguredTabIndexes(true);
focusableElements().forEach((el, index) => el.setAttribute('tabindex', index + 1));
containerRef?.current?.addEventListener('keydown', handleKeyDownEvent);
}
});
return children;
}
export default ReactModalTabbing;