Automatically close dropdown when a new dropdown is selected/opened (React) - javascript

I have a dropdown component and I'd like to be able to automatically close the previous dropdown when you click on a different dropdown menu item. I have the dropdown component working but I can't get them to close after selecting a new item. Additionally I'd like to close the items if you click anywhere on the page. Any help would be greatly appreciated, thanks in advance!
export const Dropdown: FC<Props> = ({ passedBindings }) => {
let [isDropdownOpen, setDropdownOpen] = useState(false);
const [ firstMediaBindings, ...restMediaBindings ] = bindings.mediaFlagBindings||[{}];
const toggleDropdown = () => {
setDropdownOpen(!isDropdownOpen)
};
return (
<div { ...optionalAttributes }>
<Container>
{
firstMoleculeMediaFlag()
}
{isDropdownOpen && restMediaBindings.length > 0 &&
<Container passedBindings={({
padding: {
direction: "all",
size: "size2"
}
})}>
{
restMediaBindings.map((mediaFlagBindings: IMoleculeMediaFlag, index: number) => {
return (
<Container
key={index}
passedBindings={({
padding: {
direction: "all",
size: "size1"
}
})}>
<MediaFlag key={index} passedBindings={mediaFlagBindings} />
</Container>
)
})
}
</Container>
}
</Container>
</Container>
</div>
)
``

You need a backdrop DIV to allow click and exit anywhere on the page.
It's like a 3-layer system.
1st : your page content
2nd : the backdrop goes on top
3rd : your dropdown goes on top of the backdrop
An example (using styled-components for style, but you can style as you wish):
Backdrop.js
This renders a div on top of your page content.
const Backdrop = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.2);
z-index: 100;
`;
Use it like that: conditionally with isDropdownOpen
Dropdown.js
But remember to style your dropdown with a z-index with something higher than the z-index you used on the backdrop. In this example, I used 100 for the backdrop so you can use 200.
export const Dropdown() {
...
return(
isDropdownOpen ?
<Backdrop onClick={toggleDropdown}/>
// Here goes the rest of your return for your dropdown
);
}
If you need one dropdown to close the previous when they are clicked you need something in your state that tells you which one is opened.
Something like:
const [indexDropdownOpened, setIndexDropdownOpened] = useState(false);
You can set it to false (initial value) when no dropdown is opened, and you can set it with an index (or key) to tell your component which one is openend.
On each dropdown, when you render them:
...
return(
<Container
key={index}
onClick={()=>setIndexDropdownOpened(index)}
/>
);
Then, on the backdrop, you could do:
<Backdrop onClick={()=>setIndexDropdownOpnened(false)}/> // So it closes the dropdown

Related

React Toggle Body Class with button

I'm still learning React but I'm having an issue toggling a body class with a button in the menu.
const toggleSideMenu = event => {
// toggle class on click
//Below is not correct
event.getElementsByTagName('body').classList.toggle('sb-sidenav-toggled');
};`
<button onClick={toggleSideMenu} id="sidebarToggle" href="#!"><i className="fas fa-bars"></i></button>
I'm used to doing this easily in jQuery but it's not recommended to use jQuery in React because of the dom. I would appreciate any suggestions.
Thanks so much!
In this example, we are using the useState hook to keep track of the toggle state. The initial state is set to false. We are using the isToggled state in the JSX to determine what to render on the screen, and to update the text of the button.
We have an onClick event on the button, which calls the setIsToggled function and pass the negation of the current state (!isToggled), this is the way to toggle the state, every time the button is clicked.
import React, { useState } from 'react';
const MyComponent = () => {
// useState hook to keep track of the toggle state
const [isToggled, setIsToggled] = useState(false);
return (
<div>
{/* render some content or change className based on the toggle state */}
<p className={isToggled? "class1" : "classB">Toggled on</p>
<button onClick={() => setIsToggled(!isToggled)}>
{isToggled ? 'Turn off' : 'Turn on'}
</button>
</div>
);
}
export default MyComponent;
But if you need to do something more advanced, maybe you can learn more about React Context.
import React, { useState } from 'react';
// Create a context to share the toggle state
const ToggleContext = React.createContext();
const MyApp = () => {
// useState hook to keep track of the toggle state
const [isToggled, setIsToggled] = useState(false);
return (
<ToggleContext.Provider value={{ isToggled, setIsToggled }}>
<MyComponent1 />
<MyComponent2 />
{/* any other components that need access to the toggle state */}
</ToggleContext.Provider>
);
}
const MyComponent1 = () => {
// use the toggle state and toggle function from the context
const { isToggled, setIsToggled } = useContext(ToggleContext);
return (
<div>
<p className={isToggled? "class1" : "classB">Toggled on</p>
<button onClick={() => setIsToggled(!isToggled)}>
{isToggled ? 'Turn off' : 'Turn on'}
</button>
</div>
);
}
const MyComponent2 = () => {
// use the toggle state from the context
const { isToggled } = useContext(ToggleContext);
return (
<div>
{isToggled ? <p>Toggled on</p> : <p>Toggled off</p>}
</div>
);
}
export default MyApp;
A very basic example to show you how to use state to maintain whether a menu should be open or not.
It has one button that when clicked calls a function that updates the state.
It has one Menu component that accepts that state, and uses CSS to determine whether it should be "open" (ie on/off screen).
Like I said, as simple as I could make it.
const { useState } = React;
function Example() {
// The state set to either true or false
// Initially it's false / menu closed
const [ menuOpen, setMenuOpen ] = useState(false);
// When the button is clicked we take the
// previous state and toggle it - either from true
// to false, or vice versa
function handleClick() {
setMenuOpen(prev => !prev);
}
// One Menu component that accepts that state
// and one button that updates the state
return (
<div>
<Menu open={menuOpen} />
<button onClick={handleClick}>
Toggle Sidebar Menu
</button>
</div>
);
}
// Small menu (an aside element) which uses CSS
// to work out its position on the screen
// It does this by creating a classList using the default
// "menu" which it ties together with "open" but it only
// adds that if the state is true
// And then just use that joined array as the className on
// the element
// You can see in the CSS what both those classes do
function Menu({ open }) {
const menuStyle = [
'menu',
open && 'open'
].join(' ');
return (
<aside className={menuStyle}>
I am a sidebar
</aside>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
.menu { width: 100px; top: 0px; left: -120px; background-color: salmon; position: fixed; height: 100vh; padding: 10px; transition-property: left; transition-duration: 0.25s;}
.open { left: 0px; }
button { position: fixed; left: 150px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
getElementsByTagName() is method of Document or Element, not react event.
What you need to do, is to look for body inside document.
Also getElementsByTagName(), returns HTMLCollection (many elements), so you need to grab first one (usually there is only one body element on page)
document.getElementsByTagName('body')[0].classList.toggle('sb-sidenav-toggled');
There is also shortcut for body element document.body, so it can be also written as:
document.body.classList.toggle('sb-sidenav-toggled');

Adding a div to the bottom of a list view pushes all view upward out of viewport

I am implementing a chat view in React,and the desired behavior is that whenever new data is pushed into the dataSource, the chat view (an infinite list) scrolls to the bottom, there are many implementations, some found here: How to scroll to bottom in react?. However when I try to implement it, I am getting this strange behavior where all the views in the window are "pushed upward" out of sight by some 300px, as if to accomadate this new <div> that is at the bottom of the list view. My implementation below:
import React, {useEffect, useRef} from "react";
import { createStyles, makeStyles, Theme } from "#material-ui/core/styles";
import InfiniteScroll from 'react-infinite-scroll-component';
const row_1 = 2.5;
const row_chat = 4
const useStyles = makeStyles((theme: Theme) => createStyles({
container: {
width: '40vw',
height: `calc(100vh - 240px)`,
position: 'relative',
padding: theme.spacing(3),
},
}));
const chat_container_style = {
width: '40vw',
height: `calc(100vh - 240px - ${row_chat}vh - ${row_1}vh)`,
}
function ChatView(props) {
const classes = useStyles();
const { _dataSource } = props;
// scroll to bottom
const messagesEndRef = useRef(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
useEffect(() => {
scrollToBottom()
}, [_dataSource]);
return (
<div className={classes.container}>
{/* chat window */}
<InfiniteScroll
dataLength={_dataSource.length}
next={() => { return }}
hasMore={true}
loader={<></>}
style={chat_container_style}
>
{_dataSource.map((item, index) => {
return (
<div {...props} key={index} item={item}>
{`item: ${index}`}
</div>
)
})}
{/* putting an item here push all divs upward */}
<div ref={messagesEndRef} />
</InfiniteScroll>
</div>
)
}
Note the use of <InfiniteScroll/> is not the cause of the behavior, Really if I put the ref={messagesEndRef} into any view, it pushes all the views up in the viewport.
The issue has been resolved. The source of the issue is the scrollIntoView function, it's scrolling the entire page instead of just the listView, here's the correct scrollIntoView function with the correct parameters:
const scrollDivRef = createRef();
useEffect(() => {
scrollDivRef.current?.scrollIntoView({
block : 'nearest',
inline : 'start',
behavior: 'smooth',
})
}, [_dataSource.length]);
Here's how the ref is nested inside the DOM:
<InfiniteScroll
next={() => { return }}
hasMore={true}
loader={<></>}
style={chat_container_style}
dataLength={_dataSource.length}
>
{_dataSource.map((item, index) => (
<BubbleView {...props} key={index} item={item}/>
))}
<div style={refDivStyle} ref={scrollDivRef}/>
</InfiniteScroll>
This problem has nothing to do w/ how I layout the style sheet.

How to change image src when onMouseEnter div in react stateless component

I'm beginner in react.
I want to change my image src when mouse enter the div.
Here is my code.
const CategoryImage = styled.img.attrs(props => ({
src: props.url,
}))`
width: 80px;
height: 80px;
margin: 5px auto;
`;
let imgUrl = ``;
const Category = ({ categoryItems }) => {
function handleHover(category) {
const {
category: { hoverUrl },
} = category;
// console.log(hoverUrl);
imgUrl = hoverUrl;
}
function handleUnHover(category) {
const {
category: { url },
} = category;
// console.log(url);
imgUrl = url;
}
return (
<Container>
<Grid>
{categoryItems.map(category => (
<CategoryContainer
key={category.id}
onMouseEnter={() => handleHover({ category })}
onMouseLeave={() => handleUnHover({ category })}
>
<CategoryImage url={imgUrl} alt={category.name} />
<CategoryName key={category.id}> {category.name} </CategoryName>
</CategoryContainer>
))}
</Grid>
</Container>
);
};
Can I change image without using state?
Most of Questions usually use state to change image. I think state isn't needed when changes occurs in my case(codes) though.
And, I heard that performance usually better without using state. Is that right?
Always Appreciate u guys:)
In case of 2 images , just add css property. Hide it by display none , and position all the images at top ....
On mouse over or enter , in this event , pass the class name , that's it .....
I did this task long back, but can't remember exactly what I had done ,
Try this

How do you remove a class from elements inside a component in React.JS when clicking outside given component?

I am trying to emulate a behavior similar to clicking on the overlay when a Modal popup is open. When clicking outside the sidenav component, I want to close all elements that are currently in a flyout mode.
I have a multi-tier nested navigation menu that is stored in its own component, Sidebar. I have the following piece of code that handles clicks that occur outside the Sidebar component:
class Sidebar extends React.Component {
...
handleClick = (e) => {
if (this.node.contains(e.target)) {
return;
}
console.log('outside');
};
componentDidMount() {
window.addEventListener('mousedown', this.handleClick, false);
}
componentWillUnmount() {
window.removeEventListener('mousedown', this.handleClick, false);
}
render() {
return (
<div
ref={node => this.node = node}
className="sidebar"
data-color={this.props.bgColor}
data-active-color={this.props.activeColor}
>
{renderSideBar()}
</div>
);
}
...
}
This part works fine - but when the flyout menus get displayed on clicking a parent menu option, I would like it to close -any- flyout menus that are currently opened.
-|
|
- Menu Item 1
|
|-option 1 (currently open)
|-option 2
- Menu Item 2
|
|-option 1 (closed)
|-option 2 (closed, clicked to expand - this is when it should close [Menu Item 1/Option 1]
The menu items are generated using <li> tags when mapping the data object containing the menu structure.
Is there a way to basically select all registered objects that have the class of 'collapse' / aria-expanded="true" and remove it? Similar to how jQuery would select dom elements and manipulate them.
I know that this is not the premise in which React works, it is just an example of the behavior I want to emulate.
As far as I understand you want to modify the DOM subtree from another component. To achive your goal you can use ref.
Using ref is helpful when you want to access HtmlElement API directly - in my example I use animate(). Please, read the documentation as it describes more of ref use cases.
Below is the simple example of animating <Sidebar/> shrinking when user clicks on <Content />.
const { useRef } = React;
function Main() {
const sidebar = useRef(null);
const handleClick = () => {
sidebar.current.hide();
};
return (
<div className="main">
<Sidebar ref={sidebar} />
<Content onClick={handleClick} />
</div>
);
}
class Sidebar extends React.Component {
constructor(props) {
super(props);
this.state = { visible: true };
this.show = this.show.bind(this);
this.sidebar = React.createRef(null);
}
show() {
if (!this.state.visible) {
this.sidebar.current.animate(
{ flex: [1, 2], "background-color": ["teal", "red"] },
300
);
this.setState({ visible: true });
}
}
hide() {
if (this.state.visible) {
this.sidebar.current.animate(
{ flex: [2, 1], "background-color": ["red", "teal"] },
300
);
this.setState({ visible: false });
}
}
render() {
return (
<div
ref={this.sidebar}
className={this.state.visible ? "sidebar--visible" : "sidebar"}
onClick={this.show}
>
Sidebar
</div>
);
}
}
function Content({ onClick }) {
return (
<div className="content" onClick={onClick}>
Content
</div>
);
}
ReactDOM.render(<Main />, document.getElementById("root"));
.main {
display: flex;
height: 100vh;
}
.sidebar {
flex: 1;
background-color: teal;
}
.sidebar--visible {
flex: 2;
background-color: red;
}
.content {
flex: 7;
background-color: beige;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Clicking on image 'pushes' the rest of the list underneath image

I am trying to build a simple list component that displays images when a title is clicked. For each title you can open the corresponding image(s), that al works fine, but the problem is that the rest of the titles (the list) is pushed underneath the image(s).
The desired result should be that the images are displayed to the right of the list without affecting the list.
I've tried separating the component into two components, so where the image is called in the ternary operator putting a component like <ProjectImage src={project.src} alt={project.altText}/> but that doesn't seem to fix the problem.
import {items} from '../ProjectInfo/projectObjects'
export class Sidenav extends Component {
constructor(props) {
super(props);
this.state = {isToggleOn: false, items: items};
this.showProjectOnClick = this.showProjectOnClick.bind(this);
}
showProjectOnClick(event){
const checkActive = this.state.items.id === items.id
const activeProject = {...event, active: !checkActive}
this.setState(state => ({
isToggleOn: !state.isToggleOn,
activeProject
}));
}
render() {
const {items} = this.state
return (
<div className="sidenav">
{items.map((project) => {
return ( <div className="Box" key={project.id}>
<p className={this.state.isToggleOn && this.state.activeProject.id === project.id ? 'P_Color' : null}
onClick={() => {this.showProjectOnClick(project)}}><b>Project name: </b>{project.title}</p>
{
this.state.isToggleOn && this.state.activeProject.id === project.id
?
<div className="ProjectImageBox">
<img className="ProjectImage" src={project.src} alt={project.altText}/>
</div>
: ''
}
</div>)
})}
</div>
)
}
}
The project list
The current result when a title is clicked
The desired result
You could do it with css.
.image{
position: absolute;
top: 0;
right: 40px;
}
But a better option would be using a different component and passing the selected item to this component. This reduces the amount of components and you just have to position them accordingly. I have created a simple stackblitz showing how this works.
To do this you have to absolute position you image so it doesn't take up the space in the list.
the css class should be the following:
.image {
position: "absolute";
top: 0;
left: "100%";
/* the propreties below you can customize */
width: "50px";
height: "auto";
}

Categories