Detecting clicks outside a div and toggle menu - javascript

I have a toggle menu and I'm trying detect the click events outside of a menu to be able to close the menu, I can close the menu when user clicks outside of the menu, however to open the menu again you would have to click on it twice, does anyone know what I have to do to fix that, (the menu should open with one click)
const RightMenu = ({ t, history }) => {
let [menuOpen, setMenuOpen] = useState(false);
const menuDiv = useRef({});
const toggleMenu = useRef();
useEffect(() => {
window.addEventListener("click", () => {
if ((menuDiv.current.style.display = "block")) {
menuDiv.current.style.display = "none";
}
});
return () => {
window.removeEventListener("click", () => {});
};
}, []);
const handleClick = e => {
e.stopPropagation();
if (menuOpen === false) {
menuDiv.current.style.display = "block";
setMenuOpen(true);
}
if (menuOpen === true) {
menuDiv.current.style.display = "none";
setMenuOpen(false);
}
};
return (
<div>
<div
id="menu"
ref={menuDiv}
style={{
display: "none"
}}
>Menu items</div>
<div
className="text-center"
ref={toggleMenu}
onClick={e => handleClick(e)}
> Menu Button</div>
)
}

const RightMenu = ({ t, history }) => {
let [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
window.addEventListener("click", () => {
setMenuOpen(prevState => {
return !prevState
})
});
return () => {
window.removeEventListener("click", () => {});
};
}, []);
const handleClick = () => {
e.stopPropagation();
setMenuOpen(!menuOpen);
};
return (
<div>
{menuOpen && (<div
id="menu"
>Menu items</div>)}
<div
className="text-center"
onClick={handleClick}
> Menu Button</div>
)
You do not need refs to achieve this, u can conditionally render the menu based on the menuOpen state like in the example provided.

You are not actually removing the event listener from window when your component unmounts. The second argument to the removeEventListener should be a reference to the same function you added with addRemoveListener. E.g.
useEffect(() => {
const closeMenu = () => {
if ((menuDiv.current.style.display = "block")) {
menuDiv.current.style.display = "none";
}
};
window.addEventListener("click", closeMenu);
return () => {
window.removeEventListener("click", closeMenu);
};
}, []);

#Watch('openDropdown')
openHandler(newValue) {
newValue ? this.setOpen() : this.setClose();
}
componentWillLoad() {
document.addEventListener('click', this.handleClick, false)
}
componentDidUpdate() {
//
if (this.dropdownNode != null && this.collapsableIconNode != null) {
this.dropdownNode.style.top = this.collapsableIconNode.offsetTop + 20 + 'px'
this.dropdownNode.style.left = this.collapsableIconNode.offsetLeft - 11 + 'px'
}
}
componentDidUnload() {
document.removeEventListener('click', this.handleClick, true)
}
handleClick = (e) => {
if (this.collapsableIconNode.contains(e.target)) {
this.openDropdown = true;
}
else {
this.handleClickOutside()
}
}
handleClickOutside() {
this.openDropdown = false;
}
<span ref={collapsableIconNode => this.collapsableIconNode = collapsableIconNode as HTMLSpanElement} id="collapseIcon" class="collapsable-icon" onClick={() => this.setOpen()}>
This is Written in StencilJS, Logic is same,It is similar to React!!

I was dealing with the same problem recently. I ended up using this lib - https://github.com/Andarist/use-onclickoutside
Worked perfectly for me. Minimal effort. It covers all the edge cases.
Maybe you should give it a try.

Related

React setting state to child component causing infinite loop

I have a page with some dropdown menu's used to search some content, the dropdown is a non-functional component. The page is a listsing page. Not important but gives some context.
I do some calculation on the listing page and update the state, then I pass this state into the Dropdown component. However, I'm getting an infinite loop and I'm not sure how to stop it or where I'm going wrong.
my listing page is here:
constructor(props){
super(props)
let industryList = this.createList(this.props.data.mainYaml.caseStudiesDropdowns[0].items)
let areaList = this.createList(this.props.data.mainYaml.caseStudiesDropdowns[1].items)
let techniqueList = this.createList(this.props.data.mainYaml.caseStudiesDropdowns[2].items)
this.state = {
industry: "All Industries",
area: "All Areas",
technique: [],
industries: industryList,
areas: areaList,
techniques: techniqueList
}
}
createList = (listItems) => {
let listArr = []
listItems.forEach((item) => {
let obj = {
name: item,
disabled: false
}
listArr.push(obj)
})
return listArr
}
filterCaseStudies = (caseStudies) => {
const filterIndustry = (caseStudies) => {
if (this.state.industry == "All Industries") {
return caseStudies
} else {
return caseStudies.filter((study) => study.node.industry == this.state.industry)
}
}
const filterArea = (caseStudies) => {
if (this.state.area == "All Areas") {
return caseStudies
} else {
return caseStudies.filter((study) => study.node.area == this.state.area)
}
}
const filterTechnique = (caseStudies) => {
if (this.state.technique.length === 0) {
return caseStudies
} else {
let matchedStudies = []
caseStudies.forEach((study) => {
let count = 0;
let techCount = study.node.technique.length - 1;
study.node.technique.forEach((item, i) => {
this.state.technique.forEach((selectedItems) => {
if (selectedItems == item) {
count++;
return
}
})
if (i == techCount && count > 0) {
study.node.count = count
matchedStudies.push(study)
}
})
})
matchedStudies.sort((a, b) => b.node.count - a.node.count);
return matchedStudies;
}
}
let industryMatches = filterIndustry(caseStudies)
let areaMatches = filterArea(industryMatches)
this.filterDropdowns(areaMatches)
let techniqueMatches = filterTechnique(areaMatches)
return techniqueMatches;
}
filterDropdowns = (filteredCaseStudies) => {
console.log(filteredCaseStudies)
let disabledIndustries = [];
let disabledAreas = [];
let disabledTechniques = [];
this.state.industries.forEach((industry) => {
let obj = {
name: industry.name
}
if (industry.name == "All Industries") {
console.log(industry.name)
obj.disabled = false;
disabledIndustries.push(obj);
} else {
obj.disabled = true;
filteredCaseStudies.forEach((study) => {
if (study.node.industry == industry.name) {
obj.disabled = false;
}
})
disabledIndustries.push(obj);
}
})
console.log(disabledIndustries)
this.setState({industries: disabledIndustries})
}
getCaseStudies = (caseStudies) => {
let filteredCaseStudies = this.filterCaseStudies(caseStudies)
if (filteredCaseStudies.length > 0) {
return filteredCaseStudies.map((study, i) => {
return (
<div key={i} className="col-lg-4 col-md-6 col-12 px-4 mb-5">
<CaseStudyListItem
data={study.node}
className="CaseStudyListItem--lg"
index={i}/>
</div>
)
})
} else {
return (
<div className="col-12 px-4 mb-5">
<h4>We're Sorry!</h4>
<p>We can't seem to find any case studies that match your search. Please try other search terms.</p>
</div>
)
}
}
dropdownChange = (selected, name) => {
this.setState({[name]: selected})
}
render () {
console.log(this.state)
return (
<Layout bodyClass="k-reverse-header">
<div className="CaseStudies">
<section className="CaseStudies__header k-bg--grey">
<div className="container-fluid">
<div className="d-flex k-row">
<div className="col-12 px-4">
<DropdownSelect className="CaseStudies__search-industry mb-4" data={this.props.data.mainYaml.caseStudiesDropdowns[0]} list={this.state.industries} selected={this.dropdownChange} />
<DropdownSelect className="CaseStudies__search-area mb-4" data={this.props.data.mainYaml.caseStudiesDropdowns[1]} list={this.state.areas} selected={this.dropdownChange} />
</div>
</div>
</div>
</section>
<section className="CaseStudies__list">
<div className="container-fluid">
<div className="d-flex flex-wrap k-row">
{this.getCaseStudies(this.props.data.allCaseStudiesYaml.edges)}
</div>
</div>
</section>
</div>
</Layout>
)
}
}
I believe the issue happens as I pass the state into the Dropdown component, it is also updated in the filterDropdowns function. The Dropdown component code is as follows.
const DropdownSelect = ({ data, className, list, selected}) => {
const [isActive, setActive] = useState(false);
const [activeItem, changeActiveItem] = useState(data.placeholder);
const ref = useRef();
useEffect(() => {
const checkIfClickedOutside = (e) => {
// If the menu is open and the clicked target is not within the menu,
// then close the menu
if (isActive && ref.current && !ref.current.contains(e.target)) {
setActive(false)
}
}
document.addEventListener("mousedown", checkIfClickedOutside)
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", checkIfClickedOutside)
}
}, [isActive])
const toggleClass = () => {
setActive(!isActive);
}
const buildDropdown = () => {
const splitArr = (arr, len) => {
let chunks = [], i = 0, n = arr.length;
while (i < n) {
chunks.push(arr.slice(i, i += len));
}
return chunks;
}
const buildList = (items) => {
return items.map((item, i) =>
<li
key={i}
className={`DropdownSelect__list-item ${activeItem == item.name ? "active" : ""} ${item.disabled ? "disabled" : ""}`}
onClick={() => itemClicked(item.name, selected, data.name)}
>
{item.name}
</li>
)
}
const itemClicked = (item, selected, search) => {
changeActiveItem(item)
selected(item, search)
}
const arrLen = list.length < 10 ? 3 : 4;
const listsArr = splitArr(list, arrLen);
return listsArr.map((list, i) =>
<ul key={i} className="DropdownSelect__list">
{buildList(list)}
</ul>
)
}
return (
<div className={`DropdownSelect ${className ? className : ''}`} ref={ref}>
<div
className={`DropdownSelect__button ${isActive ? "active" : ""}`}
onClick={toggleClass}
>
{activeItem == null ? data.placeholder : activeItem}
</div>
<div className={`DropdownSelect__list-wrapper ${isActive ? "active" : ""}`}>{buildDropdown}</div>
</div>
)
}
export default DropdownSelect
I feel like i could have all of my state in the listing page but then the Dropdown component is pointless as it wouldn't be self sufficient and usable elsewhere.
I guess I want to know how I break this loop but also what are my bad practices here? ie am I using state wrongly?
Any help greatly appreciated!
PS Here's the React error i get
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
If I'm reading this right...
In your render, you have {this.getCaseStudies(this.props.data.allCaseStudiesYaml.edges)}. getCaseStudies calls filterCaseStudies, which calls filterDropdowns, which has a setState in it. When a setState occurs, the page re-renders, causing the page to go through all those function calls again, another setState occurs, the page re-renders again, forever, causing an infinite loop.
You'll have to re-write your code somewhat. You could possibly use that state to store the data in a different format, like an array, and map the data in your render?

document.activeElement condition won't work (with id or ref inside a div element)

I'm trying to make a menu and that when pressing outside of it it will get closed and will active clearTimeout so it won't get close again if re-opened. Somehow when I press on the menu this condition is always true -
if (document.activeElement !== menuRef.current) {//document.activeElement.id !== 'myField'
<div ref={menuRef} id="myField"> is the wrapping div of the menu
So it doesn't work.
What am I doing here wrong ?
All in the codesandbox -
Codesandbox
export default function Example() {
const { toggleMenu } = useMenuState({ transition: true });
let closeTimeMenu;
const menuRef = useRef(null);
useEffect(() => {
function handleClickOutside() {
setTimeout(() => {
if (document.activeElement !== menuRef.current) {//document.activeElement.id !== 'myField'
console.log(" !== menuRef");
clearTimeout(closeTimeMenu);
window.onclick = toggleMenu(false);
}
}, 500);
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
function handleClick() {
//menuRef.current.focus(); or -
document.getElementById('myField').focus();
}
function timeout() {
closeTimeMenu = setTimeout(function () {
toggleMenu(false);
}, 1500);
}
return (
<div ref={menuRef} id="myField">
<button
onMouseEnter={() => {
toggleMenu(true);
clearTimeout(closeTimeMenu);
}}
onMouseLeave={() => timeout()}
>
Hover to Open
</button>
<ControlledMenu
anchorRef={menuRef}
onMouseEnter={() => {
toggleMenu(true);
clearTimeout(closeTimeMenu);
}}
onItemClick={() => {
handleClick();
toggleMenu(false);
}}
onMouseLeave={() => timeout()}
>
<MenuItem>Save</MenuItem>
<MenuItem>Close Window</MenuItem>
</ControlledMenu>
</div>
);
}
Thanks

how to use clearInteval let timer clear it self in ReactJS?

I am new to react, I am trying to write a react component, component has several features.
user can input a random number, then number will be displayed in the
page too.
implement a button with text value 'start', once click the button,
the number value displayed will reduce one every 1second and the
text value will become 'stop'.
continue click button, minus one will stop and text value of button
will become back to 'start'.
when number subtracted down to 0 will automatically stop itself.
I have implemented first three features. but I am not sure how do I start the last one. should I set another clearInteval? based on if statement when timer counts down 0?
code is here:
var myTimer;
class App extends Component {
constructor(props) {
super(props);
this.state = {
details: [{ id: 1, number: "" }],
type: false
};
this.handleClick = this.handleClick.bind(this);
}
changeNumber = (e, target) => {
this.setState({
details: this.state.details.map(detail => {
if (detail.id === target.id) {
detail.number = e.target.value;
}
return detail;
})
});
};
handleClick = () => {
this.setState(prevState => ({
type: !prevState.type
}));
if (this.state.type === false) {
myTimer = setInterval(
() =>
this.setState({
details: this.state.details.map(detail => {
if (detail.id) {
detail.number = parseInt(detail.number) - 1;
}
return detail;
})
}),
1000
);
}
if (this.state.type === true) {
clearInterval(myTimer);
}
};
render() {
return (
<div>
{this.state.details.map(detail => {
return (
<div key={detail.id}>
Number:{detail.number}
<input
type="number"
onChange={e => this.changeNumber(e, detail)}
value={detail.number}
/>
<input
type="button"
onClick={() => this.handleClick()}
value={this.state.type ? "stop" : "start"}
/>
</div>
);
})}
</div>
);
}
}
export default App;
just add
if (detail.number === 0) {
clearInterval(myTimer);
}
in
handleClick = () => {
this.setState(prevState => ({
type: !prevState.type
}));
if (this.state.type === false) {
myTimer = setInterval(
() =>
this.setState({
details: this.state.details.map(detail => {
if (detail.id) {
detail.number = parseInt(detail.number) - 1;
if (detail.number === 0) {
clearInterval(myTimer);
}
}
return detail;
})
}),
1000
);
}
if (this.state.type === true) {
clearInterval(myTimer);
}
};
Here You have this solution on Hooks :)
const Test2 = () => {
const [on, setOn] = useState(false)
const initialDetails = [{ id: 1, number: "" }]
const [details, setDetails] = useState(initialDetails)
const changeNumber = (e, target) => {
setDetails({ details: details.map(detail => { if (detail.id === target.id) { detail.number = e.target.value; } return detail; }) });
if (this.state.details.number === 0) { setOn(false) }
};
const handleClick = () => {
if (on === false) {myTimer = setInterval(() =>
setDetails({details: details.map(detail => {if (detail.id) {detail.number = parseInt(detail.number) - 1; if (detail.number === 0) {clearInterval(myTimer);} }
return detail;})}),1000);}
if (on === true) { clearInterval(myTimer); }
};
return (
<div>
{details.map(detail => {
return (
<div key={detail.id}>
Number:{detail.number}
<input
type="number"
onChange={e => changeNumber(e, detail)}
value={detail.number}
/>
<input
type="button"
onClick={() => handleClick()}
value={on ? "stop" : "start"}
/>
</div>
);
})}
</div>
)
}

PanHandler doesn't detect touch events inside Modal

I have added a PanHandler to check if my app goes idle, and when it does, it shows a warning. The PanHandler works for all the content inside the page, but not when a Modal is shown. The touch events inside the modal seems to be ignored. The code is as follows.
render() {
return (
<View style={styles.mainContainer}
collapsable={false}
{...this.pagePanResponder.panHandlers}>
{this.addModal()}
//Content
</View>
);
addModal() {
return (
<Modal
animationType="fade"
transparent={true}
visible={this.state.addModalVisible}>
//Content
</Modal>
)
}
pageLastInteraction = new Date();
pagePanResponder = {};
componentWillMount() {
this.panResponderSetup();
}
panResponderSetup() {
this.pagePanResponder = PanResponder.create({
onStartShouldSetPanResponder: this.handleStartShouldSetPanResponder,
onMoveShouldSetPanResponder: this.handleMoveShouldSetPanResponder,
onResponderTerminationRequest: () => false,
onStartShouldSetPanResponderCapture: () => false,
onMoveShouldSetPanResponderCapture: () => false,
onPanResponderTerminationRequest: () => true,
onShouldBlockNativeResponder: () => false,
});
this._maybeStartWatchingForInactivity();
}
_maybeStartWatchingForInactivity = () => {
if (this._inactivityTimer) {
return;
}
this._inactivityTimer = setInterval(() => {
if (
new Date() - this.pageLastInteraction >= TIME_TO_WAIT_FOR_INACTIVITY_MS
) {
this._setIsInactive();
}
}, INACTIVITY_CHECK_INTERVAL_MS);
};
_setIsActive = () => {
this.pageLastInteraction = new Date();
if (this.state.timeWentInactive) {
this.setState({ timeWentInactive: null, isIdle: false });
}
this._maybeStartWatchingForInactivity();
};
_setIsInactive = () => {
console.log("PAGE WENT IDLE");
if (this.idleWarningModal != null) {
this.setState({
addModalVisible: false
}, () => {
this.idleWarningModal.showModal();
});
}
this.setState({ timeWentInactive: new Date(), isIdle: true });
clearInterval(this._inactivityTimer);
this._inactivityTimer = null;
};
handleStartShouldSetPanResponder = () => {
this._setIsActive();
return false;
};
handleMoveShouldSetPanResponder = () => {
this._setIsActive();
return false;
};
All the events in the main view seems to get covered by the PanHandler, except the content in the Modal. How to add the PanHandler to the Modal as well?
React Native Version - 0.55.4
React Version - 16.3.1
Well, stupid mistake.
onResponderTerminationRequest: () => this.handleMoveShouldSetPanResponder
This fixed the issue. As onPress events were ignored by my logic before.

How implement toggle with Rxjs

I am learning rxjs. I create decorator "toggleable" for Dropdown component. All work fine, but I don't like it. How can I remove condition "toggle/hide".
Uses rxjs, react.js, recompose.
It's toogleable decorator for Dropdown component.
export const toggleable = Wrapped => componentFromStream((props$) => {
// toogleHandler called with onClick
const { handler: toogleHandler, stream: toogle$ } = createEventHandler();
// hideHandler called with code below
const { handler: hideHandler, stream: hide$ } = createEventHandler();
const show$ = Observable.merge(
toogle$.mapTo('toogle'),
hide$.mapTo('hide'))
.startWith(false)
.scan((state, type) => {
if (type === 'toogle') {
return !state;
}
if (type === 'hide') {
return false;
}
return state;
});
return props$
.combineLatest(
show$,
(props, show) => (
<Wrapped
{...props}
show={show}
onToggle={toogleHandler}
onHide={hideHandler}
/>
));
});
It's decorator for Dropdown button
// hideHandler caller
class Foo extends Component {
constructor(props) {
super(props);
this.refButton.bind(this);
this.documentClick$ = Observable.fromEvent(global.document, 'click')
.filter(event => this.button !== event.target)
.do((event) => { this.props.onHide(event); });
}
componentDidMount() {
this.documentClick$.subscribe();
}
componentWillUnmount() {
this.documentClick$.unsubscribe();
}
refButton = (ref) => {
this.button = ref;
}
}
You can implement show$ with no conditions by mapping the toggle$/hide$ to functions on the previous state:
const show$ = Observable.merge(
toggle$.mapTo(prev => !prev),
hide$.mapTo(prev => false))
.startWith(false)
.scan((state, changeState) => changeState(state));
Another improvement you can do is with your toggleable implementation. Instead of using recompose componentFromStream, you can use recompose mapPropsStream:
export const toggleable = mapPropsStream(props$ => {
const { handler: toogleHandler, stream: toogle$ } = createEventHandler();
const { handler: hideHandler, stream: hide$ } = createEventHandler();
const show$ = Observable.merge(
toggle$.map(() => prev => !prev),
hide$.map(() => prev => false))
.startWith(false)
.scan((state, changeState) => changeState(state));
return props$
.combineLatest(
show$,
(props, show) => ({
...props,
show
onToggle: toogleHandler
onHide: hideHandler
})
);
});

Categories